Skip to main content

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