datasynth_generators/subledger/
document_flow_linker.rs1use rust_decimal::Decimal;
9
10use datasynth_core::models::documents::{CustomerInvoice, VendorInvoice};
11use datasynth_core::models::subledger::ap::{APInvoice, APInvoiceLine, MatchStatus};
12use datasynth_core::models::subledger::ar::{ARInvoice, ARInvoiceLine};
13use datasynth_core::models::subledger::PaymentTerms;
14
15pub struct DocumentFlowLinker {
17 ap_counter: u64,
18 ar_counter: u64,
19}
20
21impl DocumentFlowLinker {
22 pub fn new() -> Self {
24 Self {
25 ap_counter: 0,
26 ar_counter: 0,
27 }
28 }
29
30 pub fn create_ap_invoice_from_vendor_invoice(
35 &mut self,
36 vendor_invoice: &VendorInvoice,
37 ) -> APInvoice {
38 self.ap_counter += 1;
39
40 let invoice_number = format!("APINV{:08}", self.ap_counter);
42
43 let mut ap_invoice = APInvoice::new(
45 invoice_number,
46 vendor_invoice.vendor_invoice_number.clone(),
47 vendor_invoice.header.company_code.clone(),
48 vendor_invoice.vendor_id.clone(),
49 format!("Vendor {}", vendor_invoice.vendor_id), vendor_invoice.invoice_date,
51 parse_payment_terms(&vendor_invoice.payment_terms),
52 vendor_invoice.header.currency.clone(),
53 );
54
55 if let Some(po_id) = &vendor_invoice.purchase_order_id {
57 ap_invoice.reference_po = Some(po_id.clone());
58 ap_invoice.match_status = match vendor_invoice.verification_status {
59 datasynth_core::models::documents::InvoiceVerificationStatus::ThreeWayMatchPassed => {
60 MatchStatus::Matched
61 }
62 datasynth_core::models::documents::InvoiceVerificationStatus::ThreeWayMatchFailed => {
63 MatchStatus::MatchedWithVariance {
64 price_variance: Decimal::ZERO,
65 quantity_variance: Decimal::ZERO,
66 }
67 }
68 _ => MatchStatus::NotRequired,
69 };
70 }
71
72 for (idx, item) in vendor_invoice.items.iter().enumerate() {
74 let line = APInvoiceLine::new(
75 (idx + 1) as u32,
76 item.base.description.clone(),
77 item.base.quantity,
78 item.base.uom.clone(),
79 item.base.unit_price,
80 item.base
81 .gl_account
82 .clone()
83 .unwrap_or_else(|| "5000".to_string()),
84 )
85 .with_tax(
86 item.tax_code.clone().unwrap_or_else(|| "VAT".to_string()),
87 item.base.tax_amount,
88 );
89
90 ap_invoice.add_line(line);
91 }
92
93 ap_invoice
94 }
95
96 pub fn create_ar_invoice_from_customer_invoice(
101 &mut self,
102 customer_invoice: &CustomerInvoice,
103 ) -> ARInvoice {
104 self.ar_counter += 1;
105
106 let invoice_number = format!("ARINV{:08}", self.ar_counter);
108
109 let mut ar_invoice = ARInvoice::new(
111 invoice_number,
112 customer_invoice.header.company_code.clone(),
113 customer_invoice.customer_id.clone(),
114 format!("Customer {}", customer_invoice.customer_id), customer_invoice.header.document_date,
116 parse_payment_terms(&customer_invoice.payment_terms),
117 customer_invoice.header.currency.clone(),
118 );
119
120 for (idx, item) in customer_invoice.items.iter().enumerate() {
122 let line = ARInvoiceLine::new(
123 (idx + 1) as u32,
124 item.base.description.clone(),
125 item.base.quantity,
126 item.base.uom.clone(),
127 item.base.unit_price,
128 item.revenue_account
129 .clone()
130 .unwrap_or_else(|| "4000".to_string()),
131 )
132 .with_tax("VAT".to_string(), item.base.tax_amount);
133
134 ar_invoice.add_line(line);
135 }
136
137 ar_invoice
138 }
139
140 pub fn batch_create_ap_invoices(
142 &mut self,
143 vendor_invoices: &[VendorInvoice],
144 ) -> Vec<APInvoice> {
145 vendor_invoices
146 .iter()
147 .map(|vi| self.create_ap_invoice_from_vendor_invoice(vi))
148 .collect()
149 }
150
151 pub fn batch_create_ar_invoices(
153 &mut self,
154 customer_invoices: &[CustomerInvoice],
155 ) -> Vec<ARInvoice> {
156 customer_invoices
157 .iter()
158 .map(|ci| self.create_ar_invoice_from_customer_invoice(ci))
159 .collect()
160 }
161}
162
163impl Default for DocumentFlowLinker {
164 fn default() -> Self {
165 Self::new()
166 }
167}
168
169fn parse_payment_terms(terms_str: &str) -> PaymentTerms {
171 match terms_str.to_uppercase().as_str() {
173 "NET30" | "N30" => PaymentTerms::net_30(),
174 "NET60" | "N60" => PaymentTerms::net_60(),
175 "NET90" | "N90" => PaymentTerms::net_90(),
176 "DUE ON RECEIPT" | "COD" => PaymentTerms::net(0), _ => {
178 PaymentTerms::net_30()
180 }
181 }
182}
183
184#[derive(Debug, Clone, Default)]
186pub struct SubledgerLinkResult {
187 pub ap_invoices: Vec<APInvoice>,
189 pub ar_invoices: Vec<ARInvoice>,
191 pub vendor_invoices_processed: usize,
193 pub customer_invoices_processed: usize,
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use chrono::NaiveDate;
201 use datasynth_core::models::documents::VendorInvoiceItem;
202 use rust_decimal_macros::dec;
203
204 #[test]
205 fn test_create_ap_invoice_from_vendor_invoice() {
206 let mut vendor_invoice = VendorInvoice::new(
207 "VI-001",
208 "1000",
209 "VEND001",
210 "V-INV-001",
211 2024,
212 1,
213 NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
214 "SYSTEM",
215 );
216
217 vendor_invoice.add_item(VendorInvoiceItem::new(1, "Test Item", dec!(10), dec!(100)));
218
219 let mut linker = DocumentFlowLinker::new();
220 let ap_invoice = linker.create_ap_invoice_from_vendor_invoice(&vendor_invoice);
221
222 assert_eq!(ap_invoice.vendor_id, "VEND001");
223 assert_eq!(ap_invoice.vendor_invoice_number, "V-INV-001");
224 assert_eq!(ap_invoice.lines.len(), 1);
225 assert!(ap_invoice.gross_amount.document_amount > Decimal::ZERO);
226 }
227
228 #[test]
229 fn test_create_ar_invoice_from_customer_invoice() {
230 use datasynth_core::models::documents::CustomerInvoiceItem;
231
232 let mut customer_invoice = CustomerInvoice::new(
233 "CI-001",
234 "1000",
235 "CUST001",
236 2024,
237 1,
238 NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
239 NaiveDate::from_ymd_opt(2024, 2, 14).unwrap(),
240 "SYSTEM",
241 );
242
243 customer_invoice.add_item(CustomerInvoiceItem::new(1, "Product A", dec!(5), dec!(200)));
244
245 let mut linker = DocumentFlowLinker::new();
246 let ar_invoice = linker.create_ar_invoice_from_customer_invoice(&customer_invoice);
247
248 assert_eq!(ar_invoice.customer_id, "CUST001");
249 assert_eq!(ar_invoice.lines.len(), 1);
250 assert!(ar_invoice.gross_amount.document_amount > Decimal::ZERO);
251 }
252
253 #[test]
254 fn test_batch_conversion() {
255 let vendor_invoice = VendorInvoice::new(
256 "VI-001",
257 "1000",
258 "VEND001",
259 "V-INV-001",
260 2024,
261 1,
262 NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
263 "SYSTEM",
264 );
265
266 let mut linker = DocumentFlowLinker::new();
267 let ap_invoices =
268 linker.batch_create_ap_invoices(&[vendor_invoice.clone(), vendor_invoice]);
269
270 assert_eq!(ap_invoices.len(), 2);
271 assert_eq!(ap_invoices[0].invoice_number, "APINV00000001");
272 assert_eq!(ap_invoices[1].invoice_number, "APINV00000002");
273 }
274
275 #[test]
276 fn test_parse_payment_terms() {
277 let terms = parse_payment_terms("NET30");
278 assert_eq!(terms.net_due_days, 30);
279
280 let terms = parse_payment_terms("NET60");
281 assert_eq!(terms.net_due_days, 60);
282
283 let terms = parse_payment_terms("DUE ON RECEIPT");
284 assert_eq!(terms.net_due_days, 0);
285
286 let terms = parse_payment_terms("CUSTOM");
288 assert_eq!(terms.net_due_days, 30);
289 }
290}