Skip to main content

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