Skip to main content

backbone_pos/domain/entity/
pos_invoice.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use sqlx::FromRow;
4use uuid::Uuid;
5use rust_decimal::Decimal;
6
7use super::PosInvoiceStatus;
8use super::AuditMetadata;
9
10/// Strongly-typed ID for PosInvoice
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[serde(transparent)]
13pub struct PosInvoiceId(pub Uuid);
14
15impl PosInvoiceId {
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 PosInvoiceId {
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 PosInvoiceId {
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 PosInvoiceId {
35    fn from(id: Uuid) -> Self { Self(id) }
36}
37
38impl From<PosInvoiceId> for Uuid {
39    fn from(id: PosInvoiceId) -> Self { id.0 }
40}
41
42impl AsRef<Uuid> for PosInvoiceId {
43    fn as_ref(&self) -> &Uuid { &self.0 }
44}
45
46impl std::ops::Deref for PosInvoiceId {
47    type Target = Uuid;
48    fn deref(&self) -> &Self::Target { &self.0 }
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
52pub struct PosInvoice {
53    pub id: Uuid,
54    pub pos_profile_id: Uuid,
55    pub opening_entry_id: Uuid,
56    pub branch_id: Option<Uuid>,
57    pub customer_id: Option<Uuid>,
58    pub pos_table_id: Option<Uuid>,
59    pub receipt_number: String,
60    pub client_uuid: Option<Uuid>,
61    pub posting_at: DateTime<Utc>,
62    pub net_total: Decimal,
63    pub tax_total: Decimal,
64    pub grand_total: Decimal,
65    pub rounding_adjustment: Decimal,
66    pub rounded_total: Decimal,
67    pub paid_total: Decimal,
68    pub change_due: Decimal,
69    pub billing_invoice_id: Option<Uuid>,
70    pub payment_entry_id: Option<Uuid>,
71    pub stock_delivery_note_id: Option<Uuid>,
72    pub is_return: bool,
73    pub return_against: Option<Uuid>,
74    pub status: PosInvoiceStatus,
75    #[serde(default)]
76    #[sqlx(json)]
77    pub metadata: AuditMetadata,
78}
79
80impl PosInvoice {
81    /// Create a builder for PosInvoice
82    pub fn builder() -> PosInvoiceBuilder {
83        <PosInvoiceBuilder as Default>::default()
84    }
85
86    /// Create a new PosInvoice with required fields
87    pub fn new(pos_profile_id: Uuid, opening_entry_id: Uuid, receipt_number: String, posting_at: DateTime<Utc>, net_total: Decimal, tax_total: Decimal, grand_total: Decimal, rounding_adjustment: Decimal, rounded_total: Decimal, paid_total: Decimal, change_due: Decimal, is_return: bool, status: PosInvoiceStatus) -> Self {
88        Self {
89            id: Uuid::new_v4(),
90            pos_profile_id,
91            opening_entry_id,
92            branch_id: None,
93            customer_id: None,
94            pos_table_id: None,
95            receipt_number,
96            client_uuid: None,
97            posting_at,
98            net_total,
99            tax_total,
100            grand_total,
101            rounding_adjustment,
102            rounded_total,
103            paid_total,
104            change_due,
105            billing_invoice_id: None,
106            payment_entry_id: None,
107            stock_delivery_note_id: None,
108            is_return,
109            return_against: None,
110            status,
111            metadata: AuditMetadata::default(),
112        }
113    }
114
115    /// Get the entity's unique identifier
116    pub fn id(&self) -> &Uuid {
117        &self.id
118    }
119
120    /// Get a strongly-typed ID for this entity
121    pub fn typed_id(&self) -> PosInvoiceId {
122        PosInvoiceId(self.id)
123    }
124
125    /// Get when this entity was created
126    pub fn created_at(&self) -> Option<&DateTime<Utc>> {
127        self.metadata.created_at.as_ref()
128    }
129
130    /// Get when this entity was last updated
131    pub fn updated_at(&self) -> Option<&DateTime<Utc>> {
132        self.metadata.updated_at.as_ref()
133    }
134
135    /// Check if this entity is soft deleted
136    pub fn is_deleted(&self) -> bool {
137        self.metadata.deleted_at.is_some()
138    }
139
140    /// Check if this entity is active (not deleted)
141    pub fn is_active(&self) -> bool {
142        self.metadata.deleted_at.is_none()
143    }
144
145    /// Get when this entity was deleted
146    pub fn deleted_at(&self) -> Option<&DateTime<Utc>> {
147        self.metadata.deleted_at.as_ref()
148    }
149
150    /// Get who created this entity
151    pub fn created_by(&self) -> Option<&Uuid> {
152        self.metadata.created_by.as_ref()
153    }
154
155    /// Get who last updated this entity
156    pub fn updated_by(&self) -> Option<&Uuid> {
157        self.metadata.updated_by.as_ref()
158    }
159
160    /// Get who deleted this entity
161    pub fn deleted_by(&self) -> Option<&Uuid> {
162        self.metadata.deleted_by.as_ref()
163    }
164
165    /// Get the current status
166    pub fn status(&self) -> &PosInvoiceStatus {
167        &self.status
168    }
169
170
171    // ==========================================================
172    // Fluent Setters (with_* for optional fields)
173    // ==========================================================
174
175    /// Set the branch_id field (chainable)
176    pub fn with_branch_id(mut self, value: Uuid) -> Self {
177        self.branch_id = Some(value);
178        self
179    }
180
181    /// Set the customer_id field (chainable)
182    pub fn with_customer_id(mut self, value: Uuid) -> Self {
183        self.customer_id = Some(value);
184        self
185    }
186
187    /// Set the pos_table_id field (chainable)
188    pub fn with_pos_table_id(mut self, value: Uuid) -> Self {
189        self.pos_table_id = Some(value);
190        self
191    }
192
193    /// Set the client_uuid field (chainable)
194    pub fn with_client_uuid(mut self, value: Uuid) -> Self {
195        self.client_uuid = Some(value);
196        self
197    }
198
199    /// Set the billing_invoice_id field (chainable)
200    pub fn with_billing_invoice_id(mut self, value: Uuid) -> Self {
201        self.billing_invoice_id = Some(value);
202        self
203    }
204
205    /// Set the payment_entry_id field (chainable)
206    pub fn with_payment_entry_id(mut self, value: Uuid) -> Self {
207        self.payment_entry_id = Some(value);
208        self
209    }
210
211    /// Set the stock_delivery_note_id field (chainable)
212    pub fn with_stock_delivery_note_id(mut self, value: Uuid) -> Self {
213        self.stock_delivery_note_id = Some(value);
214        self
215    }
216
217    /// Set the return_against field (chainable)
218    pub fn with_return_against(mut self, value: Uuid) -> Self {
219        self.return_against = Some(value);
220        self
221    }
222
223    // ==========================================================
224    // Partial Update
225    // ==========================================================
226
227    /// Apply partial updates from a map of field name to JSON value
228    pub fn apply_patch(&mut self, fields: std::collections::HashMap<String, serde_json::Value>) {
229        for (key, value) in fields {
230            match key.as_str() {
231                "pos_profile_id" => {
232                    if let Ok(v) = serde_json::from_value(value) { self.pos_profile_id = v; }
233                }
234                "opening_entry_id" => {
235                    if let Ok(v) = serde_json::from_value(value) { self.opening_entry_id = v; }
236                }
237                "branch_id" => {
238                    if let Ok(v) = serde_json::from_value(value) { self.branch_id = v; }
239                }
240                "customer_id" => {
241                    if let Ok(v) = serde_json::from_value(value) { self.customer_id = v; }
242                }
243                "pos_table_id" => {
244                    if let Ok(v) = serde_json::from_value(value) { self.pos_table_id = v; }
245                }
246                "receipt_number" => {
247                    if let Ok(v) = serde_json::from_value(value) { self.receipt_number = v; }
248                }
249                "client_uuid" => {
250                    if let Ok(v) = serde_json::from_value(value) { self.client_uuid = v; }
251                }
252                "posting_at" => {
253                    if let Ok(v) = serde_json::from_value(value) { self.posting_at = v; }
254                }
255                "net_total" => {
256                    if let Ok(v) = serde_json::from_value(value) { self.net_total = v; }
257                }
258                "tax_total" => {
259                    if let Ok(v) = serde_json::from_value(value) { self.tax_total = v; }
260                }
261                "grand_total" => {
262                    if let Ok(v) = serde_json::from_value(value) { self.grand_total = v; }
263                }
264                "rounding_adjustment" => {
265                    if let Ok(v) = serde_json::from_value(value) { self.rounding_adjustment = v; }
266                }
267                "rounded_total" => {
268                    if let Ok(v) = serde_json::from_value(value) { self.rounded_total = v; }
269                }
270                "paid_total" => {
271                    if let Ok(v) = serde_json::from_value(value) { self.paid_total = v; }
272                }
273                "change_due" => {
274                    if let Ok(v) = serde_json::from_value(value) { self.change_due = v; }
275                }
276                "billing_invoice_id" => {
277                    if let Ok(v) = serde_json::from_value(value) { self.billing_invoice_id = v; }
278                }
279                "payment_entry_id" => {
280                    if let Ok(v) = serde_json::from_value(value) { self.payment_entry_id = v; }
281                }
282                "stock_delivery_note_id" => {
283                    if let Ok(v) = serde_json::from_value(value) { self.stock_delivery_note_id = v; }
284                }
285                "is_return" => {
286                    if let Ok(v) = serde_json::from_value(value) { self.is_return = v; }
287                }
288                "return_against" => {
289                    if let Ok(v) = serde_json::from_value(value) { self.return_against = v; }
290                }
291                "status" => {
292                    if let Ok(v) = serde_json::from_value(value) { self.status = v; }
293                }
294                _ => {} // ignore unknown fields
295            }
296        }
297    }
298
299}
300
301impl super::Entity for PosInvoice {
302    type Id = Uuid;
303
304    fn entity_id(&self) -> &Self::Id {
305        &self.id
306    }
307
308    fn entity_type() -> &'static str {
309        "PosInvoice"
310    }
311}
312
313impl backbone_core::PersistentEntity for PosInvoice {
314    fn write_protected_fields() -> &'static [&'static str] {
315        &["client_uuid", "net_total", "tax_total", "grand_total", "rounding_adjustment", "rounded_total", "paid_total", "change_due", "billing_invoice_id", "payment_entry_id", "stock_delivery_note_id", "is_return", "return_against", "status"]
316    }
317    fn entity_id(&self) -> String {
318        self.id.to_string()
319    }
320    fn set_entity_id(&mut self, id: String) {
321        if let Ok(uuid) = uuid::Uuid::parse_str(&id) {
322            self.id = uuid;
323        }
324    }
325    fn created_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
326        self.metadata.created_at
327    }
328    fn set_created_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
329        self.metadata.created_at = Some(ts);
330    }
331    fn updated_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
332        self.metadata.updated_at
333    }
334    fn set_updated_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
335        self.metadata.updated_at = Some(ts);
336    }
337    fn deleted_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
338        self.metadata.deleted_at
339    }
340    fn set_deleted_at(&mut self, ts: Option<chrono::DateTime<chrono::Utc>>) {
341        self.metadata.deleted_at = ts;
342    }
343}
344
345impl backbone_orm::EntityRepoMeta for PosInvoice {
346    fn column_types() -> std::collections::HashMap<String, String> {
347        let mut m = std::collections::HashMap::new();
348        m.insert("id".to_string(), "uuid".to_string());
349        m.insert("pos_profile_id".to_string(), "uuid".to_string());
350        m.insert("opening_entry_id".to_string(), "uuid".to_string());
351        m.insert("branch_id".to_string(), "uuid".to_string());
352        m.insert("customer_id".to_string(), "uuid".to_string());
353        m.insert("pos_table_id".to_string(), "uuid".to_string());
354        m.insert("billing_invoice_id".to_string(), "uuid".to_string());
355        m.insert("payment_entry_id".to_string(), "uuid".to_string());
356        m.insert("stock_delivery_note_id".to_string(), "uuid".to_string());
357        m.insert("status".to_string(), "pos_invoice_status".to_string());
358        m.insert("posting_at".to_string(), "timestamptz".to_string());
359        m
360    }
361    fn search_fields() -> &'static [&'static str] {
362        &["receipt_number"]
363    }
364}
365
366/// Builder for PosInvoice entity
367///
368/// Provides a fluent API for constructing PosInvoice instances.
369/// System fields (id, metadata, timestamps) are auto-initialized.
370#[derive(Debug, Clone, Default)]
371pub struct PosInvoiceBuilder {
372    pos_profile_id: Option<Uuid>,
373    opening_entry_id: Option<Uuid>,
374    branch_id: Option<Uuid>,
375    customer_id: Option<Uuid>,
376    pos_table_id: Option<Uuid>,
377    receipt_number: Option<String>,
378    client_uuid: Option<Uuid>,
379    posting_at: Option<DateTime<Utc>>,
380    net_total: Option<Decimal>,
381    tax_total: Option<Decimal>,
382    grand_total: Option<Decimal>,
383    rounding_adjustment: Option<Decimal>,
384    rounded_total: Option<Decimal>,
385    paid_total: Option<Decimal>,
386    change_due: Option<Decimal>,
387    billing_invoice_id: Option<Uuid>,
388    payment_entry_id: Option<Uuid>,
389    stock_delivery_note_id: Option<Uuid>,
390    is_return: Option<bool>,
391    return_against: Option<Uuid>,
392    status: Option<PosInvoiceStatus>,
393}
394
395impl PosInvoiceBuilder {
396    /// Set the pos_profile_id field (required)
397    pub fn pos_profile_id(mut self, value: Uuid) -> Self {
398        self.pos_profile_id = Some(value);
399        self
400    }
401
402    /// Set the opening_entry_id field (required)
403    pub fn opening_entry_id(mut self, value: Uuid) -> Self {
404        self.opening_entry_id = Some(value);
405        self
406    }
407
408    /// Set the branch_id field (optional)
409    pub fn branch_id(mut self, value: Uuid) -> Self {
410        self.branch_id = Some(value);
411        self
412    }
413
414    /// Set the customer_id field (optional)
415    pub fn customer_id(mut self, value: Uuid) -> Self {
416        self.customer_id = Some(value);
417        self
418    }
419
420    /// Set the pos_table_id field (optional)
421    pub fn pos_table_id(mut self, value: Uuid) -> Self {
422        self.pos_table_id = Some(value);
423        self
424    }
425
426    /// Set the receipt_number field (required)
427    pub fn receipt_number(mut self, value: String) -> Self {
428        self.receipt_number = Some(value);
429        self
430    }
431
432    /// Set the client_uuid field (optional)
433    pub fn client_uuid(mut self, value: Uuid) -> Self {
434        self.client_uuid = Some(value);
435        self
436    }
437
438    /// Set the posting_at field (required)
439    pub fn posting_at(mut self, value: DateTime<Utc>) -> Self {
440        self.posting_at = Some(value);
441        self
442    }
443
444    /// Set the net_total field (default: `Decimal::from(0)`)
445    pub fn net_total(mut self, value: Decimal) -> Self {
446        self.net_total = Some(value);
447        self
448    }
449
450    /// Set the tax_total field (default: `Decimal::from(0)`)
451    pub fn tax_total(mut self, value: Decimal) -> Self {
452        self.tax_total = Some(value);
453        self
454    }
455
456    /// Set the grand_total field (default: `Decimal::from(0)`)
457    pub fn grand_total(mut self, value: Decimal) -> Self {
458        self.grand_total = Some(value);
459        self
460    }
461
462    /// Set the rounding_adjustment field (default: `Decimal::from(0)`)
463    pub fn rounding_adjustment(mut self, value: Decimal) -> Self {
464        self.rounding_adjustment = Some(value);
465        self
466    }
467
468    /// Set the rounded_total field (default: `Decimal::from(0)`)
469    pub fn rounded_total(mut self, value: Decimal) -> Self {
470        self.rounded_total = Some(value);
471        self
472    }
473
474    /// Set the paid_total field (default: `Decimal::from(0)`)
475    pub fn paid_total(mut self, value: Decimal) -> Self {
476        self.paid_total = Some(value);
477        self
478    }
479
480    /// Set the change_due field (default: `Decimal::from(0)`)
481    pub fn change_due(mut self, value: Decimal) -> Self {
482        self.change_due = Some(value);
483        self
484    }
485
486    /// Set the billing_invoice_id field (optional)
487    pub fn billing_invoice_id(mut self, value: Uuid) -> Self {
488        self.billing_invoice_id = Some(value);
489        self
490    }
491
492    /// Set the payment_entry_id field (optional)
493    pub fn payment_entry_id(mut self, value: Uuid) -> Self {
494        self.payment_entry_id = Some(value);
495        self
496    }
497
498    /// Set the stock_delivery_note_id field (optional)
499    pub fn stock_delivery_note_id(mut self, value: Uuid) -> Self {
500        self.stock_delivery_note_id = Some(value);
501        self
502    }
503
504    /// Set the is_return field (default: `false`)
505    pub fn is_return(mut self, value: bool) -> Self {
506        self.is_return = Some(value);
507        self
508    }
509
510    /// Set the return_against field (optional)
511    pub fn return_against(mut self, value: Uuid) -> Self {
512        self.return_against = Some(value);
513        self
514    }
515
516    /// Set the status field (default: `PosInvoiceStatus::default()`)
517    pub fn status(mut self, value: PosInvoiceStatus) -> Self {
518        self.status = Some(value);
519        self
520    }
521
522    /// Build the PosInvoice entity
523    ///
524    /// Returns Err if any required field without a default is missing.
525    pub fn build(self) -> Result<PosInvoice, String> {
526        let pos_profile_id = self.pos_profile_id.ok_or_else(|| "pos_profile_id is required".to_string())?;
527        let opening_entry_id = self.opening_entry_id.ok_or_else(|| "opening_entry_id is required".to_string())?;
528        let receipt_number = self.receipt_number.ok_or_else(|| "receipt_number is required".to_string())?;
529        let posting_at = self.posting_at.ok_or_else(|| "posting_at is required".to_string())?;
530
531        Ok(PosInvoice {
532            id: Uuid::new_v4(),
533            pos_profile_id,
534            opening_entry_id,
535            branch_id: self.branch_id,
536            customer_id: self.customer_id,
537            pos_table_id: self.pos_table_id,
538            receipt_number,
539            client_uuid: self.client_uuid,
540            posting_at,
541            net_total: self.net_total.unwrap_or(Decimal::from(0)),
542            tax_total: self.tax_total.unwrap_or(Decimal::from(0)),
543            grand_total: self.grand_total.unwrap_or(Decimal::from(0)),
544            rounding_adjustment: self.rounding_adjustment.unwrap_or(Decimal::from(0)),
545            rounded_total: self.rounded_total.unwrap_or(Decimal::from(0)),
546            paid_total: self.paid_total.unwrap_or(Decimal::from(0)),
547            change_due: self.change_due.unwrap_or(Decimal::from(0)),
548            billing_invoice_id: self.billing_invoice_id,
549            payment_entry_id: self.payment_entry_id,
550            stock_delivery_note_id: self.stock_delivery_note_id,
551            is_return: self.is_return.unwrap_or(false),
552            return_against: self.return_against,
553            status: self.status.unwrap_or_default(),
554            metadata: AuditMetadata::default(),
555        })
556    }
557}