datasynth_generators/subledger/
document_flow_linker.rs1use std::collections::HashMap;
9
10use rust_decimal::Decimal;
11
12use datasynth_core::accounts::{expense_accounts, revenue_accounts};
13use datasynth_core::models::documents::{CustomerInvoice, Payment, PaymentType, VendorInvoice};
14use datasynth_core::models::subledger::ap::{APInvoice, APInvoiceLine, MatchStatus};
15use datasynth_core::models::subledger::ar::{ARInvoice, ARInvoiceLine};
16use datasynth_core::models::subledger::{PaymentTerms, SubledgerDocumentStatus};
17
18#[derive(Default)]
20pub struct DocumentFlowLinker {
21 ap_counter: u64,
22 ar_counter: u64,
23 vendor_names: HashMap<String, String>,
25 customer_names: HashMap<String, String>,
27}
28
29impl DocumentFlowLinker {
30 pub fn new() -> Self {
32 Self {
33 ap_counter: 0,
34 ar_counter: 0,
35 vendor_names: HashMap::new(),
36 customer_names: HashMap::new(),
37 }
38 }
39
40 pub fn with_vendor_names(mut self, names: HashMap<String, String>) -> Self {
42 self.vendor_names = names;
43 self
44 }
45
46 pub fn with_customer_names(mut self, names: HashMap<String, String>) -> Self {
48 self.customer_names = names;
49 self
50 }
51
52 pub fn create_ap_invoice_from_vendor_invoice(
57 &mut self,
58 vendor_invoice: &VendorInvoice,
59 ) -> APInvoice {
60 self.ap_counter += 1;
61
62 let invoice_number = format!("APINV{:08}", self.ap_counter);
64
65 let mut ap_invoice = APInvoice::new(
70 invoice_number,
71 vendor_invoice.header.document_id.clone(),
72 vendor_invoice.header.company_code.clone(),
73 vendor_invoice.vendor_id.clone(),
74 self.vendor_names
75 .get(&vendor_invoice.vendor_id)
76 .cloned()
77 .unwrap_or_else(|| format!("Vendor {}", vendor_invoice.vendor_id)),
78 vendor_invoice.invoice_date,
79 parse_payment_terms(&vendor_invoice.payment_terms),
80 vendor_invoice.header.currency.clone(),
81 );
82
83 if let Some(po_id) = &vendor_invoice.purchase_order_id {
85 ap_invoice.reference_po = Some(po_id.clone());
86 ap_invoice.match_status = match vendor_invoice.verification_status {
87 datasynth_core::models::documents::InvoiceVerificationStatus::ThreeWayMatchPassed => {
88 MatchStatus::Matched
89 }
90 datasynth_core::models::documents::InvoiceVerificationStatus::ThreeWayMatchFailed => {
91 let total_line_amount: Decimal = vendor_invoice
94 .items
95 .iter()
96 .map(|item| item.base.unit_price * item.base.quantity)
97 .sum();
98 let price_var = (total_line_amount * Decimal::new(3, 2)).round_dp(2);
100 let qty_var = (total_line_amount * Decimal::new(15, 3)).round_dp(2);
102 MatchStatus::MatchedWithVariance {
103 price_variance: price_var,
104 quantity_variance: qty_var,
105 }
106 }
107 _ => MatchStatus::NotRequired,
108 };
109 }
110
111 for (idx, item) in vendor_invoice.items.iter().enumerate() {
113 let line = APInvoiceLine::new(
114 (idx + 1) as u32,
115 item.base.description.clone(),
116 item.base.quantity,
117 item.base.uom.clone(),
118 item.base.unit_price,
119 item.base
120 .gl_account
121 .clone()
122 .unwrap_or_else(|| expense_accounts::COGS.to_string()),
123 )
124 .with_tax(
125 item.tax_code.clone().unwrap_or_else(|| "VAT".to_string()),
126 item.base.tax_amount,
127 );
128
129 ap_invoice.add_line(line);
130 }
131
132 ap_invoice
133 }
134
135 pub fn create_ar_invoice_from_customer_invoice(
140 &mut self,
141 customer_invoice: &CustomerInvoice,
142 ) -> ARInvoice {
143 self.ar_counter += 1;
144
145 let invoice_number = customer_invoice.header.document_id.clone();
149
150 let mut ar_invoice = ARInvoice::new(
152 invoice_number,
153 customer_invoice.header.company_code.clone(),
154 customer_invoice.customer_id.clone(),
155 self.customer_names
156 .get(&customer_invoice.customer_id)
157 .cloned()
158 .unwrap_or_else(|| format!("Customer {}", customer_invoice.customer_id)),
159 customer_invoice.header.document_date,
160 parse_payment_terms(&customer_invoice.payment_terms),
161 customer_invoice.header.currency.clone(),
162 );
163
164 for (idx, item) in customer_invoice.items.iter().enumerate() {
166 let line = ARInvoiceLine::new(
167 (idx + 1) as u32,
168 item.base.description.clone(),
169 item.base.quantity,
170 item.base.uom.clone(),
171 item.base.unit_price,
172 item.revenue_account
173 .clone()
174 .unwrap_or_else(|| revenue_accounts::PRODUCT_REVENUE.to_string()),
175 )
176 .with_tax("VAT".to_string(), item.base.tax_amount);
177
178 ar_invoice.add_line(line);
179 }
180
181 ar_invoice
182 }
183
184 pub fn batch_create_ap_invoices(
186 &mut self,
187 vendor_invoices: &[VendorInvoice],
188 ) -> Vec<APInvoice> {
189 vendor_invoices
190 .iter()
191 .map(|vi| self.create_ap_invoice_from_vendor_invoice(vi))
192 .collect()
193 }
194
195 pub fn batch_create_ar_invoices(
197 &mut self,
198 customer_invoices: &[CustomerInvoice],
199 ) -> Vec<ARInvoice> {
200 customer_invoices
201 .iter()
202 .map(|ci| self.create_ar_invoice_from_customer_invoice(ci))
203 .collect()
204 }
205}
206
207pub fn apply_ap_settlements(ap_invoices: &mut [APInvoice], payments: &[Payment]) {
214 let mut index_map: HashMap<String, Vec<usize>> = HashMap::new();
218 for (idx, inv) in ap_invoices.iter().enumerate() {
219 index_map
220 .entry(inv.vendor_invoice_number.clone())
221 .or_default()
222 .push(idx);
223 }
224
225 for payment in payments {
226 if payment.payment_type != PaymentType::ApPayment {
227 continue;
228 }
229 for allocation in &payment.allocations {
230 if let Some(indices) = index_map.get(&allocation.invoice_id) {
231 for &idx in indices {
232 let inv = &mut ap_invoices[idx];
233 inv.amount_remaining =
234 (inv.amount_remaining - allocation.amount).max(Decimal::ZERO);
235 inv.status = if inv.amount_remaining == Decimal::ZERO {
237 SubledgerDocumentStatus::Cleared
238 } else {
239 SubledgerDocumentStatus::PartiallyCleared
240 };
241 }
242 }
243 }
244 }
245}
246
247pub fn apply_ar_settlements(ar_invoices: &mut [ARInvoice], payments: &[Payment]) {
254 let mut index_map: HashMap<String, Vec<usize>> = HashMap::new();
258 for (idx, inv) in ar_invoices.iter().enumerate() {
259 index_map
260 .entry(inv.invoice_number.clone())
261 .or_default()
262 .push(idx);
263 }
264
265 for payment in payments {
266 if payment.payment_type != PaymentType::ArReceipt {
267 continue;
268 }
269 for allocation in &payment.allocations {
270 if let Some(indices) = index_map.get(&allocation.invoice_id) {
271 for &idx in indices {
272 let inv = &mut ar_invoices[idx];
273 inv.amount_remaining =
274 (inv.amount_remaining - allocation.amount).max(Decimal::ZERO);
275 inv.status = if inv.amount_remaining == Decimal::ZERO {
277 SubledgerDocumentStatus::Cleared
278 } else {
279 SubledgerDocumentStatus::PartiallyCleared
280 };
281 }
282 }
283 }
284 }
285}
286
287fn parse_payment_terms(terms_str: &str) -> PaymentTerms {
289 match terms_str.to_uppercase().as_str() {
291 "NET30" | "N30" => PaymentTerms::net_30(),
292 "NET60" | "N60" => PaymentTerms::net_60(),
293 "NET90" | "N90" => PaymentTerms::net_90(),
294 "DUE ON RECEIPT" | "COD" => PaymentTerms::net(0), _ => {
296 PaymentTerms::net_30()
298 }
299 }
300}
301
302#[derive(Debug, Clone, Default)]
304pub struct SubledgerLinkResult {
305 pub ap_invoices: Vec<APInvoice>,
307 pub ar_invoices: Vec<ARInvoice>,
309 pub vendor_invoices_processed: usize,
311 pub customer_invoices_processed: usize,
313}
314
315#[cfg(test)]
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}