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 state_fields() -> &'static [&'static str] {
288        &["status"]
289    }
290    fn references() -> &'static [backbone_core::Reference] {
291        const REFERENCES: &[backbone_core::Reference] = &[
292            backbone_core::Reference::new("warehouse_id", "inventory.warehouses", "id"),
293            backbone_core::Reference::new("transfer_id", "inventory.transfers", "id"),
294        ];
295        REFERENCES
296    }
297    fn entity_id(&self) -> String {
298        self.id.to_string()
299    }
300    fn set_entity_id(&mut self, id: String) {
301        if let Ok(uuid) = uuid::Uuid::parse_str(&id) {
302            self.id = uuid;
303        }
304    }
305    fn created_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
306        self.metadata.created_at
307    }
308    fn set_created_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
309        self.metadata.created_at = Some(ts);
310    }
311    fn updated_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
312        self.metadata.updated_at
313    }
314    fn set_updated_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
315        self.metadata.updated_at = Some(ts);
316    }
317    fn deleted_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
318        self.metadata.deleted_at
319    }
320    fn set_deleted_at(&mut self, ts: Option<chrono::DateTime<chrono::Utc>>) {
321        self.metadata.deleted_at = ts;
322    }
323}
324
325impl backbone_orm::EntityRepoMeta for DeliveryNote {
326    fn column_types() -> std::collections::HashMap<String, String> {
327        let mut m = std::collections::HashMap::new();
328        m.insert("id".to_string(), "uuid".to_string());
329        m.insert("branch_id".to_string(), "uuid".to_string());
330        m.insert("customer_id".to_string(), "uuid".to_string());
331        m.insert("source_so_id".to_string(), "uuid".to_string());
332        m.insert("warehouse_id".to_string(), "uuid".to_string());
333        m.insert("cogs_account_id".to_string(), "uuid".to_string());
334        m.insert("inventory_account_id".to_string(), "uuid".to_string());
335        m.insert("transfer_id".to_string(), "uuid".to_string());
336        m.insert("journal_id".to_string(), "uuid".to_string());
337        m.insert("accounting_post_id".to_string(), "uuid".to_string());
338        m.insert("status".to_string(), "doc_status".to_string());
339        m.insert("posting_state".to_string(), "gl_posting_state".to_string());
340        m.insert("posting_date".to_string(), "date".to_string());
341        m.insert("posted_at".to_string(), "timestamptz".to_string());
342        m
343    }
344    fn search_fields() -> &'static [&'static str] {
345        &["delivery_number"]
346    }
347    fn relations() -> &'static [(&'static str, &'static str, &'static str)] {
348        &[("warehouse", "warehouses", "warehouseId"), ("transfer", "transfers", "transferId")]
349    }
350}
351
352/// Builder for DeliveryNote entity
353///
354/// Provides a fluent API for constructing DeliveryNote instances.
355/// System fields (id, metadata, timestamps) are auto-initialized.
356#[derive(Debug, Clone, Default)]
357pub struct DeliveryNoteBuilder {
358    delivery_number: Option<String>,
359    branch_id: Option<Uuid>,
360    customer_id: Option<Uuid>,
361    source_so_id: Option<Uuid>,
362    warehouse_id: Option<Uuid>,
363    posting_date: Option<NaiveDate>,
364    total_cogs: Option<Decimal>,
365    cogs_account_id: Option<Uuid>,
366    inventory_account_id: Option<Uuid>,
367    transfer_id: Option<Uuid>,
368    status: Option<DocStatus>,
369    posting_state: Option<GlPostingState>,
370    journal_id: Option<Uuid>,
371    accounting_post_id: Option<Uuid>,
372    posted_at: Option<DateTime<Utc>>,
373    notes: Option<String>,
374}
375
376impl DeliveryNoteBuilder {
377    /// Set the delivery_number field (required)
378    pub fn delivery_number(mut self, value: String) -> Self {
379        self.delivery_number = Some(value);
380        self
381    }
382
383    /// Set the branch_id field (optional)
384    pub fn branch_id(mut self, value: Uuid) -> Self {
385        self.branch_id = Some(value);
386        self
387    }
388
389    /// Set the customer_id field (required)
390    pub fn customer_id(mut self, value: Uuid) -> Self {
391        self.customer_id = Some(value);
392        self
393    }
394
395    /// Set the source_so_id field (optional)
396    pub fn source_so_id(mut self, value: Uuid) -> Self {
397        self.source_so_id = Some(value);
398        self
399    }
400
401    /// Set the warehouse_id field (required)
402    pub fn warehouse_id(mut self, value: Uuid) -> Self {
403        self.warehouse_id = Some(value);
404        self
405    }
406
407    /// Set the posting_date field (required)
408    pub fn posting_date(mut self, value: NaiveDate) -> Self {
409        self.posting_date = Some(value);
410        self
411    }
412
413    /// Set the total_cogs field (default: `Decimal::from(0)`)
414    pub fn total_cogs(mut self, value: Decimal) -> Self {
415        self.total_cogs = Some(value);
416        self
417    }
418
419    /// Set the cogs_account_id field (required)
420    pub fn cogs_account_id(mut self, value: Uuid) -> Self {
421        self.cogs_account_id = Some(value);
422        self
423    }
424
425    /// Set the inventory_account_id field (required)
426    pub fn inventory_account_id(mut self, value: Uuid) -> Self {
427        self.inventory_account_id = Some(value);
428        self
429    }
430
431    /// Set the transfer_id field (optional)
432    pub fn transfer_id(mut self, value: Uuid) -> Self {
433        self.transfer_id = Some(value);
434        self
435    }
436
437    /// Set the status field (default: `DocStatus::default()`)
438    pub fn status(mut self, value: DocStatus) -> Self {
439        self.status = Some(value);
440        self
441    }
442
443    /// Set the posting_state field (default: `GlPostingState::default()`)
444    pub fn posting_state(mut self, value: GlPostingState) -> Self {
445        self.posting_state = Some(value);
446        self
447    }
448
449    /// Set the journal_id field (optional)
450    pub fn journal_id(mut self, value: Uuid) -> Self {
451        self.journal_id = Some(value);
452        self
453    }
454
455    /// Set the accounting_post_id field (optional)
456    pub fn accounting_post_id(mut self, value: Uuid) -> Self {
457        self.accounting_post_id = Some(value);
458        self
459    }
460
461    /// Set the posted_at field (optional)
462    pub fn posted_at(mut self, value: DateTime<Utc>) -> Self {
463        self.posted_at = Some(value);
464        self
465    }
466
467    /// Set the notes field (optional)
468    pub fn notes(mut self, value: String) -> Self {
469        self.notes = Some(value);
470        self
471    }
472
473    /// Build the DeliveryNote entity
474    ///
475    /// Returns Err if any required field without a default is missing.
476    pub fn build(self) -> Result<DeliveryNote, String> {
477        let delivery_number = self.delivery_number.ok_or_else(|| "delivery_number is required".to_string())?;
478        let customer_id = self.customer_id.ok_or_else(|| "customer_id is required".to_string())?;
479        let warehouse_id = self.warehouse_id.ok_or_else(|| "warehouse_id is required".to_string())?;
480        let posting_date = self.posting_date.ok_or_else(|| "posting_date is required".to_string())?;
481        let cogs_account_id = self.cogs_account_id.ok_or_else(|| "cogs_account_id is required".to_string())?;
482        let inventory_account_id = self.inventory_account_id.ok_or_else(|| "inventory_account_id is required".to_string())?;
483
484        Ok(DeliveryNote {
485            id: Uuid::new_v4(),
486            delivery_number,
487            branch_id: self.branch_id,
488            customer_id,
489            source_so_id: self.source_so_id,
490            warehouse_id,
491            posting_date,
492            total_cogs: self.total_cogs.unwrap_or(Decimal::from(0)),
493            cogs_account_id,
494            inventory_account_id,
495            transfer_id: self.transfer_id,
496            status: self.status.unwrap_or_default(),
497            posting_state: self.posting_state.unwrap_or_default(),
498            journal_id: self.journal_id,
499            accounting_post_id: self.accounting_post_id,
500            posted_at: self.posted_at,
501            notes: self.notes,
502            metadata: AuditMetadata::default(),
503        })
504    }
505}