datasynth_generators/subledger/
document_flow_linker.rs1use std::collections::HashMap;
9
10use rust_decimal::Decimal;
11
12use datasynth_core::models::documents::{CustomerInvoice, VendorInvoice};
13use datasynth_core::models::subledger::ap::{APInvoice, APInvoiceLine, MatchStatus};
14use datasynth_core::models::subledger::ar::{ARInvoice, ARInvoiceLine};
15use datasynth_core::models::subledger::PaymentTerms;
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(
66 invoice_number,
67 vendor_invoice.vendor_invoice_number.clone(),
68 vendor_invoice.header.company_code.clone(),
69 vendor_invoice.vendor_id.clone(),
70 self.vendor_names
71 .get(&vendor_invoice.vendor_id)
72 .cloned()
73 .unwrap_or_else(|| format!("Vendor {}", vendor_invoice.vendor_id)),
74 vendor_invoice.invoice_date,
75 parse_payment_terms(&vendor_invoice.payment_terms),
76 vendor_invoice.header.currency.clone(),
77 );
78
79 if let Some(po_id) = &vendor_invoice.purchase_order_id {
81 ap_invoice.reference_po = Some(po_id.clone());
82 ap_invoice.match_status = match vendor_invoice.verification_status {
83 datasynth_core::models::documents::InvoiceVerificationStatus::ThreeWayMatchPassed => {
84 MatchStatus::Matched
85 }
86 datasynth_core::models::documents::InvoiceVerificationStatus::ThreeWayMatchFailed => {
87 let total_line_amount: Decimal = vendor_invoice
90 .items
91 .iter()
92 .map(|item| item.base.unit_price * item.base.quantity)
93 .sum();
94 let price_var = (total_line_amount * Decimal::new(3, 2)).round_dp(2);
96 let qty_var = (total_line_amount * Decimal::new(15, 3)).round_dp(2);
98 MatchStatus::MatchedWithVariance {
99 price_variance: price_var,
100 quantity_variance: qty_var,
101 }
102 }
103 _ => MatchStatus::NotRequired,
104 };
105 }
106
107 for (idx, item) in vendor_invoice.items.iter().enumerate() {
109 let line = APInvoiceLine::new(
110 (idx + 1) as u32,
111 item.base.description.clone(),
112 item.base.quantity,
113 item.base.uom.clone(),
114 item.base.unit_price,
115 item.base
116 .gl_account
117 .clone()
118 .unwrap_or_else(|| "5000".to_string()),
119 )
120 .with_tax(
121 item.tax_code.clone().unwrap_or_else(|| "VAT".to_string()),
122 item.base.tax_amount,
123 );
124
125 ap_invoice.add_line(line);
126 }
127
128 ap_invoice
129 }
130
131 pub fn create_ar_invoice_from_customer_invoice(
136 &mut self,
137 customer_invoice: &CustomerInvoice,
138 ) -> ARInvoice {
139 self.ar_counter += 1;
140
141 let invoice_number = format!("ARINV{:08}", self.ar_counter);
143
144 let mut ar_invoice = ARInvoice::new(
146 invoice_number,
147 customer_invoice.header.company_code.clone(),
148 customer_invoice.customer_id.clone(),
149 self.customer_names
150 .get(&customer_invoice.customer_id)
151 .cloned()
152 .unwrap_or_else(|| format!("Customer {}", customer_invoice.customer_id)),
153 customer_invoice.header.document_date,
154 parse_payment_terms(&customer_invoice.payment_terms),
155 customer_invoice.header.currency.clone(),
156 );
157
158 for (idx, item) in customer_invoice.items.iter().enumerate() {
160 let line = ARInvoiceLine::new(
161 (idx + 1) as u32,
162 item.base.description.clone(),
163 item.base.quantity,
164 item.base.uom.clone(),
165 item.base.unit_price,
166 item.revenue_account
167 .clone()
168 .unwrap_or_else(|| "4000".to_string()),
169 )
170 .with_tax("VAT".to_string(), item.base.tax_amount);
171
172 ar_invoice.add_line(line);
173 }
174
175 ar_invoice
176 }
177
178 pub fn batch_create_ap_invoices(
180 &mut self,
181 vendor_invoices: &[VendorInvoice],
182 ) -> Vec<APInvoice> {
183 vendor_invoices
184 .iter()
185 .map(|vi| self.create_ap_invoice_from_vendor_invoice(vi))
186 .collect()
187 }
188
189 pub fn batch_create_ar_invoices(
191 &mut self,
192 customer_invoices: &[CustomerInvoice],
193 ) -> Vec<ARInvoice> {
194 customer_invoices
195 .iter()
196 .map(|ci| self.create_ar_invoice_from_customer_invoice(ci))
197 .collect()
198 }
199}
200
201fn parse_payment_terms(terms_str: &str) -> PaymentTerms {
203 match terms_str.to_uppercase().as_str() {
205 "NET30" | "N30" => PaymentTerms::net_30(),
206 "NET60" | "N60" => PaymentTerms::net_60(),
207 "NET90" | "N90" => PaymentTerms::net_90(),
208 "DUE ON RECEIPT" | "COD" => PaymentTerms::net(0), _ => {
210 PaymentTerms::net_30()
212 }
213 }
214}
215
216#[derive(Debug, Clone, Default)]
218pub struct SubledgerLinkResult {
219 pub ap_invoices: Vec<APInvoice>,
221 pub ar_invoices: Vec<ARInvoice>,
223 pub vendor_invoices_processed: usize,
225 pub customer_invoices_processed: usize,
227}
228
229#[cfg(test)]
230#[allow(clippy::unwrap_used)]
231mod tests {
232 use super::*;
233 use chrono::NaiveDate;
234 use datasynth_core::models::documents::VendorInvoiceItem;
235 use rust_decimal_macros::dec;
236
237 #[test]
238 fn test_create_ap_invoice_from_vendor_invoice() {
239 let mut vendor_invoice = VendorInvoice::new(
240 "VI-001",
241 "1000",
242 "VEND001",
243 "V-INV-001",
244 2024,
245 1,
246 NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
247 "SYSTEM",
248 );
249
250 vendor_invoice.add_item(VendorInvoiceItem::new(1, "Test Item", dec!(10), dec!(100)));
251
252 let mut linker = DocumentFlowLinker::new();
253 let ap_invoice = linker.create_ap_invoice_from_vendor_invoice(&vendor_invoice);
254
255 assert_eq!(ap_invoice.vendor_id, "VEND001");
256 assert_eq!(ap_invoice.vendor_invoice_number, "V-INV-001");
257 assert_eq!(ap_invoice.lines.len(), 1);
258 assert!(ap_invoice.gross_amount.document_amount > Decimal::ZERO);
259 }
260
261 #[test]
262 fn test_create_ar_invoice_from_customer_invoice() {
263 use datasynth_core::models::documents::CustomerInvoiceItem;
264
265 let mut customer_invoice = CustomerInvoice::new(
266 "CI-001",
267 "1000",
268 "CUST001",
269 2024,
270 1,
271 NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
272 NaiveDate::from_ymd_opt(2024, 2, 14).unwrap(),
273 "SYSTEM",
274 );
275
276 customer_invoice.add_item(CustomerInvoiceItem::new(1, "Product A", dec!(5), dec!(200)));
277
278 let mut linker = DocumentFlowLinker::new();
279 let ar_invoice = linker.create_ar_invoice_from_customer_invoice(&customer_invoice);
280
281 assert_eq!(ar_invoice.customer_id, "CUST001");
282 assert_eq!(ar_invoice.lines.len(), 1);
283 assert!(ar_invoice.gross_amount.document_amount > Decimal::ZERO);
284 }
285
286 #[test]
287 fn test_batch_conversion() {
288 let vendor_invoice = VendorInvoice::new(
289 "VI-001",
290 "1000",
291 "VEND001",
292 "V-INV-001",
293 2024,
294 1,
295 NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
296 "SYSTEM",
297 );
298
299 let mut linker = DocumentFlowLinker::new();
300 let ap_invoices =
301 linker.batch_create_ap_invoices(&[vendor_invoice.clone(), vendor_invoice]);
302
303 assert_eq!(ap_invoices.len(), 2);
304 assert_eq!(ap_invoices[0].invoice_number, "APINV00000001");
305 assert_eq!(ap_invoices[1].invoice_number, "APINV00000002");
306 }
307
308 #[test]
309 fn test_parse_payment_terms() {
310 let terms = parse_payment_terms("NET30");
311 assert_eq!(terms.net_due_days, 30);
312
313 let terms = parse_payment_terms("NET60");
314 assert_eq!(terms.net_due_days, 60);
315
316 let terms = parse_payment_terms("DUE ON RECEIPT");
317 assert_eq!(terms.net_due_days, 0);
318
319 let terms = parse_payment_terms("CUSTOM");
321 assert_eq!(terms.net_due_days, 30);
322 }
323}