datasynth_generators/subledger/
document_flow_linker.rs1use std::collections::HashMap;
9
10use rust_decimal::Decimal;
11
12use datasynth_core::models::documents::{CustomerInvoice, Payment, PaymentType, VendorInvoice};
13use datasynth_core::models::subledger::ap::{APInvoice, APInvoiceLine, MatchStatus};
14use datasynth_core::models::subledger::ar::{ARInvoice, ARInvoiceLine};
15use datasynth_core::models::subledger::{PaymentTerms, SubledgerDocumentStatus};
16
17#[derive(Default)]
19pub struct DocumentFlowLinker {
20 ap_counter: u64,
21 ar_counter: u64,
22 vendor_names: HashMap<String, String>,
24 customer_names: HashMap<String, String>,
26}
27
28impl DocumentFlowLinker {
29 pub fn new() -> Self {
31 Self {
32 ap_counter: 0,
33 ar_counter: 0,
34 vendor_names: HashMap::new(),
35 customer_names: HashMap::new(),
36 }
37 }
38
39 pub fn with_vendor_names(mut self, names: HashMap<String, String>) -> Self {
41 self.vendor_names = names;
42 self
43 }
44
45 pub fn with_customer_names(mut self, names: HashMap<String, String>) -> Self {
47 self.customer_names = names;
48 self
49 }
50
51 pub fn create_ap_invoice_from_vendor_invoice(
56 &mut self,
57 vendor_invoice: &VendorInvoice,
58 ) -> APInvoice {
59 self.ap_counter += 1;
60
61 let invoice_number = format!("APINV{:08}", self.ap_counter);
63
64 let mut ap_invoice = APInvoice::new(
69 invoice_number,
70 vendor_invoice.header.document_id.clone(),
71 vendor_invoice.header.company_code.clone(),
72 vendor_invoice.vendor_id.clone(),
73 self.vendor_names
74 .get(&vendor_invoice.vendor_id)
75 .cloned()
76 .unwrap_or_else(|| format!("Vendor {}", vendor_invoice.vendor_id)),
77 vendor_invoice.invoice_date,
78 parse_payment_terms(&vendor_invoice.payment_terms),
79 vendor_invoice.header.currency.clone(),
80 );
81
82 if let Some(po_id) = &vendor_invoice.purchase_order_id {
84 ap_invoice.reference_po = Some(po_id.clone());
85 ap_invoice.match_status = match vendor_invoice.verification_status {
86 datasynth_core::models::documents::InvoiceVerificationStatus::ThreeWayMatchPassed => {
87 MatchStatus::Matched
88 }
89 datasynth_core::models::documents::InvoiceVerificationStatus::ThreeWayMatchFailed => {
90 let total_line_amount: Decimal = vendor_invoice
93 .items
94 .iter()
95 .map(|item| item.base.unit_price * item.base.quantity)
96 .sum();
97 let price_var = (total_line_amount * Decimal::new(3, 2)).round_dp(2);
99 let qty_var = (total_line_amount * Decimal::new(15, 3)).round_dp(2);
101 MatchStatus::MatchedWithVariance {
102 price_variance: price_var,
103 quantity_variance: qty_var,
104 }
105 }
106 _ => MatchStatus::NotRequired,
107 };
108 }
109
110 for (idx, item) in vendor_invoice.items.iter().enumerate() {
112 let line = APInvoiceLine::new(
113 (idx + 1) as u32,
114 item.base.description.clone(),
115 item.base.quantity,
116 item.base.uom.clone(),
117 item.base.unit_price,
118 item.base
119 .gl_account
120 .clone()
121 .unwrap_or_else(|| "5000".to_string()),
122 )
123 .with_tax(
124 item.tax_code.clone().unwrap_or_else(|| "VAT".to_string()),
125 item.base.tax_amount,
126 );
127
128 ap_invoice.add_line(line);
129 }
130
131 ap_invoice
132 }
133
134 pub fn create_ar_invoice_from_customer_invoice(
139 &mut self,
140 customer_invoice: &CustomerInvoice,
141 ) -> ARInvoice {
142 self.ar_counter += 1;
143
144 let invoice_number = customer_invoice.header.document_id.clone();
148
149 let mut ar_invoice = ARInvoice::new(
151 invoice_number,
152 customer_invoice.header.company_code.clone(),
153 customer_invoice.customer_id.clone(),
154 self.customer_names
155 .get(&customer_invoice.customer_id)
156 .cloned()
157 .unwrap_or_else(|| format!("Customer {}", customer_invoice.customer_id)),
158 customer_invoice.header.document_date,
159 parse_payment_terms(&customer_invoice.payment_terms),
160 customer_invoice.header.currency.clone(),
161 );
162
163 for (idx, item) in customer_invoice.items.iter().enumerate() {
165 let line = ARInvoiceLine::new(
166 (idx + 1) as u32,
167 item.base.description.clone(),
168 item.base.quantity,
169 item.base.uom.clone(),
170 item.base.unit_price,
171 item.revenue_account
172 .clone()
173 .unwrap_or_else(|| "4000".to_string()),
174 )
175 .with_tax("VAT".to_string(), item.base.tax_amount);
176
177 ar_invoice.add_line(line);
178 }
179
180 ar_invoice
181 }
182
183 pub fn batch_create_ap_invoices(
185 &mut self,
186 vendor_invoices: &[VendorInvoice],
187 ) -> Vec<APInvoice> {
188 vendor_invoices
189 .iter()
190 .map(|vi| self.create_ap_invoice_from_vendor_invoice(vi))
191 .collect()
192 }
193
194 pub fn batch_create_ar_invoices(
196 &mut self,
197 customer_invoices: &[CustomerInvoice],
198 ) -> Vec<ARInvoice> {
199 customer_invoices
200 .iter()
201 .map(|ci| self.create_ar_invoice_from_customer_invoice(ci))
202 .collect()
203 }
204}
205
206pub fn apply_ap_settlements(ap_invoices: &mut [APInvoice], payments: &[Payment]) {
213 let mut index_map: HashMap<String, Vec<usize>> = HashMap::new();
217 for (idx, inv) in ap_invoices.iter().enumerate() {
218 index_map
219 .entry(inv.vendor_invoice_number.clone())
220 .or_default()
221 .push(idx);
222 }
223
224 for payment in payments {
225 if payment.payment_type != PaymentType::ApPayment {
226 continue;
227 }
228 for allocation in &payment.allocations {
229 if let Some(indices) = index_map.get(&allocation.invoice_id) {
230 for &idx in indices {
231 let inv = &mut ap_invoices[idx];
232 inv.amount_remaining =
233 (inv.amount_remaining - allocation.amount).max(Decimal::ZERO);
234 inv.status = if inv.amount_remaining == Decimal::ZERO {
236 SubledgerDocumentStatus::Cleared
237 } else {
238 SubledgerDocumentStatus::PartiallyCleared
239 };
240 }
241 }
242 }
243 }
244}
245
246pub fn apply_ar_settlements(ar_invoices: &mut [ARInvoice], payments: &[Payment]) {
253 let mut index_map: HashMap<String, Vec<usize>> = HashMap::new();
257 for (idx, inv) in ar_invoices.iter().enumerate() {
258 index_map
259 .entry(inv.invoice_number.clone())
260 .or_default()
261 .push(idx);
262 }
263
264 for payment in payments {
265 if payment.payment_type != PaymentType::ArReceipt {
266 continue;
267 }
268 for allocation in &payment.allocations {
269 if let Some(indices) = index_map.get(&allocation.invoice_id) {
270 for &idx in indices {
271 let inv = &mut ar_invoices[idx];
272 inv.amount_remaining =
273 (inv.amount_remaining - allocation.amount).max(Decimal::ZERO);
274 inv.status = if inv.amount_remaining == Decimal::ZERO {
276 SubledgerDocumentStatus::Cleared
277 } else {
278 SubledgerDocumentStatus::PartiallyCleared
279 };
280 }
281 }
282 }
283 }
284}
285
286fn parse_payment_terms(terms_str: &str) -> PaymentTerms {
288 match terms_str.to_uppercase().as_str() {
290 "NET30" | "N30" => PaymentTerms::net_30(),
291 "NET60" | "N60" => PaymentTerms::net_60(),
292 "NET90" | "N90" => PaymentTerms::net_90(),
293 "DUE ON RECEIPT" | "COD" => PaymentTerms::net(0), _ => {
295 PaymentTerms::net_30()
297 }
298 }
299}
300
301#[derive(Debug, Clone, Default)]
303pub struct SubledgerLinkResult {
304 pub ap_invoices: Vec<APInvoice>,
306 pub ar_invoices: Vec<ARInvoice>,
308 pub vendor_invoices_processed: usize,
310 pub customer_invoices_processed: usize,
312}
313
314#[cfg(test)]
315#[allow(clippy::unwrap_used)]
316mod tests {
317 use super::*;
318 use chrono::NaiveDate;
319 use datasynth_core::models::documents::VendorInvoiceItem;
320 use rust_decimal_macros::dec;
321
322 #[test]
323 fn test_create_ap_invoice_from_vendor_invoice() {
324 let mut vendor_invoice = VendorInvoice::new(
325 "VI-001",
326 "1000",
327 "VEND001",
328 "V-INV-001",
329 2024,
330 1,
331 NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
332 "SYSTEM",
333 );
334
335 vendor_invoice.add_item(VendorInvoiceItem::new(1, "Test Item", dec!(10), dec!(100)));
336
337 let mut linker = DocumentFlowLinker::new();
338 let ap_invoice = linker.create_ap_invoice_from_vendor_invoice(&vendor_invoice);
339
340 assert_eq!(ap_invoice.vendor_id, "VEND001");
341 assert_eq!(ap_invoice.vendor_invoice_number, "VI-001");
344 assert_eq!(ap_invoice.lines.len(), 1);
345 assert!(ap_invoice.gross_amount.document_amount > Decimal::ZERO);
346 }
347
348 #[test]
349 fn test_create_ar_invoice_from_customer_invoice() {
350 use datasynth_core::models::documents::CustomerInvoiceItem;
351
352 let mut customer_invoice = CustomerInvoice::new(
353 "CI-001",
354 "1000",
355 "CUST001",
356 2024,
357 1,
358 NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
359 NaiveDate::from_ymd_opt(2024, 2, 14).unwrap(),
360 "SYSTEM",
361 );
362
363 customer_invoice.add_item(CustomerInvoiceItem::new(1, "Product A", dec!(5), dec!(200)));
364
365 let mut linker = DocumentFlowLinker::new();
366 let ar_invoice = linker.create_ar_invoice_from_customer_invoice(&customer_invoice);
367
368 assert_eq!(ar_invoice.customer_id, "CUST001");
369 assert_eq!(ar_invoice.lines.len(), 1);
370 assert!(ar_invoice.gross_amount.document_amount > Decimal::ZERO);
371 }
372
373 #[test]
374 fn test_batch_conversion() {
375 let vendor_invoice = VendorInvoice::new(
376 "VI-001",
377 "1000",
378 "VEND001",
379 "V-INV-001",
380 2024,
381 1,
382 NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
383 "SYSTEM",
384 );
385
386 let mut linker = DocumentFlowLinker::new();
387 let ap_invoices =
388 linker.batch_create_ap_invoices(&[vendor_invoice.clone(), vendor_invoice]);
389
390 assert_eq!(ap_invoices.len(), 2);
391 assert_eq!(ap_invoices[0].invoice_number, "APINV00000001");
392 assert_eq!(ap_invoices[1].invoice_number, "APINV00000002");
393 }
394
395 #[test]
396 fn test_parse_payment_terms() {
397 let terms = parse_payment_terms("NET30");
398 assert_eq!(terms.net_due_days, 30);
399
400 let terms = parse_payment_terms("NET60");
401 assert_eq!(terms.net_due_days, 60);
402
403 let terms = parse_payment_terms("DUE ON RECEIPT");
404 assert_eq!(terms.net_due_days, 0);
405
406 let terms = parse_payment_terms("CUSTOM");
408 assert_eq!(terms.net_due_days, 30);
409 }
410}