Skip to main content

datasynth_generators/subledger/
document_flow_linker.rs

1//! Links document flows to subledger records.
2//!
3//! This module provides conversion functions to create subledger records
4//! from document flow documents, ensuring data coherence between:
5//! - P2P document flow (VendorInvoice) -> AP subledger (APInvoice)
6//! - O2C document flow (CustomerInvoice) -> AR subledger (ARInvoice)
7
8use 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
15/// Links document flow invoices to subledger records.
16pub struct DocumentFlowLinker {
17    ap_counter: u64,
18    ar_counter: u64,
19}
20
21impl DocumentFlowLinker {
22    /// Create a new document flow linker.
23    pub fn new() -> Self {
24        Self {
25            ap_counter: 0,
26            ar_counter: 0,
27        }
28    }
29
30    /// Convert a document flow VendorInvoice to an AP subledger APInvoice.
31    ///
32    /// This ensures that vendor invoices from the P2P flow create corresponding
33    /// AP subledger records for complete data coherence.
34    pub fn create_ap_invoice_from_vendor_invoice(
35        &mut self,
36        vendor_invoice: &VendorInvoice,
37    ) -> APInvoice {
38        self.ap_counter += 1;
39
40        // Generate AP invoice number based on vendor invoice
41        let invoice_number = format!("APINV{:08}", self.ap_counter);
42
43        // Create the AP invoice
44        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 name
50            vendor_invoice.invoice_date,
51            parse_payment_terms(&vendor_invoice.payment_terms),
52            vendor_invoice.header.currency.clone(),
53        );
54
55        // Set PO reference if available
56        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        // Add line items
73        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    /// Convert a document flow CustomerInvoice to an AR subledger ARInvoice.
97    ///
98    /// This ensures that customer invoices from the O2C flow create corresponding
99    /// AR subledger records for complete data coherence.
100    pub fn create_ar_invoice_from_customer_invoice(
101        &mut self,
102        customer_invoice: &CustomerInvoice,
103    ) -> ARInvoice {
104        self.ar_counter += 1;
105
106        // Generate AR invoice number based on customer invoice
107        let invoice_number = format!("ARINV{:08}", self.ar_counter);
108
109        // Create the AR invoice
110        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 name
115            customer_invoice.header.document_date,
116            parse_payment_terms(&customer_invoice.payment_terms),
117            customer_invoice.header.currency.clone(),
118        );
119
120        // Add line items
121        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    /// Batch convert multiple vendor invoices to AP invoices.
141    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    /// Batch convert multiple customer invoices to AR invoices.
152    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
169/// Parse payment terms string into PaymentTerms struct.
170fn parse_payment_terms(terms_str: &str) -> PaymentTerms {
171    // Try to parse common payment terms formats
172    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), // Due immediately
177        _ => {
178            // Default to NET30 if parsing fails
179            PaymentTerms::net_30()
180        }
181    }
182}
183
184/// Result of linking document flows to subledgers.
185#[derive(Debug, Clone, Default)]
186pub struct SubledgerLinkResult {
187    /// AP invoices created from vendor invoices.
188    pub ap_invoices: Vec<APInvoice>,
189    /// AR invoices created from customer invoices.
190    pub ar_invoices: Vec<ARInvoice>,
191    /// Number of vendor invoices processed.
192    pub vendor_invoices_processed: usize,
193    /// Number of customer invoices processed.
194    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        // Unknown terms default to NET30
287        let terms = parse_payment_terms("CUSTOM");
288        assert_eq!(terms.net_due_days, 30);
289    }
290}