stateset_core/models/
credit.rs

1//! Credit Management domain models
2//!
3//! Models for customer credit limits, credit holds, and credit applications.
4
5use chrono::{DateTime, Utc};
6use rust_decimal::Decimal;
7use serde::{Deserialize, Serialize};
8use std::str::FromStr;
9use uuid::Uuid;
10
11// ============================================================================
12// Core Credit Types
13// ============================================================================
14
15/// Customer credit account.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct CreditAccount {
18    pub id: Uuid,
19    pub customer_id: Uuid,
20    pub credit_limit: Decimal,
21    pub available_credit: Decimal,
22    pub current_balance: Decimal,
23    pub hold_amount: Decimal,
24    pub currency: String,
25    pub status: CreditAccountStatus,
26    pub payment_terms: Option<String>,
27    pub risk_rating: Option<RiskRating>,
28    pub last_review_date: Option<DateTime<Utc>>,
29    pub next_review_date: Option<DateTime<Utc>>,
30    pub notes: Option<String>,
31    pub created_at: DateTime<Utc>,
32    pub updated_at: DateTime<Utc>,
33}
34
35/// Credit hold on an order.
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct CreditHold {
38    pub id: Uuid,
39    pub customer_id: Uuid,
40    pub order_id: Option<Uuid>,
41    pub hold_type: CreditHoldType,
42    pub hold_amount: Decimal,
43    pub reason: String,
44    pub status: CreditHoldStatus,
45    pub placed_by: Option<String>,
46    pub placed_at: DateTime<Utc>,
47    pub released_by: Option<String>,
48    pub released_at: Option<DateTime<Utc>>,
49    pub release_notes: Option<String>,
50    pub created_at: DateTime<Utc>,
51}
52
53/// Credit application from a customer.
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct CreditApplication {
56    pub id: Uuid,
57    pub application_number: String,
58    pub customer_id: Uuid,
59    pub requested_limit: Decimal,
60    pub approved_limit: Option<Decimal>,
61    pub status: CreditApplicationStatus,
62    pub business_name: Option<String>,
63    pub tax_id: Option<String>,
64    pub years_in_business: Option<i32>,
65    pub annual_revenue: Option<Decimal>,
66    pub bank_reference: Option<String>,
67    pub trade_references: Option<String>,
68    pub submitted_at: DateTime<Utc>,
69    pub reviewed_by: Option<String>,
70    pub reviewed_at: Option<DateTime<Utc>>,
71    pub decision_notes: Option<String>,
72    pub created_at: DateTime<Utc>,
73    pub updated_at: DateTime<Utc>,
74}
75
76/// Credit transaction history.
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct CreditTransaction {
79    pub id: Uuid,
80    pub customer_id: Uuid,
81    pub transaction_type: CreditTransactionType,
82    pub amount: Decimal,
83    pub running_balance: Decimal,
84    pub reference_type: Option<String>,
85    pub reference_id: Option<Uuid>,
86    pub notes: Option<String>,
87    pub created_at: DateTime<Utc>,
88}
89
90/// Credit check result.
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct CreditCheckResult {
93    pub customer_id: Uuid,
94    pub order_amount: Decimal,
95    pub credit_limit: Decimal,
96    pub available_credit: Decimal,
97    pub current_balance: Decimal,
98    pub approved: bool,
99    pub reason: Option<String>,
100    pub requires_approval: bool,
101    pub checked_at: DateTime<Utc>,
102}
103
104// ============================================================================
105// Enums
106// ============================================================================
107
108/// Credit account status.
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
110#[serde(rename_all = "snake_case")]
111pub enum CreditAccountStatus {
112    #[default]
113    Active,
114    Suspended,
115    OnHold,
116    Closed,
117    PendingReview,
118}
119
120impl std::fmt::Display for CreditAccountStatus {
121    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        match self {
123            CreditAccountStatus::Active => write!(f, "active"),
124            CreditAccountStatus::Suspended => write!(f, "suspended"),
125            CreditAccountStatus::OnHold => write!(f, "on_hold"),
126            CreditAccountStatus::Closed => write!(f, "closed"),
127            CreditAccountStatus::PendingReview => write!(f, "pending_review"),
128        }
129    }
130}
131
132impl FromStr for CreditAccountStatus {
133    type Err = String;
134    fn from_str(s: &str) -> Result<Self, Self::Err> {
135        match s.trim().to_ascii_lowercase().as_str() {
136            "active" => Ok(CreditAccountStatus::Active),
137            "suspended" => Ok(CreditAccountStatus::Suspended),
138            "on_hold" | "onhold" => Ok(CreditAccountStatus::OnHold),
139            "closed" => Ok(CreditAccountStatus::Closed),
140            "pending_review" | "pendingreview" => Ok(CreditAccountStatus::PendingReview),
141            _ => Err(format!("Unknown credit account status: {}", s)),
142        }
143    }
144}
145
146/// Risk rating.
147#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
148#[serde(rename_all = "snake_case")]
149pub enum RiskRating {
150    Low,
151    #[default]
152    Medium,
153    High,
154    Critical,
155}
156
157impl std::fmt::Display for RiskRating {
158    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159        match self {
160            RiskRating::Low => write!(f, "low"),
161            RiskRating::Medium => write!(f, "medium"),
162            RiskRating::High => write!(f, "high"),
163            RiskRating::Critical => write!(f, "critical"),
164        }
165    }
166}
167
168impl FromStr for RiskRating {
169    type Err = String;
170    fn from_str(s: &str) -> Result<Self, Self::Err> {
171        match s.trim().to_ascii_lowercase().as_str() {
172            "low" => Ok(RiskRating::Low),
173            "medium" => Ok(RiskRating::Medium),
174            "high" => Ok(RiskRating::High),
175            "critical" => Ok(RiskRating::Critical),
176            _ => Err(format!("Unknown risk rating: {}", s)),
177        }
178    }
179}
180
181/// Credit hold type.
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
183#[serde(rename_all = "snake_case")]
184pub enum CreditHoldType {
185    #[default]
186    OverLimit,
187    PastDue,
188    Manual,
189    NewCustomer,
190    HighRisk,
191}
192
193impl std::fmt::Display for CreditHoldType {
194    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195        match self {
196            CreditHoldType::OverLimit => write!(f, "over_limit"),
197            CreditHoldType::PastDue => write!(f, "past_due"),
198            CreditHoldType::Manual => write!(f, "manual"),
199            CreditHoldType::NewCustomer => write!(f, "new_customer"),
200            CreditHoldType::HighRisk => write!(f, "high_risk"),
201        }
202    }
203}
204
205impl FromStr for CreditHoldType {
206    type Err = String;
207    fn from_str(s: &str) -> Result<Self, Self::Err> {
208        match s.trim().to_ascii_lowercase().as_str() {
209            "over_limit" | "overlimit" => Ok(CreditHoldType::OverLimit),
210            "past_due" | "pastdue" => Ok(CreditHoldType::PastDue),
211            "manual" => Ok(CreditHoldType::Manual),
212            "new_customer" | "newcustomer" => Ok(CreditHoldType::NewCustomer),
213            "high_risk" | "highrisk" => Ok(CreditHoldType::HighRisk),
214            _ => Err(format!("Unknown credit hold type: {}", s)),
215        }
216    }
217}
218
219/// Credit hold status.
220#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
221#[serde(rename_all = "snake_case")]
222pub enum CreditHoldStatus {
223    #[default]
224    Active,
225    Released,
226    Expired,
227}
228
229impl std::fmt::Display for CreditHoldStatus {
230    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
231        match self {
232            CreditHoldStatus::Active => write!(f, "active"),
233            CreditHoldStatus::Released => write!(f, "released"),
234            CreditHoldStatus::Expired => write!(f, "expired"),
235        }
236    }
237}
238
239impl FromStr for CreditHoldStatus {
240    type Err = String;
241    fn from_str(s: &str) -> Result<Self, Self::Err> {
242        match s.trim().to_ascii_lowercase().as_str() {
243            "active" => Ok(CreditHoldStatus::Active),
244            "released" => Ok(CreditHoldStatus::Released),
245            "expired" => Ok(CreditHoldStatus::Expired),
246            _ => Err(format!("Unknown credit hold status: {}", s)),
247        }
248    }
249}
250
251/// Credit application status.
252#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
253#[serde(rename_all = "snake_case")]
254pub enum CreditApplicationStatus {
255    #[default]
256    Pending,
257    UnderReview,
258    Approved,
259    Denied,
260    MoreInfoNeeded,
261    Withdrawn,
262}
263
264impl std::fmt::Display for CreditApplicationStatus {
265    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
266        match self {
267            CreditApplicationStatus::Pending => write!(f, "pending"),
268            CreditApplicationStatus::UnderReview => write!(f, "under_review"),
269            CreditApplicationStatus::Approved => write!(f, "approved"),
270            CreditApplicationStatus::Denied => write!(f, "denied"),
271            CreditApplicationStatus::MoreInfoNeeded => write!(f, "more_info_needed"),
272            CreditApplicationStatus::Withdrawn => write!(f, "withdrawn"),
273        }
274    }
275}
276
277impl FromStr for CreditApplicationStatus {
278    type Err = String;
279    fn from_str(s: &str) -> Result<Self, Self::Err> {
280        match s.trim().to_ascii_lowercase().as_str() {
281            "pending" => Ok(CreditApplicationStatus::Pending),
282            "under_review" | "underreview" => Ok(CreditApplicationStatus::UnderReview),
283            "approved" => Ok(CreditApplicationStatus::Approved),
284            "denied" | "rejected" => Ok(CreditApplicationStatus::Denied),
285            "more_info_needed" | "moreinfoneeded" | "info_needed" => {
286                Ok(CreditApplicationStatus::MoreInfoNeeded)
287            }
288            "withdrawn" => Ok(CreditApplicationStatus::Withdrawn),
289            _ => Err(format!("Unknown credit application status: {}", s)),
290        }
291    }
292}
293
294/// Credit transaction type.
295#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
296#[serde(rename_all = "snake_case")]
297pub enum CreditTransactionType {
298    #[default]
299    Charge,
300    Payment,
301    CreditMemo,
302    Adjustment,
303    WriteOff,
304    LimitChange,
305}
306
307impl std::fmt::Display for CreditTransactionType {
308    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
309        match self {
310            CreditTransactionType::Charge => write!(f, "charge"),
311            CreditTransactionType::Payment => write!(f, "payment"),
312            CreditTransactionType::CreditMemo => write!(f, "credit_memo"),
313            CreditTransactionType::Adjustment => write!(f, "adjustment"),
314            CreditTransactionType::WriteOff => write!(f, "write_off"),
315            CreditTransactionType::LimitChange => write!(f, "limit_change"),
316        }
317    }
318}
319
320impl FromStr for CreditTransactionType {
321    type Err = String;
322    fn from_str(s: &str) -> Result<Self, Self::Err> {
323        match s.trim().to_ascii_lowercase().as_str() {
324            "charge" => Ok(CreditTransactionType::Charge),
325            "payment" => Ok(CreditTransactionType::Payment),
326            "credit_memo" | "creditmemo" => Ok(CreditTransactionType::CreditMemo),
327            "adjustment" => Ok(CreditTransactionType::Adjustment),
328            "write_off" | "writeoff" => Ok(CreditTransactionType::WriteOff),
329            "limit_change" | "limitchange" => Ok(CreditTransactionType::LimitChange),
330            _ => Err(format!("Unknown credit transaction type: {}", s)),
331        }
332    }
333}
334
335// ============================================================================
336// Input Types
337// ============================================================================
338
339/// Input for creating a credit account.
340#[derive(Debug, Clone, Serialize, Deserialize, Default)]
341pub struct CreateCreditAccount {
342    pub customer_id: Uuid,
343    pub credit_limit: Decimal,
344    pub currency: Option<String>,
345    pub payment_terms: Option<String>,
346    pub risk_rating: Option<RiskRating>,
347    pub notes: Option<String>,
348}
349
350/// Input for updating a credit account.
351#[derive(Debug, Clone, Serialize, Deserialize, Default)]
352pub struct UpdateCreditAccount {
353    pub credit_limit: Option<Decimal>,
354    pub status: Option<CreditAccountStatus>,
355    pub payment_terms: Option<String>,
356    pub risk_rating: Option<RiskRating>,
357    pub notes: Option<String>,
358}
359
360/// Input for placing a credit hold.
361#[derive(Debug, Clone, Serialize, Deserialize)]
362pub struct PlaceCreditHold {
363    pub customer_id: Uuid,
364    pub order_id: Option<Uuid>,
365    pub hold_type: CreditHoldType,
366    pub hold_amount: Decimal,
367    pub reason: String,
368    pub placed_by: Option<String>,
369}
370
371/// Input for releasing a credit hold.
372#[derive(Debug, Clone, Serialize, Deserialize)]
373pub struct ReleaseCreditHold {
374    pub hold_id: Uuid,
375    pub released_by: Option<String>,
376    pub release_notes: Option<String>,
377}
378
379/// Input for submitting a credit application.
380#[derive(Debug, Clone, Serialize, Deserialize, Default)]
381pub struct SubmitCreditApplication {
382    pub customer_id: Uuid,
383    pub requested_limit: Decimal,
384    pub business_name: Option<String>,
385    pub tax_id: Option<String>,
386    pub years_in_business: Option<i32>,
387    pub annual_revenue: Option<Decimal>,
388    pub bank_reference: Option<String>,
389    pub trade_references: Option<String>,
390}
391
392/// Input for reviewing a credit application.
393#[derive(Debug, Clone, Serialize, Deserialize)]
394pub struct ReviewCreditApplication {
395    pub application_id: Uuid,
396    pub approved_limit: Option<Decimal>,
397    pub status: CreditApplicationStatus,
398    pub reviewed_by: String,
399    pub decision_notes: Option<String>,
400}
401
402/// Input for recording a credit transaction.
403#[derive(Debug, Clone, Serialize, Deserialize)]
404pub struct RecordCreditTransaction {
405    pub customer_id: Uuid,
406    pub transaction_type: CreditTransactionType,
407    pub amount: Decimal,
408    pub reference_type: Option<String>,
409    pub reference_id: Option<Uuid>,
410    pub notes: Option<String>,
411}
412
413// ============================================================================
414// Filter Types
415// ============================================================================
416
417/// Filter for listing credit accounts.
418#[derive(Debug, Clone, Serialize, Deserialize, Default)]
419pub struct CreditAccountFilter {
420    pub customer_id: Option<Uuid>,
421    pub status: Option<CreditAccountStatus>,
422    pub risk_rating: Option<RiskRating>,
423    pub over_limit: Option<bool>,
424    pub limit: Option<u32>,
425    pub offset: Option<u32>,
426}
427
428/// Filter for listing credit holds.
429#[derive(Debug, Clone, Serialize, Deserialize, Default)]
430pub struct CreditHoldFilter {
431    pub customer_id: Option<Uuid>,
432    pub order_id: Option<Uuid>,
433    pub hold_type: Option<CreditHoldType>,
434    pub status: Option<CreditHoldStatus>,
435    pub limit: Option<u32>,
436    pub offset: Option<u32>,
437}
438
439/// Filter for listing credit applications.
440#[derive(Debug, Clone, Serialize, Deserialize, Default)]
441pub struct CreditApplicationFilter {
442    pub customer_id: Option<Uuid>,
443    pub status: Option<CreditApplicationStatus>,
444    pub from_date: Option<DateTime<Utc>>,
445    pub to_date: Option<DateTime<Utc>>,
446    pub limit: Option<u32>,
447    pub offset: Option<u32>,
448}
449
450/// Filter for listing credit transactions.
451#[derive(Debug, Clone, Serialize, Deserialize, Default)]
452pub struct CreditTransactionFilter {
453    pub customer_id: Option<Uuid>,
454    pub transaction_type: Option<CreditTransactionType>,
455    pub from_date: Option<DateTime<Utc>>,
456    pub to_date: Option<DateTime<Utc>>,
457    pub limit: Option<u32>,
458    pub offset: Option<u32>,
459}
460
461// ============================================================================
462// Summary Types
463// ============================================================================
464
465/// Credit aging bucket.
466#[derive(Debug, Clone, Serialize, Deserialize)]
467pub struct CreditAgingBucket {
468    pub current: Decimal,
469    pub days_1_30: Decimal,
470    pub days_31_60: Decimal,
471    pub days_61_90: Decimal,
472    pub days_over_90: Decimal,
473    pub total: Decimal,
474}
475
476/// Customer credit summary.
477#[derive(Debug, Clone, Serialize, Deserialize)]
478pub struct CustomerCreditSummary {
479    pub customer_id: Uuid,
480    pub credit_limit: Decimal,
481    pub current_balance: Decimal,
482    pub available_credit: Decimal,
483    pub oldest_due_date: Option<DateTime<Utc>>,
484    pub days_past_due: i32,
485    pub hold_count: i32,
486}
487
488// ============================================================================
489// Helper Functions
490// ============================================================================
491
492/// Generate a credit application number.
493pub fn generate_credit_application_number() -> String {
494    let timestamp = chrono::Utc::now().format("%Y%m%d").to_string();
495    let random = &uuid::Uuid::new_v4().to_string()[..6].to_uppercase();
496    format!("CAPP-{}-{}", timestamp, random)
497}
498
499#[cfg(test)]
500mod tests {
501    use super::*;
502
503    #[test]
504    fn test_credit_account_status_from_str() {
505        assert_eq!(CreditAccountStatus::from_str("active").unwrap(), CreditAccountStatus::Active);
506        assert_eq!(CreditAccountStatus::from_str("OnHold").unwrap(), CreditAccountStatus::OnHold);
507        assert!(CreditAccountStatus::from_str("nope").is_err());
508    }
509
510    #[test]
511    fn test_risk_rating_from_str() {
512        assert_eq!(RiskRating::from_str("low").unwrap(), RiskRating::Low);
513        assert_eq!(RiskRating::from_str("CRITICAL").unwrap(), RiskRating::Critical);
514        assert!(RiskRating::from_str("nope").is_err());
515    }
516
517    #[test]
518    fn test_credit_hold_type_from_str() {
519        assert_eq!(CreditHoldType::from_str("overlimit").unwrap(), CreditHoldType::OverLimit);
520        assert_eq!(CreditHoldType::from_str("past_due").unwrap(), CreditHoldType::PastDue);
521        assert!(CreditHoldType::from_str("nope").is_err());
522    }
523
524    #[test]
525    fn test_credit_hold_status_from_str() {
526        assert_eq!(CreditHoldStatus::from_str("released").unwrap(), CreditHoldStatus::Released);
527        assert!(CreditHoldStatus::from_str("nope").is_err());
528    }
529
530    #[test]
531    fn test_credit_application_status_from_str() {
532        assert_eq!(
533            CreditApplicationStatus::from_str("under_review").unwrap(),
534            CreditApplicationStatus::UnderReview
535        );
536        assert_eq!(
537            CreditApplicationStatus::from_str("info_needed").unwrap(),
538            CreditApplicationStatus::MoreInfoNeeded
539        );
540        assert!(CreditApplicationStatus::from_str("nope").is_err());
541    }
542
543    #[test]
544    fn test_credit_transaction_type_from_str() {
545        assert_eq!(
546            CreditTransactionType::from_str("creditmemo").unwrap(),
547            CreditTransactionType::CreditMemo
548        );
549        assert_eq!(
550            CreditTransactionType::from_str("limit_change").unwrap(),
551            CreditTransactionType::LimitChange
552        );
553        assert!(CreditTransactionType::from_str("nope").is_err());
554    }
555}