Skip to main content

backbone_inventory/domain/entity/
delivery_note.rs

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