Skip to main content

backbone_maintenance/domain/entity/
maintenance_visit.rs

1use chrono::{DateTime, Utc, NaiveDate};
2use serde::{Deserialize, Serialize};
3use sqlx::FromRow;
4use uuid::Uuid;
5use rust_decimal::Decimal;
6
7use super::MaintenanceType;
8use super::VisitStatus;
9use super::AuditMetadata;
10
11/// Strongly-typed ID for MaintenanceVisit
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13#[serde(transparent)]
14pub struct MaintenanceVisitId(pub Uuid);
15
16impl MaintenanceVisitId {
17    pub fn new(id: Uuid) -> Self { Self(id) }
18    pub fn generate() -> Self { Self(Uuid::new_v4()) }
19    pub fn into_inner(self) -> Uuid { self.0 }
20}
21
22impl std::fmt::Display for MaintenanceVisitId {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        write!(f, "{}", self.0)
25    }
26}
27
28impl std::str::FromStr for MaintenanceVisitId {
29    type Err = uuid::Error;
30    fn from_str(s: &str) -> Result<Self, Self::Err> {
31        Ok(Self(Uuid::parse_str(s)?))
32    }
33}
34
35impl From<Uuid> for MaintenanceVisitId {
36    fn from(id: Uuid) -> Self { Self(id) }
37}
38
39impl From<MaintenanceVisitId> for Uuid {
40    fn from(id: MaintenanceVisitId) -> Self { id.0 }
41}
42
43impl AsRef<Uuid> for MaintenanceVisitId {
44    fn as_ref(&self) -> &Uuid { &self.0 }
45}
46
47impl std::ops::Deref for MaintenanceVisitId {
48    type Target = Uuid;
49    fn deref(&self) -> &Self::Target { &self.0 }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
53pub struct MaintenanceVisit {
54    pub id: Uuid,
55    pub asset_id: Uuid,
56    pub schedule_id: Option<Uuid>,
57    pub maintenance_type: MaintenanceType,
58    pub status: VisitStatus,
59    pub warehouse_id: Option<Uuid>,
60    pub warranty_claim_id: Option<Uuid>,
61    pub scheduled_date: NaiveDate,
62    pub performed_date: Option<NaiveDate>,
63    pub labor_cost: Decimal,
64    pub parts_cost: Decimal,
65    pub total_cost: Decimal,
66    pub maintenance_expense_account_id: Option<Uuid>,
67    pub parts_inventory_account_id: Option<Uuid>,
68    pub labor_payable_account_id: Option<Uuid>,
69    pub journal_id: Option<Uuid>,
70    pub accounting_post_id: Option<Uuid>,
71    pub notes: Option<String>,
72    #[serde(default)]
73    #[sqlx(json)]
74    pub metadata: AuditMetadata,
75}
76
77impl MaintenanceVisit {
78    /// Create a builder for MaintenanceVisit
79    pub fn builder() -> MaintenanceVisitBuilder {
80        <MaintenanceVisitBuilder as Default>::default()
81    }
82
83    /// Create a new MaintenanceVisit with required fields
84    pub fn new(asset_id: Uuid, maintenance_type: MaintenanceType, status: VisitStatus, scheduled_date: NaiveDate, labor_cost: Decimal, parts_cost: Decimal, total_cost: Decimal) -> Self {
85        Self {
86            id: Uuid::new_v4(),
87            asset_id,
88            schedule_id: None,
89            maintenance_type,
90            status,
91            warehouse_id: None,
92            warranty_claim_id: None,
93            scheduled_date,
94            performed_date: None,
95            labor_cost,
96            parts_cost,
97            total_cost,
98            maintenance_expense_account_id: None,
99            parts_inventory_account_id: None,
100            labor_payable_account_id: None,
101            journal_id: None,
102            accounting_post_id: None,
103            notes: None,
104            metadata: AuditMetadata::default(),
105        }
106    }
107
108    /// Get the entity's unique identifier
109    pub fn id(&self) -> &Uuid {
110        &self.id
111    }
112
113    /// Get a strongly-typed ID for this entity
114    pub fn typed_id(&self) -> MaintenanceVisitId {
115        MaintenanceVisitId(self.id)
116    }
117
118    /// Get when this entity was created
119    pub fn created_at(&self) -> Option<&DateTime<Utc>> {
120        self.metadata.created_at.as_ref()
121    }
122
123    /// Get when this entity was last updated
124    pub fn updated_at(&self) -> Option<&DateTime<Utc>> {
125        self.metadata.updated_at.as_ref()
126    }
127
128    /// Check if this entity is soft deleted
129    pub fn is_deleted(&self) -> bool {
130        self.metadata.deleted_at.is_some()
131    }
132
133    /// Check if this entity is active (not deleted)
134    pub fn is_active(&self) -> bool {
135        self.metadata.deleted_at.is_none()
136    }
137
138    /// Get when this entity was deleted
139    pub fn deleted_at(&self) -> Option<&DateTime<Utc>> {
140        self.metadata.deleted_at.as_ref()
141    }
142
143    /// Get who created this entity
144    pub fn created_by(&self) -> Option<&Uuid> {
145        self.metadata.created_by.as_ref()
146    }
147
148    /// Get who last updated this entity
149    pub fn updated_by(&self) -> Option<&Uuid> {
150        self.metadata.updated_by.as_ref()
151    }
152
153    /// Get who deleted this entity
154    pub fn deleted_by(&self) -> Option<&Uuid> {
155        self.metadata.deleted_by.as_ref()
156    }
157
158    /// Get the current status
159    pub fn status(&self) -> &VisitStatus {
160        &self.status
161    }
162
163
164    // ==========================================================
165    // Fluent Setters (with_* for optional fields)
166    // ==========================================================
167
168    /// Set the schedule_id field (chainable)
169    pub fn with_schedule_id(mut self, value: Uuid) -> Self {
170        self.schedule_id = Some(value);
171        self
172    }
173
174    /// Set the warehouse_id field (chainable)
175    pub fn with_warehouse_id(mut self, value: Uuid) -> Self {
176        self.warehouse_id = Some(value);
177        self
178    }
179
180    /// Set the warranty_claim_id field (chainable)
181    pub fn with_warranty_claim_id(mut self, value: Uuid) -> Self {
182        self.warranty_claim_id = Some(value);
183        self
184    }
185
186    /// Set the performed_date field (chainable)
187    pub fn with_performed_date(mut self, value: NaiveDate) -> Self {
188        self.performed_date = Some(value);
189        self
190    }
191
192    /// Set the maintenance_expense_account_id field (chainable)
193    pub fn with_maintenance_expense_account_id(mut self, value: Uuid) -> Self {
194        self.maintenance_expense_account_id = Some(value);
195        self
196    }
197
198    /// Set the parts_inventory_account_id field (chainable)
199    pub fn with_parts_inventory_account_id(mut self, value: Uuid) -> Self {
200        self.parts_inventory_account_id = Some(value);
201        self
202    }
203
204    /// Set the labor_payable_account_id field (chainable)
205    pub fn with_labor_payable_account_id(mut self, value: Uuid) -> Self {
206        self.labor_payable_account_id = Some(value);
207        self
208    }
209
210    /// Set the journal_id field (chainable)
211    pub fn with_journal_id(mut self, value: Uuid) -> Self {
212        self.journal_id = Some(value);
213        self
214    }
215
216    /// Set the accounting_post_id field (chainable)
217    pub fn with_accounting_post_id(mut self, value: Uuid) -> Self {
218        self.accounting_post_id = Some(value);
219        self
220    }
221
222    /// Set the notes field (chainable)
223    pub fn with_notes(mut self, value: String) -> Self {
224        self.notes = Some(value);
225        self
226    }
227
228    // ==========================================================
229    // Partial Update
230    // ==========================================================
231
232    /// Apply partial updates from a map of field name to JSON value
233    pub fn apply_patch(&mut self, fields: std::collections::HashMap<String, serde_json::Value>) {
234        for (key, value) in fields {
235            match key.as_str() {
236                "asset_id" => {
237                    if let Ok(v) = serde_json::from_value(value) { self.asset_id = v; }
238                }
239                "schedule_id" => {
240                    if let Ok(v) = serde_json::from_value(value) { self.schedule_id = v; }
241                }
242                "maintenance_type" => {
243                    if let Ok(v) = serde_json::from_value(value) { self.maintenance_type = v; }
244                }
245                "status" => {
246                    if let Ok(v) = serde_json::from_value(value) { self.status = v; }
247                }
248                "warehouse_id" => {
249                    if let Ok(v) = serde_json::from_value(value) { self.warehouse_id = v; }
250                }
251                "warranty_claim_id" => {
252                    if let Ok(v) = serde_json::from_value(value) { self.warranty_claim_id = v; }
253                }
254                "scheduled_date" => {
255                    if let Ok(v) = serde_json::from_value(value) { self.scheduled_date = v; }
256                }
257                "performed_date" => {
258                    if let Ok(v) = serde_json::from_value(value) { self.performed_date = v; }
259                }
260                "labor_cost" => {
261                    if let Ok(v) = serde_json::from_value(value) { self.labor_cost = v; }
262                }
263                "parts_cost" => {
264                    if let Ok(v) = serde_json::from_value(value) { self.parts_cost = v; }
265                }
266                "total_cost" => {
267                    if let Ok(v) = serde_json::from_value(value) { self.total_cost = v; }
268                }
269                "maintenance_expense_account_id" => {
270                    if let Ok(v) = serde_json::from_value(value) { self.maintenance_expense_account_id = v; }
271                }
272                "parts_inventory_account_id" => {
273                    if let Ok(v) = serde_json::from_value(value) { self.parts_inventory_account_id = v; }
274                }
275                "labor_payable_account_id" => {
276                    if let Ok(v) = serde_json::from_value(value) { self.labor_payable_account_id = v; }
277                }
278                "journal_id" => {
279                    if let Ok(v) = serde_json::from_value(value) { self.journal_id = v; }
280                }
281                "accounting_post_id" => {
282                    if let Ok(v) = serde_json::from_value(value) { self.accounting_post_id = v; }
283                }
284                "notes" => {
285                    if let Ok(v) = serde_json::from_value(value) { self.notes = v; }
286                }
287                _ => {} // ignore unknown fields
288            }
289        }
290    }
291
292    // <<< CUSTOM METHODS START >>>
293    // <<< CUSTOM METHODS END >>>
294}
295
296impl super::Entity for MaintenanceVisit {
297    type Id = Uuid;
298
299    fn entity_id(&self) -> &Self::Id {
300        &self.id
301    }
302
303    fn entity_type() -> &'static str {
304        "MaintenanceVisit"
305    }
306}
307
308impl backbone_core::PersistentEntity for MaintenanceVisit {
309    fn entity_id(&self) -> String {
310        self.id.to_string()
311    }
312    fn set_entity_id(&mut self, id: String) {
313        if let Ok(uuid) = uuid::Uuid::parse_str(&id) {
314            self.id = uuid;
315        }
316    }
317    fn created_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
318        self.metadata.created_at
319    }
320    fn set_created_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
321        self.metadata.created_at = Some(ts);
322    }
323    fn updated_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
324        self.metadata.updated_at
325    }
326    fn set_updated_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
327        self.metadata.updated_at = Some(ts);
328    }
329    fn deleted_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
330        self.metadata.deleted_at
331    }
332    fn set_deleted_at(&mut self, ts: Option<chrono::DateTime<chrono::Utc>>) {
333        self.metadata.deleted_at = ts;
334    }
335}
336
337impl backbone_orm::EntityRepoMeta for MaintenanceVisit {
338    fn column_types() -> std::collections::HashMap<String, String> {
339        let mut m = std::collections::HashMap::new();
340        m.insert("id".to_string(), "uuid".to_string());
341        m.insert("asset_id".to_string(), "uuid".to_string());
342        m.insert("schedule_id".to_string(), "uuid".to_string());
343        m.insert("warehouse_id".to_string(), "uuid".to_string());
344        m.insert("warranty_claim_id".to_string(), "uuid".to_string());
345        m.insert("maintenance_expense_account_id".to_string(), "uuid".to_string());
346        m.insert("parts_inventory_account_id".to_string(), "uuid".to_string());
347        m.insert("labor_payable_account_id".to_string(), "uuid".to_string());
348        m.insert("journal_id".to_string(), "uuid".to_string());
349        m.insert("accounting_post_id".to_string(), "uuid".to_string());
350        m.insert("maintenance_type".to_string(), "maintenance_type".to_string());
351        m.insert("status".to_string(), "visit_status".to_string());
352        m.insert("scheduled_date".to_string(), "date".to_string());
353        m.insert("performed_date".to_string(), "date".to_string());
354        m
355    }
356    fn search_fields() -> &'static [&'static str] {
357        &[]
358    }
359}
360
361/// Builder for MaintenanceVisit entity
362///
363/// Provides a fluent API for constructing MaintenanceVisit instances.
364/// System fields (id, metadata, timestamps) are auto-initialized.
365#[derive(Debug, Clone, Default)]
366pub struct MaintenanceVisitBuilder {
367    asset_id: Option<Uuid>,
368    schedule_id: Option<Uuid>,
369    maintenance_type: Option<MaintenanceType>,
370    status: Option<VisitStatus>,
371    warehouse_id: Option<Uuid>,
372    warranty_claim_id: Option<Uuid>,
373    scheduled_date: Option<NaiveDate>,
374    performed_date: Option<NaiveDate>,
375    labor_cost: Option<Decimal>,
376    parts_cost: Option<Decimal>,
377    total_cost: Option<Decimal>,
378    maintenance_expense_account_id: Option<Uuid>,
379    parts_inventory_account_id: Option<Uuid>,
380    labor_payable_account_id: Option<Uuid>,
381    journal_id: Option<Uuid>,
382    accounting_post_id: Option<Uuid>,
383    notes: Option<String>,
384}
385
386impl MaintenanceVisitBuilder {
387    /// Set the asset_id field (required)
388    pub fn asset_id(mut self, value: Uuid) -> Self {
389        self.asset_id = Some(value);
390        self
391    }
392
393    /// Set the schedule_id field (optional)
394    pub fn schedule_id(mut self, value: Uuid) -> Self {
395        self.schedule_id = Some(value);
396        self
397    }
398
399    /// Set the maintenance_type field (default: `MaintenanceType::default()`)
400    pub fn maintenance_type(mut self, value: MaintenanceType) -> Self {
401        self.maintenance_type = Some(value);
402        self
403    }
404
405    /// Set the status field (default: `VisitStatus::default()`)
406    pub fn status(mut self, value: VisitStatus) -> Self {
407        self.status = Some(value);
408        self
409    }
410
411    /// Set the warehouse_id field (optional)
412    pub fn warehouse_id(mut self, value: Uuid) -> Self {
413        self.warehouse_id = Some(value);
414        self
415    }
416
417    /// Set the warranty_claim_id field (optional)
418    pub fn warranty_claim_id(mut self, value: Uuid) -> Self {
419        self.warranty_claim_id = Some(value);
420        self
421    }
422
423    /// Set the scheduled_date field (required)
424    pub fn scheduled_date(mut self, value: NaiveDate) -> Self {
425        self.scheduled_date = Some(value);
426        self
427    }
428
429    /// Set the performed_date field (optional)
430    pub fn performed_date(mut self, value: NaiveDate) -> Self {
431        self.performed_date = Some(value);
432        self
433    }
434
435    /// Set the labor_cost field (default: `Decimal::from(0)`)
436    pub fn labor_cost(mut self, value: Decimal) -> Self {
437        self.labor_cost = Some(value);
438        self
439    }
440
441    /// Set the parts_cost field (default: `Decimal::from(0)`)
442    pub fn parts_cost(mut self, value: Decimal) -> Self {
443        self.parts_cost = Some(value);
444        self
445    }
446
447    /// Set the total_cost field (default: `Decimal::from(0)`)
448    pub fn total_cost(mut self, value: Decimal) -> Self {
449        self.total_cost = Some(value);
450        self
451    }
452
453    /// Set the maintenance_expense_account_id field (optional)
454    pub fn maintenance_expense_account_id(mut self, value: Uuid) -> Self {
455        self.maintenance_expense_account_id = Some(value);
456        self
457    }
458
459    /// Set the parts_inventory_account_id field (optional)
460    pub fn parts_inventory_account_id(mut self, value: Uuid) -> Self {
461        self.parts_inventory_account_id = Some(value);
462        self
463    }
464
465    /// Set the labor_payable_account_id field (optional)
466    pub fn labor_payable_account_id(mut self, value: Uuid) -> Self {
467        self.labor_payable_account_id = Some(value);
468        self
469    }
470
471    /// Set the journal_id field (optional)
472    pub fn journal_id(mut self, value: Uuid) -> Self {
473        self.journal_id = Some(value);
474        self
475    }
476
477    /// Set the accounting_post_id field (optional)
478    pub fn accounting_post_id(mut self, value: Uuid) -> Self {
479        self.accounting_post_id = Some(value);
480        self
481    }
482
483    /// Set the notes field (optional)
484    pub fn notes(mut self, value: String) -> Self {
485        self.notes = Some(value);
486        self
487    }
488
489    /// Build the MaintenanceVisit entity
490    ///
491    /// Returns Err if any required field without a default is missing.
492    pub fn build(self) -> Result<MaintenanceVisit, String> {
493        let asset_id = self.asset_id.ok_or_else(|| "asset_id is required".to_string())?;
494        let scheduled_date = self.scheduled_date.ok_or_else(|| "scheduled_date is required".to_string())?;
495
496        Ok(MaintenanceVisit {
497            id: Uuid::new_v4(),
498            asset_id,
499            schedule_id: self.schedule_id,
500            maintenance_type: self.maintenance_type.unwrap_or_default(),
501            status: self.status.unwrap_or_default(),
502            warehouse_id: self.warehouse_id,
503            warranty_claim_id: self.warranty_claim_id,
504            scheduled_date,
505            performed_date: self.performed_date,
506            labor_cost: self.labor_cost.unwrap_or(Decimal::from(0)),
507            parts_cost: self.parts_cost.unwrap_or(Decimal::from(0)),
508            total_cost: self.total_cost.unwrap_or(Decimal::from(0)),
509            maintenance_expense_account_id: self.maintenance_expense_account_id,
510            parts_inventory_account_id: self.parts_inventory_account_id,
511            labor_payable_account_id: self.labor_payable_account_id,
512            journal_id: self.journal_id,
513            accounting_post_id: self.accounting_post_id,
514            notes: self.notes,
515            metadata: AuditMetadata::default(),
516        })
517    }
518}