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