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)]
198#[allow(clippy::unwrap_used)]
199mod tests {
200 use super::*;
201 use chrono::NaiveDate;
202 use datasynth_core::models::documents::VendorInvoiceItem;
203 use rust_decimal_macros::dec;
204
205 #[test]
206 fn test_create_ap_invoice_from_vendor_invoice() {
207 let mut vendor_invoice = VendorInvoice::new(
208 "VI-001",
209 "1000",
210 "VEND001",
211 "V-INV-001",
212 2024,
213 1,
214 NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
215 "SYSTEM",
216 );
217
218 vendor_invoice.add_item(VendorInvoiceItem::new(1, "Test Item", dec!(10), dec!(100)));
219
220 let mut linker = DocumentFlowLinker::new();
221 let ap_invoice = linker.create_ap_invoice_from_vendor_invoice(&vendor_invoice);
222
223 assert_eq!(ap_invoice.vendor_id, "VEND001");
224 assert_eq!(ap_invoice.vendor_invoice_number, "V-INV-001");
225 assert_eq!(ap_invoice.lines.len(), 1);
226 assert!(ap_invoice.gross_amount.document_amount > Decimal::ZERO);
227 }
228
229 #[test]
230 fn test_create_ar_invoice_from_customer_invoice() {
231 use datasynth_core::models::documents::CustomerInvoiceItem;
232
233 let mut customer_invoice = CustomerInvoice::new(
234 "CI-001",
235 "1000",
236 "CUST001",
237 2024,
238 1,
239 NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
240 NaiveDate::from_ymd_opt(2024, 2, 14).unwrap(),
241 "SYSTEM",
242 );
243
244 customer_invoice.add_item(CustomerInvoiceItem::new(1, "Product A", dec!(5), dec!(200)));
245
246 let mut linker = DocumentFlowLinker::new();
247 let ar_invoice = linker.create_ar_invoice_from_customer_invoice(&customer_invoice);
248
249 assert_eq!(ar_invoice.customer_id, "CUST001");
250 assert_eq!(ar_invoice.lines.len(), 1);
251 assert!(ar_invoice.gross_amount.document_amount > Decimal::ZERO);
252 }
253
254 #[test]
255 fn test_batch_conversion() {
256 let vendor_invoice = VendorInvoice::new(
257 "VI-001",
258 "1000",
259 "VEND001",
260 "V-INV-001",
261 2024,
262 1,
263 NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
264 "SYSTEM",
265 );
266
267 let mut linker = DocumentFlowLinker::new();
268 let ap_invoices =
269 linker.batch_create_ap_invoices(&[vendor_invoice.clone(), vendor_invoice]);
270
271 assert_eq!(ap_invoices.len(), 2);
272 assert_eq!(ap_invoices[0].invoice_number, "APINV00000001");
273 assert_eq!(ap_invoices[1].invoice_number, "APINV00000002");
274 }
275
276 #[test]
277 fn test_parse_payment_terms() {
278 let terms = parse_payment_terms("NET30");
279 assert_eq!(terms.net_due_days, 30);
280
281 let terms = parse_payment_terms("NET60");
282 assert_eq!(terms.net_due_days, 60);
283
284 let terms = parse_payment_terms("DUE ON RECEIPT");
285 assert_eq!(terms.net_due_days, 0);
286
287 let terms = parse_payment_terms("CUSTOM");
289 assert_eq!(terms.net_due_days, 30);
290 }
291}