stateset_embedded/credit.rs
1//! Credit Management operations
2//!
3//! Comprehensive credit management supporting:
4//! - Customer credit limits and accounts
5//! - Credit checks for orders
6//! - Credit holds and releases
7//! - Credit applications and approvals
8//!
9//! # Example
10//!
11//! ```ignore
12//! use stateset_embedded::{Commerce, CreateCreditAccount, CustomerId};
13//! use rust_decimal_macros::dec;
14//!
15//! let commerce = Commerce::new("./store.db")?;
16//!
17//! // Create a credit account for a customer
18//! let account = commerce.credit().create_credit_account(CreateCreditAccount {
19//! customer_id: CustomerId::new(),
20//! credit_limit: dec!(10000.00),
21//! payment_terms: Some("Net 30".into()),
22//! ..Default::default()
23//! })?;
24//!
25//! println!("Credit limit: ${}", account.credit_limit);
26//! # Ok::<(), stateset_embedded::CommerceError>(())
27//! ```
28
29use rust_decimal::Decimal;
30use stateset_core::{
31 CreateCreditAccount, CreditAccount, CreditAccountFilter, CreditAgingBucket, CreditApplication,
32 CreditApplicationFilter, CreditCheckResult, CreditHold, CreditHoldFilter, CreditId,
33 CreditTransaction, CreditTransactionFilter, CustomerCreditSummary, CustomerId, OrderId,
34 PlaceCreditHold, RecordCreditTransaction, ReleaseCreditHold, Result, ReviewCreditApplication,
35 SubmitCreditApplication, UpdateCreditAccount,
36};
37use stateset_db::Database;
38use std::sync::Arc;
39use uuid::Uuid;
40
41/// Credit Management interface.
42pub struct Credit {
43 db: Arc<dyn Database>,
44}
45
46impl std::fmt::Debug for Credit {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 f.debug_struct("Credit").finish_non_exhaustive()
49 }
50}
51
52impl Credit {
53 pub(crate) fn new(db: Arc<dyn Database>) -> Self {
54 Self { db }
55 }
56
57 // ========================================================================
58 // Credit Account Operations
59 // ========================================================================
60
61 /// Create a credit account for a customer.
62 ///
63 /// # Example
64 ///
65 /// ```rust,no_run
66 /// use stateset_embedded::{Commerce, CreateCreditAccount, CustomerId, RiskRating};
67 /// use rust_decimal_macros::dec;
68 ///
69 /// let commerce = Commerce::new(":memory:")?;
70 ///
71 /// let account = commerce.credit().create_credit_account(CreateCreditAccount {
72 /// customer_id: CustomerId::new(),
73 /// credit_limit: dec!(25000.00),
74 /// payment_terms: Some("Net 45".into()),
75 /// risk_rating: Some(RiskRating::Low),
76 /// notes: Some("Established customer since 2020".into()),
77 /// ..Default::default()
78 /// })?;
79 /// # Ok::<(), stateset_embedded::CommerceError>(())
80 /// ```
81 pub fn create_credit_account(&self, input: CreateCreditAccount) -> Result<CreditAccount> {
82 self.db.credit().create_credit_account(input)
83 }
84
85 /// Get a credit account by ID.
86 pub fn get_credit_account(&self, id: CreditId) -> Result<Option<CreditAccount>> {
87 self.db.credit().get_credit_account(id)
88 }
89
90 /// Get a credit account by customer ID.
91 pub fn get_credit_account_by_customer(
92 &self,
93 customer_id: CustomerId,
94 ) -> Result<Option<CreditAccount>> {
95 self.db.credit().get_credit_account_by_customer(customer_id)
96 }
97
98 /// Update a credit account.
99 pub fn update_credit_account(
100 &self,
101 id: CreditId,
102 input: UpdateCreditAccount,
103 ) -> Result<CreditAccount> {
104 self.db.credit().update_credit_account(id, input)
105 }
106
107 /// List credit accounts with optional filtering.
108 pub fn list_credit_accounts(&self, filter: CreditAccountFilter) -> Result<Vec<CreditAccount>> {
109 self.db.credit().list_credit_accounts(filter)
110 }
111
112 /// Adjust a customer's credit limit.
113 ///
114 /// # Example
115 ///
116 /// ```rust,no_run
117 /// use stateset_embedded::{Commerce, CustomerId};
118 /// use rust_decimal_macros::dec;
119 ///
120 /// let commerce = Commerce::new(":memory:")?;
121 ///
122 /// let account = commerce.credit().adjust_credit_limit(
123 /// CustomerId::new(),
124 /// dec!(50000.00),
125 /// "Annual review - increased based on payment history"
126 /// )?;
127 /// # Ok::<(), stateset_embedded::CommerceError>(())
128 /// ```
129 pub fn adjust_credit_limit(
130 &self,
131 customer_id: CustomerId,
132 new_limit: Decimal,
133 reason: &str,
134 ) -> Result<CreditAccount> {
135 self.db.credit().adjust_credit_limit(customer_id, new_limit, reason)
136 }
137
138 /// Suspend a credit account.
139 pub fn suspend_credit_account(
140 &self,
141 customer_id: CustomerId,
142 reason: &str,
143 ) -> Result<CreditAccount> {
144 self.db.credit().suspend_credit_account(customer_id, reason)
145 }
146
147 /// Reactivate a suspended credit account.
148 pub fn reactivate_credit_account(&self, customer_id: CustomerId) -> Result<CreditAccount> {
149 self.db.credit().reactivate_credit_account(customer_id)
150 }
151
152 // ========================================================================
153 // Credit Check Operations
154 // ========================================================================
155
156 /// Check credit availability for an order.
157 ///
158 /// # Example
159 ///
160 /// ```rust,no_run
161 /// use stateset_embedded::{Commerce, CustomerId};
162 /// use rust_decimal_macros::dec;
163 ///
164 /// let commerce = Commerce::new(":memory:")?;
165 ///
166 /// let result = commerce.credit().check_credit(
167 /// CustomerId::new(), // customer ID
168 /// dec!(5000.00), // order amount
169 /// )?;
170 ///
171 /// if result.approved {
172 /// println!("Credit approved");
173 /// } else {
174 /// println!("Credit denied: {:?}", result.reason);
175 /// if result.requires_approval {
176 /// println!("Can be approved by credit manager");
177 /// }
178 /// }
179 /// # Ok::<(), stateset_embedded::CommerceError>(())
180 /// ```
181 pub fn check_credit(
182 &self,
183 customer_id: CustomerId,
184 order_amount: Decimal,
185 ) -> Result<CreditCheckResult> {
186 self.db.credit().check_credit(customer_id, order_amount)
187 }
188
189 /// Reserve credit for an order.
190 ///
191 /// Reduces available credit until the order is invoiced or cancelled.
192 pub fn reserve_credit(
193 &self,
194 customer_id: CustomerId,
195 order_id: OrderId,
196 amount: Decimal,
197 ) -> Result<CreditAccount> {
198 self.db.credit().reserve_credit(customer_id, order_id, amount)
199 }
200
201 /// Release a credit reservation (e.g., order cancelled).
202 pub fn release_credit_reservation(
203 &self,
204 customer_id: CustomerId,
205 order_id: OrderId,
206 ) -> Result<CreditAccount> {
207 self.db.credit().release_credit_reservation(customer_id, order_id)
208 }
209
210 /// Charge credit (convert reservation to balance when order is invoiced).
211 pub fn charge_credit(
212 &self,
213 customer_id: CustomerId,
214 order_id: OrderId,
215 amount: Decimal,
216 ) -> Result<CreditAccount> {
217 self.db.credit().charge_credit(customer_id, order_id, amount)
218 }
219
220 // ========================================================================
221 // Credit Hold Operations
222 // ========================================================================
223
224 /// Place a credit hold on a customer or order.
225 ///
226 /// # Example
227 ///
228 /// ```rust,no_run
229 /// use stateset_embedded::{Commerce, PlaceCreditHold, CreditHoldType, CustomerId, OrderId};
230 /// use rust_decimal_macros::dec;
231 ///
232 /// let commerce = Commerce::new(":memory:")?;
233 ///
234 /// let hold = commerce.credit().place_hold(PlaceCreditHold {
235 /// customer_id: CustomerId::new(),
236 /// order_id: Some(OrderId::new()),
237 /// hold_type: CreditHoldType::OverLimit,
238 /// hold_amount: dec!(2500.00),
239 /// reason: "Order exceeds available credit".into(),
240 /// placed_by: Some("credit_system".into()),
241 /// })?;
242 ///
243 /// println!("Hold placed: {:?}", hold.id);
244 /// # Ok::<(), stateset_embedded::CommerceError>(())
245 /// ```
246 pub fn place_hold(&self, input: PlaceCreditHold) -> Result<CreditHold> {
247 self.db.credit().place_hold(input)
248 }
249
250 /// Get a credit hold by ID.
251 pub fn get_hold(&self, id: Uuid) -> Result<Option<CreditHold>> {
252 self.db.credit().get_hold(id)
253 }
254
255 /// List credit holds with optional filtering.
256 pub fn list_holds(&self, filter: CreditHoldFilter) -> Result<Vec<CreditHold>> {
257 self.db.credit().list_holds(filter)
258 }
259
260 /// Release a credit hold.
261 pub fn release_hold(&self, input: ReleaseCreditHold) -> Result<CreditHold> {
262 self.db.credit().release_hold(input)
263 }
264
265 /// Get all active holds for a customer.
266 pub fn get_active_holds(&self, customer_id: CustomerId) -> Result<Vec<CreditHold>> {
267 self.db.credit().get_active_holds(customer_id)
268 }
269
270 /// Get all holds for an order.
271 pub fn get_holds_for_order(&self, order_id: OrderId) -> Result<Vec<CreditHold>> {
272 self.db.credit().get_holds_for_order(order_id)
273 }
274
275 // ========================================================================
276 // Credit Application Operations
277 // ========================================================================
278
279 /// Submit a credit application.
280 ///
281 /// # Example
282 ///
283 /// ```rust,no_run
284 /// use stateset_embedded::{Commerce, CustomerId, SubmitCreditApplication};
285 /// use rust_decimal_macros::dec;
286 ///
287 /// let commerce = Commerce::new(":memory:")?;
288 ///
289 /// let app = commerce.credit().submit_application(SubmitCreditApplication {
290 /// customer_id: CustomerId::new(),
291 /// requested_limit: dec!(50000.00),
292 /// business_name: Some("Acme Corp".into()),
293 /// years_in_business: Some(10),
294 /// annual_revenue: Some(dec!(5000000.00)),
295 /// bank_reference: Some("First National Bank".into()),
296 /// ..Default::default()
297 /// })?;
298 ///
299 /// println!("Application {} submitted", app.application_number);
300 /// # Ok::<(), stateset_embedded::CommerceError>(())
301 /// ```
302 pub fn submit_application(&self, input: SubmitCreditApplication) -> Result<CreditApplication> {
303 self.db.credit().submit_application(input)
304 }
305
306 /// Get a credit application by ID.
307 pub fn get_application(&self, id: Uuid) -> Result<Option<CreditApplication>> {
308 self.db.credit().get_application(id)
309 }
310
311 /// List credit applications with optional filtering.
312 pub fn list_applications(
313 &self,
314 filter: CreditApplicationFilter,
315 ) -> Result<Vec<CreditApplication>> {
316 self.db.credit().list_applications(filter)
317 }
318
319 /// Review and approve/deny a credit application.
320 pub fn review_application(&self, input: ReviewCreditApplication) -> Result<CreditApplication> {
321 self.db.credit().review_application(input)
322 }
323
324 /// Withdraw a credit application.
325 pub fn withdraw_application(&self, id: Uuid) -> Result<CreditApplication> {
326 self.db.credit().withdraw_application(id)
327 }
328
329 // ========================================================================
330 // Transaction Operations
331 // ========================================================================
332
333 /// Record a credit transaction.
334 pub fn record_transaction(&self, input: RecordCreditTransaction) -> Result<CreditTransaction> {
335 self.db.credit().record_transaction(input)
336 }
337
338 /// List credit transactions with optional filtering.
339 pub fn list_transactions(
340 &self,
341 filter: CreditTransactionFilter,
342 ) -> Result<Vec<CreditTransaction>> {
343 self.db.credit().list_transactions(filter)
344 }
345
346 /// Apply a payment to reduce customer balance.
347 pub fn apply_payment(
348 &self,
349 customer_id: CustomerId,
350 amount: Decimal,
351 reference_id: Option<Uuid>,
352 ) -> Result<CreditAccount> {
353 self.db.credit().apply_payment(customer_id, amount, reference_id)
354 }
355
356 // ========================================================================
357 // Analytics
358 // ========================================================================
359
360 /// Get credit summary for a customer.
361 pub fn get_customer_summary(
362 &self,
363 customer_id: CustomerId,
364 ) -> Result<Option<CustomerCreditSummary>> {
365 self.db.credit().get_customer_summary(customer_id)
366 }
367
368 /// Get credit aging report.
369 pub fn get_aging_report(&self) -> Result<Vec<(CustomerId, CreditAgingBucket)>> {
370 self.db.credit().get_aging_report()
371 }
372
373 /// Get all customers over their credit limit.
374 pub fn get_over_limit_customers(&self) -> Result<Vec<CreditAccount>> {
375 self.db.credit().get_over_limit_customers()
376 }
377}