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