stateset-core 0.8.1

Core domain models and business logic for StateSet iCommerce
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
//! Invoice domain models
//!
//! Handles invoice generation, tracking, and payment reconciliation.

use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use stateset_primitives::{CurrencyCode, CustomerId, InvoiceId, OrderId, OrderItemId, ProductId};
use uuid::Uuid;

/// Invoice status
#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::Display, Serialize, Deserialize, Default)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum InvoiceStatus {
    /// Draft - not yet sent
    #[default]
    Draft,
    /// Sent to customer
    Sent,
    /// Viewed by customer
    Viewed,
    /// Partially paid
    PartiallyPaid,
    /// Fully paid
    Paid,
    /// Past due
    Overdue,
    /// Voided/cancelled
    Voided,
    /// Written off as uncollectible
    WrittenOff,
    /// In dispute
    Disputed,
}

impl std::str::FromStr for InvoiceStatus {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "draft" => Ok(Self::Draft),
            "sent" => Ok(Self::Sent),
            "viewed" => Ok(Self::Viewed),
            "partially_paid" => Ok(Self::PartiallyPaid),
            "paid" => Ok(Self::Paid),
            "overdue" => Ok(Self::Overdue),
            "voided" => Ok(Self::Voided),
            "written_off" => Ok(Self::WrittenOff),
            "disputed" => Ok(Self::Disputed),
            _ => Err(format!("Unknown invoice status: {}", s)),
        }
    }
}

/// Invoice type
#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::Display, Serialize, Deserialize, Default)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum InvoiceType {
    /// Standard invoice
    #[default]
    Standard,
    /// Credit memo/note
    CreditMemo,
    /// Debit memo/note
    DebitMemo,
    /// Proforma invoice
    Proforma,
    /// Recurring invoice
    Recurring,
    /// Final invoice
    Final,
}

impl std::str::FromStr for InvoiceType {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "standard" => Ok(Self::Standard),
            "credit_memo" | "credit_note" => Ok(Self::CreditMemo),
            "debit_memo" | "debit_note" => Ok(Self::DebitMemo),
            "proforma" => Ok(Self::Proforma),
            "recurring" => Ok(Self::Recurring),
            "final" => Ok(Self::Final),
            _ => Err(format!("Unknown invoice type: {}", s)),
        }
    }
}

/// An invoice
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Invoice {
    /// Unique ID
    pub id: InvoiceId,
    /// Human-readable invoice number
    pub invoice_number: String,
    /// Customer ID
    pub customer_id: CustomerId,
    /// Associated order ID (optional)
    pub order_id: Option<OrderId>,
    /// Invoice status
    pub status: InvoiceStatus,
    /// Invoice type
    pub invoice_type: InvoiceType,
    /// Invoice date
    pub invoice_date: DateTime<Utc>,
    /// Due date
    pub due_date: DateTime<Utc>,
    /// Payment terms description
    pub payment_terms: Option<String>,
    /// Currency code
    pub currency: CurrencyCode,

    // Billing information
    /// Billing name
    pub billing_name: Option<String>,
    /// Billing email
    pub billing_email: Option<String>,
    /// Billing address
    pub billing_address: Option<String>,
    /// Billing city
    pub billing_city: Option<String>,
    /// Billing state
    pub billing_state: Option<String>,
    /// Billing postal code
    pub billing_postal_code: Option<String>,
    /// Billing country
    pub billing_country: Option<String>,

    // Amounts
    /// Subtotal (before tax/discounts)
    pub subtotal: Decimal,
    /// Discount amount
    pub discount_amount: Decimal,
    /// Discount percentage (if applicable)
    pub discount_percent: Option<Decimal>,
    /// Tax amount
    pub tax_amount: Decimal,
    /// Tax rate (percentage)
    pub tax_rate: Option<Decimal>,
    /// Shipping/handling charges
    pub shipping_amount: Decimal,
    /// Total amount due
    pub total: Decimal,
    /// Amount paid
    pub amount_paid: Decimal,
    /// Balance due
    pub balance_due: Decimal,

    /// Purchase order reference
    pub po_number: Option<String>,
    /// Internal notes
    pub notes: Option<String>,
    /// Terms and conditions
    pub terms: Option<String>,
    /// Footer text
    pub footer: Option<String>,

    /// When invoice was sent
    pub sent_at: Option<DateTime<Utc>>,
    /// When invoice was viewed
    pub viewed_at: Option<DateTime<Utc>>,
    /// When invoice was paid in full
    pub paid_at: Option<DateTime<Utc>>,
    /// When invoice was voided
    pub voided_at: Option<DateTime<Utc>>,

    /// Line items
    pub items: Vec<InvoiceItem>,

    /// When created
    pub created_at: DateTime<Utc>,
    /// When last updated
    pub updated_at: DateTime<Utc>,
}

impl Invoice {
    /// Check if the invoice is overdue
    pub fn is_overdue(&self) -> bool {
        if self.status == InvoiceStatus::Paid || self.status == InvoiceStatus::Voided {
            return false;
        }
        Utc::now() > self.due_date
    }

    /// Get days until due (negative if overdue)
    pub fn days_until_due(&self) -> i64 {
        (self.due_date - Utc::now()).num_days()
    }

    /// Calculate the balance due
    pub fn calculate_balance(&self) -> Decimal {
        self.total - self.amount_paid
    }
}

/// A line item on an invoice
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InvoiceItem {
    /// Unique ID
    pub id: Uuid,
    /// Parent invoice ID
    pub invoice_id: InvoiceId,
    /// Associated order item ID
    pub order_item_id: Option<OrderItemId>,
    /// Product ID
    pub product_id: Option<ProductId>,
    /// SKU
    pub sku: Option<String>,
    /// Item description
    pub description: String,
    /// Quantity
    pub quantity: Decimal,
    /// Unit of measure
    pub unit_of_measure: Option<String>,
    /// Unit price
    pub unit_price: Decimal,
    /// Discount amount for this line
    pub discount_amount: Decimal,
    /// Tax amount for this line
    pub tax_amount: Decimal,
    /// Line total (quantity * `unit_price` - discount + tax)
    pub line_total: Decimal,
    /// Sort order
    pub sort_order: i32,
    /// When created
    pub created_at: DateTime<Utc>,
    /// When last updated
    pub updated_at: DateTime<Utc>,
}

/// Input for creating an invoice
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CreateInvoice {
    /// Customer ID
    pub customer_id: CustomerId,
    /// Order ID (optional)
    pub order_id: Option<OrderId>,
    /// Invoice type
    pub invoice_type: Option<InvoiceType>,
    /// Invoice date (defaults to now)
    pub invoice_date: Option<DateTime<Utc>>,
    /// Due date (defaults to invoice date + payment terms)
    pub due_date: Option<DateTime<Utc>>,
    /// Days until due (used if `due_date` not provided)
    pub days_until_due: Option<i32>,
    /// Payment terms description
    pub payment_terms: Option<String>,
    /// Currency (defaults to USD)
    pub currency: Option<CurrencyCode>,

    // Billing info
    /// Billing name
    pub billing_name: Option<String>,
    /// Billing email
    pub billing_email: Option<String>,
    /// Billing address
    pub billing_address: Option<String>,
    /// Billing city
    pub billing_city: Option<String>,
    /// Billing state
    pub billing_state: Option<String>,
    /// Billing postal code
    pub billing_postal_code: Option<String>,
    /// Billing country
    pub billing_country: Option<String>,

    /// Discount amount
    pub discount_amount: Option<Decimal>,
    /// Discount percentage
    pub discount_percent: Option<Decimal>,
    /// Tax amount (or calculated from items)
    pub tax_amount: Option<Decimal>,
    /// Tax rate
    pub tax_rate: Option<Decimal>,
    /// Shipping amount
    pub shipping_amount: Option<Decimal>,

    /// PO number reference
    pub po_number: Option<String>,
    /// Notes
    pub notes: Option<String>,
    /// Terms and conditions
    pub terms: Option<String>,
    /// Footer text
    pub footer: Option<String>,

    /// Line items
    pub items: Vec<CreateInvoiceItem>,
}

/// Input for creating an invoice line item
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CreateInvoiceItem {
    /// Order item ID
    pub order_item_id: Option<OrderItemId>,
    /// Product ID
    pub product_id: Option<ProductId>,
    /// SKU
    pub sku: Option<String>,
    /// Description
    pub description: String,
    /// Quantity
    pub quantity: Decimal,
    /// Unit of measure
    pub unit_of_measure: Option<String>,
    /// Unit price
    pub unit_price: Decimal,
    /// Discount amount
    pub discount_amount: Option<Decimal>,
    /// Tax amount
    pub tax_amount: Option<Decimal>,
    /// Sort order
    pub sort_order: Option<i32>,
}

/// Input for updating an invoice
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct UpdateInvoice {
    /// Update due date
    pub due_date: Option<DateTime<Utc>>,
    /// Update payment terms
    pub payment_terms: Option<String>,
    /// Update billing name
    pub billing_name: Option<String>,
    /// Update billing email
    pub billing_email: Option<String>,
    /// Update billing address
    pub billing_address: Option<String>,
    /// Update billing city
    pub billing_city: Option<String>,
    /// Update billing state
    pub billing_state: Option<String>,
    /// Update billing postal code
    pub billing_postal_code: Option<String>,
    /// Update billing country
    pub billing_country: Option<String>,
    /// Update discount amount
    pub discount_amount: Option<Decimal>,
    /// Update discount percent
    pub discount_percent: Option<Decimal>,
    /// Update tax amount
    pub tax_amount: Option<Decimal>,
    /// Update tax rate
    pub tax_rate: Option<Decimal>,
    /// Update shipping amount
    pub shipping_amount: Option<Decimal>,
    /// Update PO number
    pub po_number: Option<String>,
    /// Update notes
    pub notes: Option<String>,
    /// Update terms
    pub terms: Option<String>,
    /// Update footer
    pub footer: Option<String>,
}

/// Input for recording a payment on an invoice
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct RecordInvoicePayment {
    /// Amount being paid
    pub amount: Decimal,
    /// Payment ID (if linked to a payment record)
    pub payment_id: Option<Uuid>,
    /// Payment method description
    pub payment_method: Option<String>,
    /// Payment reference/check number
    pub reference: Option<String>,
    /// Notes
    pub notes: Option<String>,
}

/// Filter for listing invoices
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct InvoiceFilter {
    /// Filter by customer ID
    pub customer_id: Option<CustomerId>,
    /// Filter by order ID
    pub order_id: Option<OrderId>,
    /// Filter by status
    pub status: Option<InvoiceStatus>,
    /// Filter by invoice type
    pub invoice_type: Option<InvoiceType>,
    /// Filter overdue only
    pub overdue_only: Option<bool>,
    /// Filter by date range start (invoice date)
    pub from_date: Option<DateTime<Utc>>,
    /// Filter by date range end (invoice date)
    pub to_date: Option<DateTime<Utc>>,
    /// Filter by due date range start
    pub due_from: Option<DateTime<Utc>>,
    /// Filter by due date range end
    pub due_to: Option<DateTime<Utc>>,
    /// Filter by minimum total
    pub min_total: Option<Decimal>,
    /// Filter by maximum total
    pub max_total: Option<Decimal>,
    /// Filter by minimum balance due
    pub min_balance: Option<Decimal>,
    /// Search by invoice number
    pub invoice_number: Option<String>,
    /// Limit results
    pub limit: Option<u32>,
    /// Offset for pagination
    pub offset: Option<u32>,
}

/// Generate a unique invoice number
pub fn generate_invoice_number() -> String {
    let now = chrono::Utc::now();
    let short_id = &uuid::Uuid::new_v4().simple().to_string()[..8];
    format!("INV-{}-{short_id}", now.format("%Y%m%d%H%M%S%3f"))
}

#[cfg(test)]
mod tests {
    use super::generate_invoice_number;

    #[test]
    fn generated_invoice_numbers_include_entropy_suffix() {
        let first = generate_invoice_number();
        let second = generate_invoice_number();

        assert!(first.starts_with("INV-"));
        assert!(first.len() > "INV-20260101120000000".len());
        assert_ne!(first, second);
    }
}