Skip to main content

backbone_banking/domain/entity/
bank_reconciliation.rs

1use chrono::{DateTime, Utc, NaiveDate};
2use serde::{Deserialize, Serialize};
3use sqlx::FromRow;
4use uuid::Uuid;
5use rust_decimal::Decimal;
6
7use super::ReconStatus;
8use super::AuditMetadata;
9
10/// Strongly-typed ID for BankReconciliation
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[serde(transparent)]
13pub struct BankReconciliationId(pub Uuid);
14
15impl BankReconciliationId {
16    pub fn new(id: Uuid) -> Self { Self(id) }
17    pub fn generate() -> Self { Self(Uuid::new_v4()) }
18    pub fn into_inner(self) -> Uuid { self.0 }
19}
20
21impl std::fmt::Display for BankReconciliationId {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        write!(f, "{}", self.0)
24    }
25}
26
27impl std::str::FromStr for BankReconciliationId {
28    type Err = uuid::Error;
29    fn from_str(s: &str) -> Result<Self, Self::Err> {
30        Ok(Self(Uuid::parse_str(s)?))
31    }
32}
33
34impl From<Uuid> for BankReconciliationId {
35    fn from(id: Uuid) -> Self { Self(id) }
36}
37
38impl From<BankReconciliationId> for Uuid {
39    fn from(id: BankReconciliationId) -> Self { id.0 }
40}
41
42impl AsRef<Uuid> for BankReconciliationId {
43    fn as_ref(&self) -> &Uuid { &self.0 }
44}
45
46impl std::ops::Deref for BankReconciliationId {
47    type Target = Uuid;
48    fn deref(&self) -> &Self::Target { &self.0 }
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
52pub struct BankReconciliation {
53    pub id: Uuid,
54    pub bank_account_id: Uuid,
55    pub from_date: NaiveDate,
56    pub to_date: NaiveDate,
57    pub statement_closing_balance: Decimal,
58    pub ledger_balance: Decimal,
59    pub computed_difference: Decimal,
60    pub unreconciled_count: i32,
61    pub status: ReconStatus,
62    pub preset_id: Option<Uuid>,
63    #[serde(default)]
64    #[sqlx(json)]
65    pub metadata: AuditMetadata,
66}
67
68impl BankReconciliation {
69    /// Create a builder for BankReconciliation
70    pub fn builder() -> BankReconciliationBuilder {
71        <BankReconciliationBuilder as Default>::default()
72    }
73
74    /// Create a new BankReconciliation with required fields
75    pub fn new(bank_account_id: Uuid, from_date: NaiveDate, to_date: NaiveDate, statement_closing_balance: Decimal, ledger_balance: Decimal, computed_difference: Decimal, unreconciled_count: i32, status: ReconStatus) -> Self {
76        Self {
77            id: Uuid::new_v4(),
78            bank_account_id,
79            from_date,
80            to_date,
81            statement_closing_balance,
82            ledger_balance,
83            computed_difference,
84            unreconciled_count,
85            status,
86            preset_id: None,
87            metadata: AuditMetadata::default(),
88        }
89    }
90
91    /// Get the entity's unique identifier
92    pub fn id(&self) -> &Uuid {
93        &self.id
94    }
95
96    /// Get a strongly-typed ID for this entity
97    pub fn typed_id(&self) -> BankReconciliationId {
98        BankReconciliationId(self.id)
99    }
100
101    /// Get when this entity was created
102    pub fn created_at(&self) -> Option<&DateTime<Utc>> {
103        self.metadata.created_at.as_ref()
104    }
105
106    /// Get when this entity was last updated
107    pub fn updated_at(&self) -> Option<&DateTime<Utc>> {
108        self.metadata.updated_at.as_ref()
109    }
110
111    /// Check if this entity is soft deleted
112    pub fn is_deleted(&self) -> bool {
113        self.metadata.deleted_at.is_some()
114    }
115
116    /// Check if this entity is active (not deleted)
117    pub fn is_active(&self) -> bool {
118        self.metadata.deleted_at.is_none()
119    }
120
121    /// Get when this entity was deleted
122    pub fn deleted_at(&self) -> Option<&DateTime<Utc>> {
123        self.metadata.deleted_at.as_ref()
124    }
125
126    /// Get who created this entity
127    pub fn created_by(&self) -> Option<&Uuid> {
128        self.metadata.created_by.as_ref()
129    }
130
131    /// Get who last updated this entity
132    pub fn updated_by(&self) -> Option<&Uuid> {
133        self.metadata.updated_by.as_ref()
134    }
135
136    /// Get who deleted this entity
137    pub fn deleted_by(&self) -> Option<&Uuid> {
138        self.metadata.deleted_by.as_ref()
139    }
140
141    /// Get the current status
142    pub fn status(&self) -> &ReconStatus {
143        &self.status
144    }
145
146
147    // ==========================================================
148    // Fluent Setters (with_* for optional fields)
149    // ==========================================================
150
151    /// Set the preset_id field (chainable)
152    pub fn with_preset_id(mut self, value: Uuid) -> Self {
153        self.preset_id = Some(value);
154        self
155    }
156
157    // ==========================================================
158    // Partial Update
159    // ==========================================================
160
161    /// Apply partial updates from a map of field name to JSON value
162    pub fn apply_patch(&mut self, fields: std::collections::HashMap<String, serde_json::Value>) {
163        for (key, value) in fields {
164            match key.as_str() {
165                "bank_account_id" => {
166                    if let Ok(v) = serde_json::from_value(value) { self.bank_account_id = v; }
167                }
168                "from_date" => {
169                    if let Ok(v) = serde_json::from_value(value) { self.from_date = v; }
170                }
171                "to_date" => {
172                    if let Ok(v) = serde_json::from_value(value) { self.to_date = v; }
173                }
174                "statement_closing_balance" => {
175                    if let Ok(v) = serde_json::from_value(value) { self.statement_closing_balance = v; }
176                }
177                "ledger_balance" => {
178                    if let Ok(v) = serde_json::from_value(value) { self.ledger_balance = v; }
179                }
180                "computed_difference" => {
181                    if let Ok(v) = serde_json::from_value(value) { self.computed_difference = v; }
182                }
183                "unreconciled_count" => {
184                    if let Ok(v) = serde_json::from_value(value) { self.unreconciled_count = v; }
185                }
186                "status" => {
187                    if let Ok(v) = serde_json::from_value(value) { self.status = v; }
188                }
189                "preset_id" => {
190                    if let Ok(v) = serde_json::from_value(value) { self.preset_id = v; }
191                }
192                _ => {} // ignore unknown fields
193            }
194        }
195    }
196
197}
198
199impl super::Entity for BankReconciliation {
200    type Id = Uuid;
201
202    fn entity_id(&self) -> &Self::Id {
203        &self.id
204    }
205
206    fn entity_type() -> &'static str {
207        "BankReconciliation"
208    }
209}
210
211impl backbone_core::PersistentEntity for BankReconciliation {
212    fn entity_id(&self) -> String {
213        self.id.to_string()
214    }
215    fn set_entity_id(&mut self, id: String) {
216        if let Ok(uuid) = uuid::Uuid::parse_str(&id) {
217            self.id = uuid;
218        }
219    }
220    fn created_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
221        self.metadata.created_at
222    }
223    fn set_created_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
224        self.metadata.created_at = Some(ts);
225    }
226    fn updated_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
227        self.metadata.updated_at
228    }
229    fn set_updated_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
230        self.metadata.updated_at = Some(ts);
231    }
232    fn deleted_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
233        self.metadata.deleted_at
234    }
235    fn set_deleted_at(&mut self, ts: Option<chrono::DateTime<chrono::Utc>>) {
236        self.metadata.deleted_at = ts;
237    }
238}
239
240impl backbone_orm::EntityRepoMeta for BankReconciliation {
241    fn column_types() -> std::collections::HashMap<String, String> {
242        let mut m = std::collections::HashMap::new();
243        m.insert("id".to_string(), "uuid".to_string());
244        m.insert("bank_account_id".to_string(), "uuid".to_string());
245        m.insert("preset_id".to_string(), "uuid".to_string());
246        m.insert("status".to_string(), "recon_status".to_string());
247        m.insert("from_date".to_string(), "date".to_string());
248        m.insert("to_date".to_string(), "date".to_string());
249        m
250    }
251    fn search_fields() -> &'static [&'static str] {
252        &[]
253    }
254}
255
256/// Builder for BankReconciliation entity
257///
258/// Provides a fluent API for constructing BankReconciliation instances.
259/// System fields (id, metadata, timestamps) are auto-initialized.
260#[derive(Debug, Clone, Default)]
261pub struct BankReconciliationBuilder {
262    bank_account_id: Option<Uuid>,
263    from_date: Option<NaiveDate>,
264    to_date: Option<NaiveDate>,
265    statement_closing_balance: Option<Decimal>,
266    ledger_balance: Option<Decimal>,
267    computed_difference: Option<Decimal>,
268    unreconciled_count: Option<i32>,
269    status: Option<ReconStatus>,
270    preset_id: Option<Uuid>,
271}
272
273impl BankReconciliationBuilder {
274    /// Set the bank_account_id field (required)
275    pub fn bank_account_id(mut self, value: Uuid) -> Self {
276        self.bank_account_id = Some(value);
277        self
278    }
279
280    /// Set the from_date field (required)
281    pub fn from_date(mut self, value: NaiveDate) -> Self {
282        self.from_date = Some(value);
283        self
284    }
285
286    /// Set the to_date field (required)
287    pub fn to_date(mut self, value: NaiveDate) -> Self {
288        self.to_date = Some(value);
289        self
290    }
291
292    /// Set the statement_closing_balance field (default: `Decimal::from(0)`)
293    pub fn statement_closing_balance(mut self, value: Decimal) -> Self {
294        self.statement_closing_balance = Some(value);
295        self
296    }
297
298    /// Set the ledger_balance field (default: `Decimal::from(0)`)
299    pub fn ledger_balance(mut self, value: Decimal) -> Self {
300        self.ledger_balance = Some(value);
301        self
302    }
303
304    /// Set the computed_difference field (default: `Decimal::from(0)`)
305    pub fn computed_difference(mut self, value: Decimal) -> Self {
306        self.computed_difference = Some(value);
307        self
308    }
309
310    /// Set the unreconciled_count field (default: `0`)
311    pub fn unreconciled_count(mut self, value: i32) -> Self {
312        self.unreconciled_count = Some(value);
313        self
314    }
315
316    /// Set the status field (default: `ReconStatus::default()`)
317    pub fn status(mut self, value: ReconStatus) -> Self {
318        self.status = Some(value);
319        self
320    }
321
322    /// Set the preset_id field (optional)
323    pub fn preset_id(mut self, value: Uuid) -> Self {
324        self.preset_id = Some(value);
325        self
326    }
327
328    /// Build the BankReconciliation entity
329    ///
330    /// Returns Err if any required field without a default is missing.
331    pub fn build(self) -> Result<BankReconciliation, String> {
332        let bank_account_id = self.bank_account_id.ok_or_else(|| "bank_account_id is required".to_string())?;
333        let from_date = self.from_date.ok_or_else(|| "from_date is required".to_string())?;
334        let to_date = self.to_date.ok_or_else(|| "to_date is required".to_string())?;
335
336        Ok(BankReconciliation {
337            id: Uuid::new_v4(),
338            bank_account_id,
339            from_date,
340            to_date,
341            statement_closing_balance: self.statement_closing_balance.unwrap_or(Decimal::from(0)),
342            ledger_balance: self.ledger_balance.unwrap_or(Decimal::from(0)),
343            computed_difference: self.computed_difference.unwrap_or(Decimal::from(0)),
344            unreconciled_count: self.unreconciled_count.unwrap_or(0),
345            status: self.status.unwrap_or_default(),
346            preset_id: self.preset_id,
347            metadata: AuditMetadata::default(),
348        })
349    }
350}