1use chrono::{DateTime, Utc};
6use rust_decimal::Decimal;
7use serde::{Deserialize, Serialize};
8use std::str::FromStr;
9use uuid::Uuid;
10
11#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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
488pub 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}