Skip to main content

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