Skip to main content

backbone_inventory/domain/entity/
landed_cost.rs

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