1use 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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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}