rustkernel_accounting/
network_generation.rs

1//! Accounting Network Generation Kernel.
2//!
3//! This module implements GPU-accelerated transformation of double-entry bookkeeping
4//! journal entries into directed accounting networks (graphs). Based on the paper
5//! "Hardware Accelerated Method for Accounting Network Generation".
6//!
7//! ## Key Concepts
8//!
9//! - **Journal Entry**: A balanced set of debit/credit line items (double-entry bookkeeping)
10//! - **Accounting Flow**: A directed edge representing value transfer between accounts
11//! - **Accounting Network**: The complete directed graph of all flows
12//!
13//! ## Solving Methods
14//!
15//! The module implements five solving methods with decreasing confidence:
16//!
17//! - **Method A** (Confidence: 1.0): Trivial 1-to-1 mapping for 2-line entries
18//! - **Method B** (Confidence: 0.95): n-to-n bijective matching with Hungarian algorithm
19//! - **Method C** (Confidence: 0.85): n-to-m partition matching using integer partition
20//! - **Method D** (Confidence: 0.70): Aggregation for large entries
21//! - **Method E** (Confidence: 0.50): Decomposition for complex multi-entity entries
22//!
23//! ## GPU Acceleration
24//!
25//! - Batch processing of journal entries in parallel
26//! - Fixed-point arithmetic (128-bit) for exact decimal representation
27//! - CSR sparse matrix format for efficient network storage
28//! - Ring mode for streaming updates with temporal windowing
29
30use crate::types::{JournalEntry, JournalLine};
31use async_trait::async_trait;
32use rustkernel_core::{
33    domain::Domain,
34    error::Result,
35    kernel::KernelMetadata,
36    traits::{BatchKernel, GpuKernel},
37};
38use serde::{Deserialize, Serialize};
39use std::collections::{HashMap, HashSet};
40use std::time::Instant;
41
42// ============================================================================
43// Fixed-Point Arithmetic
44// ============================================================================
45
46/// Fixed-point representation for exact decimal arithmetic.
47/// Uses 128 bits with 18 decimal places (1e18 scale factor).
48/// Supports values up to ~170 trillion with 18 decimal precision.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
50pub struct FixedPoint128 {
51    /// Internal value scaled by 1e18.
52    pub value: i128,
53}
54
55impl FixedPoint128 {
56    /// Scale factor: 10^18 for 18 decimal places.
57    pub const SCALE: i128 = 1_000_000_000_000_000_000;
58
59    /// Zero value.
60    pub const ZERO: Self = Self { value: 0 };
61
62    /// Create from floating point.
63    #[inline]
64    pub fn from_f64(f: f64) -> Self {
65        Self {
66            value: (f * Self::SCALE as f64) as i128,
67        }
68    }
69
70    /// Convert to floating point.
71    #[inline]
72    pub fn to_f64(self) -> f64 {
73        self.value as f64 / Self::SCALE as f64
74    }
75
76    /// Create from integer.
77    #[inline]
78    pub fn from_i64(i: i64) -> Self {
79        Self {
80            value: i as i128 * Self::SCALE,
81        }
82    }
83
84    /// Absolute value.
85    #[inline]
86    pub fn abs(self) -> Self {
87        Self {
88            value: self.value.abs(),
89        }
90    }
91
92    /// Check if approximately equal within tolerance.
93    #[inline]
94    pub fn approx_eq(self, other: Self, tolerance: Self) -> bool {
95        (self.value - other.value).abs() <= tolerance.value
96    }
97
98    /// Check if zero within tolerance.
99    #[inline]
100    pub fn is_zero(self, tolerance: Self) -> bool {
101        self.value.abs() <= tolerance.value
102    }
103}
104
105impl std::ops::Add for FixedPoint128 {
106    type Output = Self;
107
108    #[inline]
109    fn add(self, rhs: Self) -> Self {
110        Self {
111            value: self.value + rhs.value,
112        }
113    }
114}
115
116impl std::ops::Sub for FixedPoint128 {
117    type Output = Self;
118
119    #[inline]
120    fn sub(self, rhs: Self) -> Self {
121        Self {
122            value: self.value - rhs.value,
123        }
124    }
125}
126
127impl std::ops::Neg for FixedPoint128 {
128    type Output = Self;
129
130    #[inline]
131    fn neg(self) -> Self {
132        Self { value: -self.value }
133    }
134}
135
136impl std::ops::AddAssign for FixedPoint128 {
137    #[inline]
138    fn add_assign(&mut self, rhs: Self) {
139        self.value += rhs.value;
140    }
141}
142
143impl std::ops::SubAssign for FixedPoint128 {
144    #[inline]
145    fn sub_assign(&mut self, rhs: Self) {
146        self.value -= rhs.value;
147    }
148}
149
150impl PartialOrd for FixedPoint128 {
151    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
152        Some(self.cmp(other))
153    }
154}
155
156impl Ord for FixedPoint128 {
157    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
158        self.value.cmp(&other.value)
159    }
160}
161
162impl Default for FixedPoint128 {
163    fn default() -> Self {
164        Self::ZERO
165    }
166}
167
168// ============================================================================
169// Network Generation Types
170// ============================================================================
171
172/// Solving method used to generate an accounting flow.
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
174pub enum SolvingMethod {
175    /// Method A: Trivial 1-to-1 for 2-line entries.
176    MethodA,
177    /// Method B: n-to-n bijective matching (Hungarian algorithm).
178    MethodB,
179    /// Method C: n-to-m partition matching.
180    MethodC,
181    /// Method D: Aggregation for large entries.
182    MethodD,
183    /// Method E: Decomposition for complex entries.
184    MethodE,
185    /// Unable to solve (suspense account).
186    Unsolvable,
187}
188
189impl SolvingMethod {
190    /// Get the confidence level for this method.
191    #[inline]
192    pub fn confidence(&self) -> f64 {
193        match self {
194            SolvingMethod::MethodA => 1.00,
195            SolvingMethod::MethodB => 0.95,
196            SolvingMethod::MethodC => 0.85,
197            SolvingMethod::MethodD => 0.70,
198            SolvingMethod::MethodE => 0.50,
199            SolvingMethod::Unsolvable => 0.00,
200        }
201    }
202
203    /// Get the method name.
204    pub fn name(&self) -> &'static str {
205        match self {
206            SolvingMethod::MethodA => "A (1-to-1)",
207            SolvingMethod::MethodB => "B (n-to-n)",
208            SolvingMethod::MethodC => "C (n-to-m)",
209            SolvingMethod::MethodD => "D (aggregation)",
210            SolvingMethod::MethodE => "E (decomposition)",
211            SolvingMethod::Unsolvable => "Unsolvable",
212        }
213    }
214}
215
216// ============================================================================
217// Account Classification System
218// ============================================================================
219
220/// Account class based on standard accounting conventions.
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
222pub enum AccountClass {
223    /// Assets (1xxx) - Cash, Bank, AR, Inventory, Fixed Assets.
224    Asset,
225    /// Liabilities (2xxx) - AP, Loans, Accruals.
226    Liability,
227    /// Equity (3xxx) - Capital, Retained Earnings.
228    Equity,
229    /// Revenue (4xxx) - Sales, Service Income.
230    Revenue,
231    /// Cost of Goods Sold (5xxx).
232    COGS,
233    /// Operating Expenses (6xxx-7xxx).
234    Expense,
235    /// Other Income/Expense (8xxx).
236    OtherIncomeExpense,
237    /// Tax accounts (VAT, Withholding, etc.).
238    Tax,
239    /// Intercompany accounts.
240    Intercompany,
241    /// Suspense/Clearing accounts.
242    Suspense,
243    /// Unknown classification.
244    Unknown,
245}
246
247impl AccountClass {
248    /// Classify an account based on its code using standard conventions.
249    pub fn from_account_code(code: &str) -> Self {
250        // Try numeric prefix first (most common)
251        if let Some(first_char) = code.chars().next() {
252            if first_char.is_ascii_digit() {
253                return Self::from_numeric_prefix(code);
254            }
255        }
256
257        // Try keyword-based classification
258        Self::from_keywords(code)
259    }
260
261    /// Classify based on numeric prefix (standard chart of accounts).
262    fn from_numeric_prefix(code: &str) -> Self {
263        let prefix: u32 = code
264            .chars()
265            .take_while(|c| c.is_ascii_digit())
266            .collect::<String>()
267            .parse()
268            .unwrap_or(0);
269
270        match prefix {
271            1000..=1999 => AccountClass::Asset,
272            2000..=2999 => AccountClass::Liability,
273            3000..=3999 => AccountClass::Equity,
274            4000..=4999 => AccountClass::Revenue,
275            5000..=5999 => AccountClass::COGS,
276            6000..=7999 => AccountClass::Expense,
277            8000..=8999 => AccountClass::OtherIncomeExpense,
278            9000..=9999 => AccountClass::Suspense,
279            _ => {
280                // Check for common sub-patterns
281                let first_digit = prefix / 1000;
282                match first_digit {
283                    1 => AccountClass::Asset,
284                    2 => AccountClass::Liability,
285                    3 => AccountClass::Equity,
286                    4 => AccountClass::Revenue,
287                    5 => AccountClass::COGS,
288                    6 | 7 => AccountClass::Expense,
289                    8 => AccountClass::OtherIncomeExpense,
290                    9 => AccountClass::Suspense,
291                    _ => AccountClass::Unknown,
292                }
293            }
294        }
295    }
296
297    /// Classify based on keywords in account code.
298    fn from_keywords(code: &str) -> Self {
299        let upper = code.to_uppercase();
300
301        // Tax accounts
302        if upper.contains("VAT")
303            || upper.contains("TAX")
304            || upper.contains("GST")
305            || upper.contains("HST")
306            || upper.contains("WITHHOLD")
307        {
308            return AccountClass::Tax;
309        }
310
311        // Intercompany
312        if upper.contains("IC_")
313            || upper.contains("INTERCO")
314            || upper.contains("INTERCOMPANY")
315            || upper.contains("DUE_FROM")
316            || upper.contains("DUE_TO")
317        {
318            return AccountClass::Intercompany;
319        }
320
321        // Suspense
322        if upper.contains("SUSPENSE") || upper.contains("CLEARING") || upper.contains("TRANSIT") {
323            return AccountClass::Suspense;
324        }
325
326        // Assets
327        if upper.contains("CASH")
328            || upper.contains("BANK")
329            || upper.contains("AR")
330            || upper.contains("RECEIVABLE")
331            || upper.contains("INVENTORY")
332            || upper.contains("PREPAID")
333            || upper.contains("FIXED_ASSET")
334            || upper.contains("EQUIPMENT")
335        {
336            return AccountClass::Asset;
337        }
338
339        // Liabilities
340        if upper.contains("AP")
341            || upper.contains("PAYABLE")
342            || upper.contains("ACCRUED")
343            || upper.contains("LOAN")
344            || upper.contains("DEBT")
345            || upper.contains("DEFERRED")
346        {
347            return AccountClass::Liability;
348        }
349
350        // Equity
351        if upper.contains("EQUITY")
352            || upper.contains("CAPITAL")
353            || upper.contains("RETAINED")
354            || upper.contains("RESERVE")
355        {
356            return AccountClass::Equity;
357        }
358
359        // Revenue
360        if upper.contains("REVENUE")
361            || upper.contains("SALES")
362            || upper.contains("INCOME")
363            || upper.contains("SERVICE_REV")
364        {
365            return AccountClass::Revenue;
366        }
367
368        // COGS
369        if upper.contains("COGS") || upper.contains("COST_OF") || upper.contains("PURCHASES") {
370            return AccountClass::COGS;
371        }
372
373        // Expenses
374        if upper.contains("EXPENSE")
375            || upper.contains("SALARY")
376            || upper.contains("RENT")
377            || upper.contains("UTILITIES")
378            || upper.contains("DEPRECIATION")
379            || upper.contains("AMORTIZATION")
380        {
381            return AccountClass::Expense;
382        }
383
384        AccountClass::Unknown
385    }
386
387    /// Check if this class typically appears on debit side.
388    pub fn is_debit_normal(&self) -> bool {
389        matches!(
390            self,
391            AccountClass::Asset
392                | AccountClass::COGS
393                | AccountClass::Expense
394                | AccountClass::OtherIncomeExpense
395        )
396    }
397
398    /// Check if this class typically appears on credit side.
399    pub fn is_credit_normal(&self) -> bool {
400        matches!(
401            self,
402            AccountClass::Liability | AccountClass::Equity | AccountClass::Revenue
403        )
404    }
405}
406
407// ============================================================================
408// VAT/Tax Detection System
409// ============================================================================
410
411/// Known VAT/GST rates by jurisdiction.
412#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
413pub struct VatRate {
414    /// Rate as decimal (e.g., 0.20 for 20%).
415    pub rate: f64,
416    /// Jurisdiction code.
417    pub jurisdiction: VatJurisdiction,
418    /// Rate type (standard, reduced, zero).
419    pub rate_type: VatRateType,
420}
421
422/// VAT jurisdiction.
423#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
424pub enum VatJurisdiction {
425    /// European Union standard rates.
426    EU,
427    /// United Kingdom.
428    UK,
429    /// United States (sales tax varies).
430    US,
431    /// Canada (GST/HST).
432    CA,
433    /// Australia (GST).
434    AU,
435    /// Germany.
436    DE,
437    /// France.
438    FR,
439    /// Generic/Unknown.
440    Generic,
441}
442
443/// VAT rate type.
444#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
445pub enum VatRateType {
446    /// Standard rate.
447    Standard,
448    /// Reduced rate.
449    Reduced,
450    /// Super-reduced rate.
451    SuperReduced,
452    /// Zero rate.
453    Zero,
454    /// Exempt.
455    Exempt,
456}
457
458/// VAT detector for identifying tax components in journal entries.
459#[derive(Debug, Clone)]
460pub struct VatDetector {
461    /// Known VAT rates to detect.
462    known_rates: Vec<VatRate>,
463    /// Tolerance for rate matching.
464    tolerance: f64,
465}
466
467impl Default for VatDetector {
468    fn default() -> Self {
469        Self::new()
470    }
471}
472
473impl VatDetector {
474    /// Create a new VAT detector with common rates.
475    pub fn new() -> Self {
476        Self {
477            known_rates: vec![
478                // EU Standard rates
479                VatRate {
480                    rate: 0.20,
481                    jurisdiction: VatJurisdiction::UK,
482                    rate_type: VatRateType::Standard,
483                },
484                VatRate {
485                    rate: 0.19,
486                    jurisdiction: VatJurisdiction::DE,
487                    rate_type: VatRateType::Standard,
488                },
489                VatRate {
490                    rate: 0.20,
491                    jurisdiction: VatJurisdiction::FR,
492                    rate_type: VatRateType::Standard,
493                },
494                VatRate {
495                    rate: 0.21,
496                    jurisdiction: VatJurisdiction::EU,
497                    rate_type: VatRateType::Standard,
498                },
499                VatRate {
500                    rate: 0.23,
501                    jurisdiction: VatJurisdiction::EU,
502                    rate_type: VatRateType::Standard,
503                },
504                VatRate {
505                    rate: 0.25,
506                    jurisdiction: VatJurisdiction::EU,
507                    rate_type: VatRateType::Standard,
508                },
509                // Reduced rates
510                VatRate {
511                    rate: 0.10,
512                    jurisdiction: VatJurisdiction::Generic,
513                    rate_type: VatRateType::Reduced,
514                },
515                VatRate {
516                    rate: 0.07,
517                    jurisdiction: VatJurisdiction::DE,
518                    rate_type: VatRateType::Reduced,
519                },
520                VatRate {
521                    rate: 0.055,
522                    jurisdiction: VatJurisdiction::FR,
523                    rate_type: VatRateType::Reduced,
524                },
525                VatRate {
526                    rate: 0.05,
527                    jurisdiction: VatJurisdiction::UK,
528                    rate_type: VatRateType::Reduced,
529                },
530                // GST rates
531                VatRate {
532                    rate: 0.10,
533                    jurisdiction: VatJurisdiction::AU,
534                    rate_type: VatRateType::Standard,
535                },
536                VatRate {
537                    rate: 0.05,
538                    jurisdiction: VatJurisdiction::CA,
539                    rate_type: VatRateType::Standard,
540                },
541                VatRate {
542                    rate: 0.13,
543                    jurisdiction: VatJurisdiction::CA,
544                    rate_type: VatRateType::Standard,
545                }, // HST
546                VatRate {
547                    rate: 0.15,
548                    jurisdiction: VatJurisdiction::CA,
549                    rate_type: VatRateType::Standard,
550                }, // HST
551            ],
552            tolerance: 0.001,
553        }
554    }
555
556    /// Add a custom VAT rate.
557    pub fn add_rate(&mut self, rate: VatRate) {
558        self.known_rates.push(rate);
559    }
560
561    /// Detect if amounts represent a VAT split (gross = net + tax).
562    /// Returns (net_amount, tax_amount, detected_rate) if found.
563    pub fn detect_vat_split(
564        &self,
565        amounts: &[FixedPoint128],
566    ) -> Option<(FixedPoint128, FixedPoint128, VatRate)> {
567        if amounts.len() < 2 {
568            return None;
569        }
570
571        // Sort amounts descending
572        let mut sorted: Vec<_> = amounts.to_vec();
573        sorted.sort_by(|a, b| b.cmp(a));
574
575        // Try each pair: largest could be gross, second could be net
576        for i in 0..sorted.len() {
577            for j in (i + 1)..sorted.len() {
578                let potential_gross = sorted[i];
579                let potential_net = sorted[j];
580
581                if potential_net.value == 0 {
582                    continue;
583                }
584
585                // Calculate implied rate
586                let implied_rate = (potential_gross.value - potential_net.value) as f64
587                    / potential_net.value as f64;
588
589                // Check against known rates
590                for vat_rate in &self.known_rates {
591                    if (implied_rate - vat_rate.rate).abs() < self.tolerance {
592                        let tax_amount = potential_gross - potential_net;
593                        return Some((potential_net, tax_amount, *vat_rate));
594                    }
595                }
596            }
597        }
598
599        None
600    }
601
602    /// Check if an account is a tax account.
603    pub fn is_tax_account(account_code: &str) -> bool {
604        let upper = account_code.to_uppercase();
605        upper.contains("VAT")
606            || upper.contains("TAX")
607            || upper.contains("GST")
608            || upper.contains("HST")
609            || upper.contains("WITHHOLD")
610            || upper.contains("OUTPUT_TAX")
611            || upper.contains("INPUT_TAX")
612    }
613
614    /// Detect VAT pattern in a set of classified lines.
615    /// Returns the detected pattern if found.
616    pub fn detect_vat_pattern(&self, lines: &[ClassifiedLine]) -> Option<VatPattern> {
617        // Find tax accounts
618        let tax_lines: Vec<_> = lines
619            .iter()
620            .filter(|l| Self::is_tax_account(&l.account_code))
621            .collect();
622
623        if tax_lines.is_empty() {
624            return None;
625        }
626
627        // Find non-tax lines
628        let non_tax_lines: Vec<_> = lines
629            .iter()
630            .filter(|l| !Self::is_tax_account(&l.account_code))
631            .collect();
632
633        if non_tax_lines.is_empty() {
634            return None;
635        }
636
637        // Calculate totals
638        let tax_total: FixedPoint128 = tax_lines
639            .iter()
640            .map(|l| l.amount)
641            .fold(FixedPoint128::ZERO, |a, b| a + b);
642        let non_tax_debit: FixedPoint128 = non_tax_lines
643            .iter()
644            .filter(|l| l.is_debit)
645            .map(|l| l.amount)
646            .fold(FixedPoint128::ZERO, |a, b| a + b);
647        let non_tax_credit: FixedPoint128 = non_tax_lines
648            .iter()
649            .filter(|l| !l.is_debit)
650            .map(|l| l.amount)
651            .fold(FixedPoint128::ZERO, |a, b| a + b);
652
653        // Determine if output VAT (sales) or input VAT (purchases)
654        let is_output_vat = tax_lines.iter().any(|l| !l.is_debit);
655
656        // Try to match a known rate
657        let base_amount = if is_output_vat {
658            non_tax_credit
659        } else {
660            non_tax_debit
661        };
662
663        if base_amount.value == 0 {
664            return None;
665        }
666
667        let implied_rate = tax_total.value as f64 / base_amount.value as f64;
668
669        for vat_rate in &self.known_rates {
670            if (implied_rate - vat_rate.rate).abs() < self.tolerance {
671                return Some(VatPattern {
672                    is_output_vat,
673                    net_amount: base_amount,
674                    tax_amount: tax_total,
675                    rate: *vat_rate,
676                    tax_accounts: tax_lines.iter().map(|l| l.account_code.clone()).collect(),
677                });
678            }
679        }
680
681        // Return pattern even if rate not matched exactly
682        Some(VatPattern {
683            is_output_vat,
684            net_amount: base_amount,
685            tax_amount: tax_total,
686            rate: VatRate {
687                rate: implied_rate,
688                jurisdiction: VatJurisdiction::Generic,
689                rate_type: VatRateType::Standard,
690            },
691            tax_accounts: tax_lines.iter().map(|l| l.account_code.clone()).collect(),
692        })
693    }
694}
695
696/// Detected VAT pattern in a journal entry.
697#[derive(Debug, Clone, Serialize, Deserialize)]
698pub struct VatPattern {
699    /// True if output VAT (sales), false if input VAT (purchases).
700    pub is_output_vat: bool,
701    /// Net amount (before VAT).
702    pub net_amount: FixedPoint128,
703    /// Tax amount.
704    pub tax_amount: FixedPoint128,
705    /// Detected VAT rate.
706    pub rate: VatRate,
707    /// Tax account codes involved.
708    pub tax_accounts: Vec<String>,
709}
710
711// ============================================================================
712// Accounting Pattern Recognition
713// ============================================================================
714
715/// Common accounting transaction patterns.
716#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
717pub enum TransactionPattern {
718    /// Simple sale: DR Asset, CR Revenue.
719    SimpleSale,
720    /// Sale with VAT: DR Asset, CR Revenue, CR VAT Payable.
721    SaleWithVat,
722    /// Simple purchase: DR Expense/Asset, CR Asset/Liability.
723    SimplePurchase,
724    /// Purchase with VAT: DR Expense, DR VAT Receivable, CR Payable.
725    PurchaseWithVat,
726    /// Payment: DR Liability, CR Asset.
727    Payment,
728    /// Receipt: DR Asset, CR Asset (AR).
729    Receipt,
730    /// Payroll: DR Expense, CR Multiple Liabilities.
731    Payroll,
732    /// Depreciation: DR Expense, CR Contra Asset.
733    Depreciation,
734    /// Accrual: DR Expense, CR Accrued Liability.
735    Accrual,
736    /// Reversal: DR Liability, CR Expense.
737    AccrualReversal,
738    /// Transfer: DR Asset, CR Asset.
739    Transfer,
740    /// Intercompany: Cross-entity transaction.
741    Intercompany,
742    /// Allocation: One account to multiple cost centers.
743    CostAllocation,
744    /// Journal adjustment.
745    Adjustment,
746    /// Unknown pattern.
747    Unknown,
748}
749
750impl TransactionPattern {
751    /// Get confidence boost for this pattern match.
752    pub fn confidence_boost(&self) -> f64 {
753        match self {
754            TransactionPattern::SimpleSale => 0.10,
755            TransactionPattern::SaleWithVat => 0.15,
756            TransactionPattern::SimplePurchase => 0.10,
757            TransactionPattern::PurchaseWithVat => 0.15,
758            TransactionPattern::Payment => 0.10,
759            TransactionPattern::Receipt => 0.10,
760            TransactionPattern::Payroll => 0.12,
761            TransactionPattern::Depreciation => 0.15,
762            TransactionPattern::Accrual => 0.10,
763            TransactionPattern::AccrualReversal => 0.10,
764            TransactionPattern::Transfer => 0.08,
765            TransactionPattern::Intercompany => 0.05,
766            TransactionPattern::CostAllocation => 0.08,
767            TransactionPattern::Adjustment => 0.05,
768            TransactionPattern::Unknown => 0.00,
769        }
770    }
771}
772
773/// Pattern matcher for identifying transaction types.
774#[derive(Debug, Clone, Default)]
775pub struct PatternMatcher {
776    vat_detector: VatDetector,
777}
778
779impl PatternMatcher {
780    /// Create a new pattern matcher.
781    pub fn new() -> Self {
782        Self {
783            vat_detector: VatDetector::new(),
784        }
785    }
786
787    /// Detect the transaction pattern from classified lines.
788    pub fn detect_pattern(
789        &self,
790        debits: &[ClassifiedLine],
791        credits: &[ClassifiedLine],
792    ) -> (TransactionPattern, Option<VatPattern>) {
793        // Check for VAT pattern first
794        let all_lines: Vec<_> = debits.iter().chain(credits.iter()).cloned().collect();
795        let vat_pattern = self.vat_detector.detect_vat_pattern(&all_lines);
796
797        // Classify accounts
798        let debit_classes: Vec<_> = debits
799            .iter()
800            .map(|l| AccountClass::from_account_code(&l.account_code))
801            .collect();
802        let credit_classes: Vec<_> = credits
803            .iter()
804            .map(|l| AccountClass::from_account_code(&l.account_code))
805            .collect();
806
807        // Check for intercompany
808        let has_cross_entity = debits
809            .iter()
810            .any(|d| credits.iter().any(|c| d.entity_id != c.entity_id));
811
812        if has_cross_entity {
813            return (TransactionPattern::Intercompany, vat_pattern);
814        }
815
816        // Detect specific patterns
817        let pattern = if let Some(ref vat) = vat_pattern {
818            self.detect_vat_transaction_pattern(&debit_classes, &credit_classes, vat)
819        } else {
820            self.detect_non_vat_pattern(&debit_classes, &credit_classes)
821        };
822
823        (pattern, vat_pattern)
824    }
825
826    fn detect_vat_transaction_pattern(
827        &self,
828        debit_classes: &[AccountClass],
829        _credit_classes: &[AccountClass],
830        vat_pattern: &VatPattern,
831    ) -> TransactionPattern {
832        if vat_pattern.is_output_vat {
833            // Output VAT = Sale
834            if debit_classes.contains(&AccountClass::Asset) {
835                TransactionPattern::SaleWithVat
836            } else {
837                TransactionPattern::Unknown
838            }
839        } else {
840            // Input VAT = Purchase
841            if debit_classes.contains(&AccountClass::Expense)
842                || debit_classes.contains(&AccountClass::Asset)
843            {
844                TransactionPattern::PurchaseWithVat
845            } else {
846                TransactionPattern::Unknown
847            }
848        }
849    }
850
851    fn detect_non_vat_pattern(
852        &self,
853        debit_classes: &[AccountClass],
854        credit_classes: &[AccountClass],
855    ) -> TransactionPattern {
856        let has_debit_asset = debit_classes.contains(&AccountClass::Asset);
857        let has_debit_expense = debit_classes.contains(&AccountClass::Expense);
858        let has_debit_liability = debit_classes.contains(&AccountClass::Liability);
859
860        let has_credit_asset = credit_classes.contains(&AccountClass::Asset);
861        let has_credit_revenue = credit_classes.contains(&AccountClass::Revenue);
862        let has_credit_liability = credit_classes.contains(&AccountClass::Liability);
863
864        // Simple Sale: DR Asset, CR Revenue
865        if has_debit_asset && has_credit_revenue && credit_classes.len() == 1 {
866            return TransactionPattern::SimpleSale;
867        }
868
869        // Simple Purchase: DR Expense, CR Asset/Liability
870        if has_debit_expense && (has_credit_asset || has_credit_liability) {
871            return TransactionPattern::SimplePurchase;
872        }
873
874        // Payment: DR Liability, CR Asset
875        if has_debit_liability && has_credit_asset {
876            return TransactionPattern::Payment;
877        }
878
879        // Receipt: DR Asset (Cash), CR Asset (AR)
880        if has_debit_asset
881            && has_credit_asset
882            && debit_classes.len() == 1
883            && credit_classes.len() == 1
884        {
885            return TransactionPattern::Receipt;
886        }
887
888        // Transfer: DR Asset, CR Asset
889        if has_debit_asset && has_credit_asset {
890            return TransactionPattern::Transfer;
891        }
892
893        // Payroll: DR Expense, CR Multiple Liabilities
894        if has_debit_expense
895            && credit_classes
896                .iter()
897                .filter(|c| **c == AccountClass::Liability)
898                .count()
899                > 1
900        {
901            return TransactionPattern::Payroll;
902        }
903
904        // Accrual: DR Expense, CR Liability (single)
905        if has_debit_expense && has_credit_liability && credit_classes.len() == 1 {
906            return TransactionPattern::Accrual;
907        }
908
909        TransactionPattern::Unknown
910    }
911}
912
913/// A line item classified as debit or credit.
914#[derive(Debug, Clone)]
915pub struct ClassifiedLine {
916    /// Line number from original entry.
917    pub line_number: u32,
918    /// Account code.
919    pub account_code: String,
920    /// Amount (always positive, sign determined by is_debit).
921    pub amount: FixedPoint128,
922    /// True if debit, false if credit.
923    pub is_debit: bool,
924    /// Entity ID.
925    pub entity_id: String,
926    /// Cost center (optional).
927    pub cost_center: Option<String>,
928}
929
930/// An accounting flow (directed edge in the network).
931#[derive(Debug, Clone, Serialize, Deserialize)]
932pub struct AccountingFlow {
933    /// Unique flow ID.
934    pub flow_id: u64,
935    /// Source journal entry ID.
936    pub entry_id: u64,
937    /// Source account (debit account).
938    pub from_account: String,
939    /// Target account (credit account).
940    pub to_account: String,
941    /// Flow amount (fixed-point).
942    pub amount: FixedPoint128,
943    /// Amount as f64 for convenience.
944    pub amount_f64: f64,
945    /// Timestamp of the journal entry.
946    pub timestamp: u64,
947    /// Solving method used to derive this flow.
948    pub method: SolvingMethod,
949    /// Confidence level (derived from method).
950    pub confidence: f64,
951    /// Source entity ID.
952    pub from_entity: String,
953    /// Target entity ID.
954    pub to_entity: String,
955    /// Currency.
956    pub currency: String,
957    /// Source line numbers (for audit trail).
958    pub source_lines: Vec<u32>,
959    // === Enhanced Fields ===
960    /// Source account class.
961    #[serde(default)]
962    pub from_account_class: Option<AccountClass>,
963    /// Target account class.
964    #[serde(default)]
965    pub to_account_class: Option<AccountClass>,
966    /// Detected transaction pattern.
967    #[serde(default)]
968    pub pattern: Option<TransactionPattern>,
969    /// Whether this flow represents a VAT/tax component.
970    #[serde(default)]
971    pub is_tax_flow: bool,
972    /// VAT rate if this is a tax flow.
973    #[serde(default)]
974    pub vat_rate: Option<f64>,
975    /// Whether this is an intercompany flow.
976    #[serde(default)]
977    pub is_intercompany: bool,
978    /// Confidence adjustments applied.
979    #[serde(default)]
980    pub confidence_factors: Vec<String>,
981}
982
983/// Result of generating the accounting network for a single entry.
984#[derive(Debug, Clone)]
985pub struct EntryNetworkResult {
986    /// Original entry ID.
987    pub entry_id: u64,
988    /// Generated flows.
989    pub flows: Vec<AccountingFlow>,
990    /// Solving method used.
991    pub method: SolvingMethod,
992    /// Weighted confidence (method confidence * amount weight).
993    pub confidence: f64,
994    /// Whether the entry was balanced.
995    pub was_balanced: bool,
996    /// Error message if any.
997    pub error: Option<String>,
998    /// Detected transaction pattern.
999    pub pattern: TransactionPattern,
1000    /// Detected VAT pattern if any.
1001    pub vat_pattern: Option<VatPattern>,
1002}
1003
1004/// Complete accounting network.
1005#[derive(Debug, Clone, Default)]
1006pub struct AccountingNetwork {
1007    /// All accounting flows.
1008    pub flows: Vec<AccountingFlow>,
1009    /// Account nodes (unique accounts).
1010    pub accounts: HashSet<String>,
1011    /// Account index mapping for CSR representation.
1012    pub account_index: HashMap<String, usize>,
1013    /// Adjacency list (from_account -> [(to_account, flow_index)]).
1014    pub adjacency: HashMap<String, Vec<(String, usize)>>,
1015    /// Statistics.
1016    pub stats: NetworkGenerationStats,
1017}
1018
1019impl AccountingNetwork {
1020    /// Create a new empty network.
1021    pub fn new() -> Self {
1022        Self::default()
1023    }
1024
1025    /// Add a flow to the network.
1026    pub fn add_flow(&mut self, flow: AccountingFlow) {
1027        // Add accounts
1028        if !self.accounts.contains(&flow.from_account) {
1029            let idx = self.accounts.len();
1030            self.accounts.insert(flow.from_account.clone());
1031            self.account_index.insert(flow.from_account.clone(), idx);
1032        }
1033        if !self.accounts.contains(&flow.to_account) {
1034            let idx = self.accounts.len();
1035            self.accounts.insert(flow.to_account.clone());
1036            self.account_index.insert(flow.to_account.clone(), idx);
1037        }
1038
1039        // Add to adjacency list
1040        let flow_idx = self.flows.len();
1041        self.adjacency
1042            .entry(flow.from_account.clone())
1043            .or_default()
1044            .push((flow.to_account.clone(), flow_idx));
1045
1046        // Add flow
1047        self.flows.push(flow);
1048    }
1049
1050    /// Get outgoing flows from an account.
1051    pub fn outgoing_flows(&self, account: &str) -> Vec<&AccountingFlow> {
1052        self.adjacency
1053            .get(account)
1054            .map(|edges| edges.iter().map(|(_, idx)| &self.flows[*idx]).collect())
1055            .unwrap_or_default()
1056    }
1057
1058    /// Get incoming flows to an account.
1059    pub fn incoming_flows(&self, account: &str) -> Vec<&AccountingFlow> {
1060        self.flows
1061            .iter()
1062            .filter(|f| f.to_account == account)
1063            .collect()
1064    }
1065
1066    /// Query flows within a time window.
1067    pub fn query_temporal(&self, start_time: u64, end_time: u64) -> Vec<&AccountingFlow> {
1068        self.flows
1069            .iter()
1070            .filter(|f| f.timestamp >= start_time && f.timestamp <= end_time)
1071            .collect()
1072    }
1073
1074    /// Calculate total flow volume.
1075    pub fn total_volume(&self) -> f64 {
1076        self.flows.iter().map(|f| f.amount_f64).sum()
1077    }
1078
1079    /// Calculate weighted average confidence.
1080    pub fn weighted_confidence(&self) -> f64 {
1081        if self.flows.is_empty() {
1082            return 0.0;
1083        }
1084        let total_amount: f64 = self.flows.iter().map(|f| f.amount_f64).sum();
1085        if total_amount == 0.0 {
1086            return self.flows.iter().map(|f| f.confidence).sum::<f64>() / self.flows.len() as f64;
1087        }
1088        self.flows
1089            .iter()
1090            .map(|f| f.confidence * f.amount_f64)
1091            .sum::<f64>()
1092            / total_amount
1093    }
1094}
1095
1096/// Statistics for network generation.
1097#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1098pub struct NetworkGenerationStats {
1099    /// Total entries processed.
1100    pub total_entries: usize,
1101    /// Entries solved by Method A (1-to-1).
1102    pub method_a_count: usize,
1103    /// Entries solved by Method B (n-to-n).
1104    pub method_b_count: usize,
1105    /// Entries solved by Method C (n-to-m).
1106    pub method_c_count: usize,
1107    /// Entries solved by Method D (aggregation).
1108    pub method_d_count: usize,
1109    /// Entries solved by Method E (decomposition).
1110    pub method_e_count: usize,
1111    /// Unsolvable entries (routed to suspense).
1112    pub unsolvable_count: usize,
1113    /// Total flows generated.
1114    pub total_flows: usize,
1115    /// Total volume processed.
1116    pub total_volume: f64,
1117    /// Weighted average confidence.
1118    pub weighted_confidence: f64,
1119    /// Processing time in microseconds.
1120    pub processing_time_us: u64,
1121    /// Entries with balance errors.
1122    pub balance_errors: usize,
1123    // === Enhanced Statistics ===
1124    /// Entries with VAT detected.
1125    pub vat_entries_count: usize,
1126    /// Total VAT amount detected.
1127    pub total_vat_amount: f64,
1128    /// Entries classified as sales.
1129    pub sales_pattern_count: usize,
1130    /// Entries classified as purchases.
1131    pub purchase_pattern_count: usize,
1132    /// Entries classified as payments.
1133    pub payment_pattern_count: usize,
1134    /// Entries classified as payroll.
1135    pub payroll_pattern_count: usize,
1136    /// Intercompany entries.
1137    pub intercompany_count: usize,
1138    /// Average confidence boost from pattern matching.
1139    pub avg_confidence_boost: f64,
1140}
1141
1142// ============================================================================
1143// Network Generation Configuration
1144// ============================================================================
1145
1146/// Configuration for network generation.
1147#[derive(Debug, Clone, Serialize, Deserialize)]
1148pub struct NetworkGenerationConfig {
1149    /// Tolerance for amount matching (default: 0.01).
1150    pub amount_tolerance: f64,
1151    /// Maximum lines for Method B (bijective matching).
1152    pub max_lines_method_b: usize,
1153    /// Maximum lines for Method C (partition matching).
1154    pub max_lines_method_c: usize,
1155    /// Maximum partition search depth for Method C.
1156    pub max_partition_depth: usize,
1157    /// Enable Method D (aggregation).
1158    pub enable_aggregation: bool,
1159    /// Enable Method E (decomposition).
1160    pub enable_decomposition: bool,
1161    /// Suspense account code for unsolvable entries.
1162    pub suspense_account: String,
1163    /// Whether to fail on unbalanced entries.
1164    pub strict_balance: bool,
1165    // === Enhanced Configuration ===
1166    /// Enable pattern-based matching and confidence boosting.
1167    pub enable_pattern_matching: bool,
1168    /// Enable VAT detection and flow splitting.
1169    pub enable_vat_detection: bool,
1170    /// Apply confidence boost from pattern recognition.
1171    pub apply_confidence_boost: bool,
1172    /// Annotate flows with account classes.
1173    pub annotate_account_classes: bool,
1174    /// Custom VAT rates (in addition to built-in rates).
1175    #[serde(default)]
1176    pub custom_vat_rates: Vec<f64>,
1177}
1178
1179impl Default for NetworkGenerationConfig {
1180    fn default() -> Self {
1181        Self {
1182            amount_tolerance: 0.01,
1183            max_lines_method_b: 10,
1184            max_lines_method_c: 20,
1185            max_partition_depth: 1000,
1186            enable_aggregation: true,
1187            enable_decomposition: true,
1188            suspense_account: "SUSPENSE".to_string(),
1189            strict_balance: false,
1190            // Enhanced defaults
1191            enable_pattern_matching: true,
1192            enable_vat_detection: true,
1193            apply_confidence_boost: true,
1194            annotate_account_classes: true,
1195            custom_vat_rates: Vec::new(),
1196        }
1197    }
1198}
1199
1200// ============================================================================
1201// Network Generation Kernel
1202// ============================================================================
1203
1204/// Accounting Network Generation Kernel.
1205///
1206/// Transforms journal entries into a directed accounting network.
1207/// Uses GPU-accelerated batch processing for high throughput.
1208///
1209/// ## Enhanced Features
1210/// - Account classification using standard chart of accounts conventions
1211/// - VAT/tax detection and automatic flow splitting
1212/// - Pattern recognition for common transaction types
1213/// - Confidence boosting based on domain knowledge
1214#[derive(Debug, Clone)]
1215pub struct NetworkGeneration {
1216    metadata: KernelMetadata,
1217    config: NetworkGenerationConfig,
1218    pattern_matcher: PatternMatcher,
1219}
1220
1221impl Default for NetworkGeneration {
1222    fn default() -> Self {
1223        Self::new()
1224    }
1225}
1226
1227impl NetworkGeneration {
1228    /// Create a new network generation kernel.
1229    #[must_use]
1230    pub fn new() -> Self {
1231        Self::with_config(NetworkGenerationConfig::default())
1232    }
1233
1234    /// Create with custom configuration.
1235    #[must_use]
1236    pub fn with_config(config: NetworkGenerationConfig) -> Self {
1237        let mut pattern_matcher = PatternMatcher::new();
1238
1239        // Add custom VAT rates if configured
1240        for rate in &config.custom_vat_rates {
1241            pattern_matcher.vat_detector.add_rate(VatRate {
1242                rate: *rate,
1243                jurisdiction: VatJurisdiction::Generic,
1244                rate_type: VatRateType::Standard,
1245            });
1246        }
1247
1248        Self {
1249            metadata: KernelMetadata::batch("accounting/network-generation", Domain::Accounting)
1250                .with_description("Journal entry to accounting network transformation")
1251                .with_throughput(50_000)
1252                .with_latency_us(50.0)
1253                .with_gpu_native(true),
1254            config,
1255            pattern_matcher,
1256        }
1257    }
1258
1259    /// Generate accounting network from journal entries.
1260    pub fn generate(&self, entries: &[JournalEntry]) -> AccountingNetwork {
1261        let start = Instant::now();
1262        let mut network = AccountingNetwork::new();
1263        let mut flow_id = 0u64;
1264        let mut total_confidence_boost = 0.0f64;
1265        let mut boosted_entries = 0usize;
1266
1267        let mut stats = NetworkGenerationStats {
1268            total_entries: entries.len(),
1269            ..Default::default()
1270        };
1271
1272        for entry in entries {
1273            let result = self.process_entry(entry, &mut flow_id);
1274
1275            // Update method stats
1276            match result.method {
1277                SolvingMethod::MethodA => stats.method_a_count += 1,
1278                SolvingMethod::MethodB => stats.method_b_count += 1,
1279                SolvingMethod::MethodC => stats.method_c_count += 1,
1280                SolvingMethod::MethodD => stats.method_d_count += 1,
1281                SolvingMethod::MethodE => stats.method_e_count += 1,
1282                SolvingMethod::Unsolvable => stats.unsolvable_count += 1,
1283            }
1284
1285            // Update pattern stats
1286            match result.pattern {
1287                TransactionPattern::SimpleSale | TransactionPattern::SaleWithVat => {
1288                    stats.sales_pattern_count += 1;
1289                }
1290                TransactionPattern::SimplePurchase | TransactionPattern::PurchaseWithVat => {
1291                    stats.purchase_pattern_count += 1;
1292                }
1293                TransactionPattern::Payment | TransactionPattern::Receipt => {
1294                    stats.payment_pattern_count += 1;
1295                }
1296                TransactionPattern::Payroll => {
1297                    stats.payroll_pattern_count += 1;
1298                }
1299                TransactionPattern::Intercompany => {
1300                    stats.intercompany_count += 1;
1301                }
1302                _ => {}
1303            }
1304
1305            // Update VAT stats
1306            if let Some(ref vat) = result.vat_pattern {
1307                stats.vat_entries_count += 1;
1308                stats.total_vat_amount += vat.tax_amount.to_f64();
1309            }
1310
1311            // Track confidence boost
1312            let boost = result.pattern.confidence_boost();
1313            if boost > 0.0 {
1314                total_confidence_boost += boost;
1315                boosted_entries += 1;
1316            }
1317
1318            if !result.was_balanced {
1319                stats.balance_errors += 1;
1320            }
1321
1322            // Add flows to network
1323            for flow in result.flows {
1324                stats.total_volume += flow.amount_f64;
1325                network.add_flow(flow);
1326            }
1327        }
1328
1329        stats.total_flows = network.flows.len();
1330        stats.weighted_confidence = network.weighted_confidence();
1331        stats.processing_time_us = start.elapsed().as_micros() as u64;
1332        stats.avg_confidence_boost = if boosted_entries > 0 {
1333            total_confidence_boost / boosted_entries as f64
1334        } else {
1335            0.0
1336        };
1337        network.stats = stats;
1338
1339        network
1340    }
1341
1342    /// Process a single journal entry.
1343    fn process_entry(&self, entry: &JournalEntry, flow_id: &mut u64) -> EntryNetworkResult {
1344        // Classify lines into debits and credits
1345        let (debits, credits, balance_diff) = self.classify_lines(&entry.lines);
1346
1347        // Check balance
1348        let tolerance = FixedPoint128::from_f64(self.config.amount_tolerance);
1349        let was_balanced = balance_diff.is_zero(tolerance);
1350
1351        if !was_balanced && self.config.strict_balance {
1352            return EntryNetworkResult {
1353                entry_id: entry.id,
1354                flows: Vec::new(),
1355                method: SolvingMethod::Unsolvable,
1356                confidence: 0.0,
1357                was_balanced: false,
1358                error: Some(format!("Entry unbalanced: diff={}", balance_diff.to_f64())),
1359                pattern: TransactionPattern::Unknown,
1360                vat_pattern: None,
1361            };
1362        }
1363
1364        // Detect transaction pattern if enabled
1365        let (pattern, vat_pattern) = if self.config.enable_pattern_matching {
1366            self.pattern_matcher.detect_pattern(&debits, &credits)
1367        } else {
1368            (TransactionPattern::Unknown, None)
1369        };
1370
1371        let debit_count = debits.len();
1372        let credit_count = credits.len();
1373
1374        // Select solving method based on line counts
1375        let (mut flows, method) = if debit_count == 1 && credit_count == 1 {
1376            // Method A: Trivial 1-to-1
1377            (
1378                self.solve_method_a(entry, &debits, &credits, flow_id),
1379                SolvingMethod::MethodA,
1380            )
1381        } else if debit_count == credit_count && debit_count <= self.config.max_lines_method_b {
1382            // Method B: n-to-n bijective matching
1383            (
1384                self.solve_method_b(entry, &debits, &credits, flow_id),
1385                SolvingMethod::MethodB,
1386            )
1387        } else if (debit_count + credit_count) <= self.config.max_lines_method_c {
1388            // Method C: n-to-m partition matching
1389            (
1390                self.solve_method_c(entry, &debits, &credits, flow_id),
1391                SolvingMethod::MethodC,
1392            )
1393        } else if self.config.enable_aggregation {
1394            // Method D: Aggregation
1395            (
1396                self.solve_method_d(entry, &debits, &credits, flow_id),
1397                SolvingMethod::MethodD,
1398            )
1399        } else if self.config.enable_decomposition {
1400            // Method E: Decomposition
1401            (
1402                self.solve_method_e(entry, &debits, &credits, flow_id),
1403                SolvingMethod::MethodE,
1404            )
1405        } else {
1406            // Unsolvable - route to suspense
1407            (
1408                self.create_suspense_flows(entry, &debits, &credits, flow_id),
1409                SolvingMethod::Unsolvable,
1410            )
1411        };
1412
1413        // Calculate confidence with pattern boost
1414        let base_confidence = method.confidence();
1415        let pattern_boost = if self.config.apply_confidence_boost {
1416            pattern.confidence_boost()
1417        } else {
1418            0.0
1419        };
1420        let final_confidence = (base_confidence + pattern_boost).min(1.0);
1421
1422        // Enhance flows with pattern and classification info
1423        self.enhance_flows(&mut flows, pattern, vat_pattern.as_ref(), final_confidence);
1424
1425        EntryNetworkResult {
1426            entry_id: entry.id,
1427            flows,
1428            method,
1429            confidence: final_confidence,
1430            was_balanced,
1431            error: None,
1432            pattern,
1433            vat_pattern,
1434        }
1435    }
1436
1437    /// Create a new AccountingFlow with default values for enhanced fields.
1438    #[allow(clippy::too_many_arguments)]
1439    fn create_flow(
1440        flow_id: u64,
1441        entry_id: u64,
1442        from_account: String,
1443        to_account: String,
1444        amount: FixedPoint128,
1445        timestamp: u64,
1446        method: SolvingMethod,
1447        from_entity: String,
1448        to_entity: String,
1449        currency: String,
1450        source_lines: Vec<u32>,
1451    ) -> AccountingFlow {
1452        AccountingFlow {
1453            flow_id,
1454            entry_id,
1455            from_account,
1456            to_account,
1457            amount,
1458            amount_f64: amount.to_f64(),
1459            timestamp,
1460            method,
1461            confidence: method.confidence(),
1462            from_entity,
1463            to_entity,
1464            currency,
1465            source_lines,
1466            // Enhanced fields - set to defaults, will be updated by enhance_flows
1467            from_account_class: None,
1468            to_account_class: None,
1469            pattern: None,
1470            is_tax_flow: false,
1471            vat_rate: None,
1472            is_intercompany: false,
1473            confidence_factors: Vec::new(),
1474        }
1475    }
1476
1477    /// Enhance flows with pattern and classification information.
1478    fn enhance_flows(
1479        &self,
1480        flows: &mut [AccountingFlow],
1481        pattern: TransactionPattern,
1482        vat_pattern: Option<&VatPattern>,
1483        confidence: f64,
1484    ) {
1485        let tax_accounts: HashSet<String> = vat_pattern
1486            .map(|v| v.tax_accounts.iter().cloned().collect())
1487            .unwrap_or_default();
1488        let vat_rate = vat_pattern.map(|v| v.rate.rate);
1489
1490        for flow in flows {
1491            // Update confidence
1492            flow.confidence = confidence;
1493
1494            // Add pattern
1495            if self.config.enable_pattern_matching {
1496                flow.pattern = Some(pattern);
1497            }
1498
1499            // Add account classifications
1500            if self.config.annotate_account_classes {
1501                flow.from_account_class = Some(AccountClass::from_account_code(&flow.from_account));
1502                flow.to_account_class = Some(AccountClass::from_account_code(&flow.to_account));
1503            }
1504
1505            // Mark tax flows
1506            if self.config.enable_vat_detection {
1507                flow.is_tax_flow = tax_accounts.contains(&flow.from_account)
1508                    || tax_accounts.contains(&flow.to_account);
1509                if flow.is_tax_flow {
1510                    flow.vat_rate = vat_rate;
1511                }
1512            }
1513
1514            // Mark intercompany flows
1515            flow.is_intercompany = flow.from_entity != flow.to_entity;
1516
1517            // Add confidence factors
1518            if pattern != TransactionPattern::Unknown {
1519                flow.confidence_factors
1520                    .push(format!("pattern:{:?}", pattern));
1521            }
1522            if flow.is_tax_flow {
1523                flow.confidence_factors.push("vat_detected".to_string());
1524            }
1525            if flow.is_intercompany {
1526                flow.confidence_factors.push("intercompany".to_string());
1527            }
1528        }
1529    }
1530
1531    /// Classify journal lines into debits and credits.
1532    fn classify_lines(
1533        &self,
1534        lines: &[JournalLine],
1535    ) -> (Vec<ClassifiedLine>, Vec<ClassifiedLine>, FixedPoint128) {
1536        let mut debits = Vec::new();
1537        let mut credits = Vec::new();
1538        let mut total_debit = FixedPoint128::ZERO;
1539        let mut total_credit = FixedPoint128::ZERO;
1540
1541        for line in lines {
1542            if line.debit > 0.0 {
1543                let amount = FixedPoint128::from_f64(line.debit);
1544                total_debit += amount;
1545                debits.push(ClassifiedLine {
1546                    line_number: line.line_number,
1547                    account_code: line.account_code.clone(),
1548                    amount,
1549                    is_debit: true,
1550                    entity_id: line.entity_id.clone(),
1551                    cost_center: line.cost_center.clone(),
1552                });
1553            }
1554            if line.credit > 0.0 {
1555                let amount = FixedPoint128::from_f64(line.credit);
1556                total_credit += amount;
1557                credits.push(ClassifiedLine {
1558                    line_number: line.line_number,
1559                    account_code: line.account_code.clone(),
1560                    amount,
1561                    is_debit: false,
1562                    entity_id: line.entity_id.clone(),
1563                    cost_center: line.cost_center.clone(),
1564                });
1565            }
1566        }
1567
1568        let balance_diff = total_debit - total_credit;
1569        (debits, credits, balance_diff)
1570    }
1571
1572    // ========================================================================
1573    // Method A: Trivial 1-to-1 Matching
1574    // ========================================================================
1575
1576    /// Method A: Simple 1-to-1 flow for 2-line entries.
1577    /// Confidence: 1.0 (deterministic)
1578    fn solve_method_a(
1579        &self,
1580        entry: &JournalEntry,
1581        debits: &[ClassifiedLine],
1582        credits: &[ClassifiedLine],
1583        flow_id: &mut u64,
1584    ) -> Vec<AccountingFlow> {
1585        if debits.len() != 1 || credits.len() != 1 {
1586            return Vec::new();
1587        }
1588
1589        let debit = &debits[0];
1590        let credit = &credits[0];
1591        let amount = debit.amount.min(credit.amount);
1592        let currency = entry
1593            .lines
1594            .first()
1595            .map(|l| l.currency.clone())
1596            .unwrap_or_else(|| "USD".to_string());
1597
1598        let flow = Self::create_flow(
1599            *flow_id,
1600            entry.id,
1601            debit.account_code.clone(),
1602            credit.account_code.clone(),
1603            amount,
1604            entry.posting_date,
1605            SolvingMethod::MethodA,
1606            debit.entity_id.clone(),
1607            credit.entity_id.clone(),
1608            currency,
1609            vec![debit.line_number, credit.line_number],
1610        );
1611
1612        *flow_id += 1;
1613        vec![flow]
1614    }
1615
1616    // ========================================================================
1617    // Method B: n-to-n Bijective Matching (Hungarian Algorithm)
1618    // ========================================================================
1619
1620    /// Method B: n-to-n bijective matching using greedy amount matching.
1621    /// Confidence: 0.95
1622    fn solve_method_b(
1623        &self,
1624        entry: &JournalEntry,
1625        debits: &[ClassifiedLine],
1626        credits: &[ClassifiedLine],
1627        flow_id: &mut u64,
1628    ) -> Vec<AccountingFlow> {
1629        if debits.len() != credits.len() {
1630            return self.solve_method_c(entry, debits, credits, flow_id);
1631        }
1632
1633        let currency = entry
1634            .lines
1635            .first()
1636            .map(|l| l.currency.clone())
1637            .unwrap_or_else(|| "USD".to_string());
1638        let tolerance = FixedPoint128::from_f64(self.config.amount_tolerance);
1639
1640        // Try to find exact amount matches first
1641        let mut flows = Vec::new();
1642        let mut matched_credits: HashSet<usize> = HashSet::new();
1643        let mut matched_debits: HashSet<usize> = HashSet::new();
1644
1645        // Pass 1: Exact matches
1646        for (di, debit) in debits.iter().enumerate() {
1647            for (ci, credit) in credits.iter().enumerate() {
1648                if matched_credits.contains(&ci) {
1649                    continue;
1650                }
1651                if debit.amount.approx_eq(credit.amount, tolerance) {
1652                    flows.push(Self::create_flow(
1653                        *flow_id,
1654                        entry.id,
1655                        debit.account_code.clone(),
1656                        credit.account_code.clone(),
1657                        debit.amount,
1658                        entry.posting_date,
1659                        SolvingMethod::MethodB,
1660                        debit.entity_id.clone(),
1661                        credit.entity_id.clone(),
1662                        currency.clone(),
1663                        vec![debit.line_number, credit.line_number],
1664                    ));
1665                    *flow_id += 1;
1666                    matched_credits.insert(ci);
1667                    matched_debits.insert(di);
1668                    break;
1669                }
1670            }
1671        }
1672
1673        // Pass 2: Match remaining by order (fallback)
1674        let unmatched_debits: Vec<_> = debits
1675            .iter()
1676            .enumerate()
1677            .filter(|(i, _)| !matched_debits.contains(i))
1678            .map(|(_, d)| d)
1679            .collect();
1680        let unmatched_credits: Vec<_> = credits
1681            .iter()
1682            .enumerate()
1683            .filter(|(i, _)| !matched_credits.contains(i))
1684            .map(|(_, c)| c)
1685            .collect();
1686
1687        for (debit, credit) in unmatched_debits.iter().zip(unmatched_credits.iter()) {
1688            let amount = debit.amount.min(credit.amount);
1689            flows.push(Self::create_flow(
1690                *flow_id,
1691                entry.id,
1692                debit.account_code.clone(),
1693                credit.account_code.clone(),
1694                amount,
1695                entry.posting_date,
1696                SolvingMethod::MethodB,
1697                debit.entity_id.clone(),
1698                credit.entity_id.clone(),
1699                currency.clone(),
1700                vec![debit.line_number, credit.line_number],
1701            ));
1702            *flow_id += 1;
1703        }
1704
1705        flows
1706    }
1707
1708    // ========================================================================
1709    // Method C: n-to-m Partition Matching
1710    // ========================================================================
1711
1712    /// Method C: n-to-m partition matching.
1713    /// Tries to find subset partitions where debit sums equal credit sums.
1714    /// Confidence: 0.85
1715    fn solve_method_c(
1716        &self,
1717        entry: &JournalEntry,
1718        debits: &[ClassifiedLine],
1719        credits: &[ClassifiedLine],
1720        flow_id: &mut u64,
1721    ) -> Vec<AccountingFlow> {
1722        let currency = entry
1723            .lines
1724            .first()
1725            .map(|l| l.currency.clone())
1726            .unwrap_or_else(|| "USD".to_string());
1727        let tolerance = FixedPoint128::from_f64(self.config.amount_tolerance);
1728
1729        // Try to partition credits to match each debit
1730        let mut flows = Vec::new();
1731        let mut remaining_credits: Vec<(usize, ClassifiedLine)> =
1732            credits.iter().cloned().enumerate().collect();
1733
1734        for debit in debits {
1735            // Find subset of credits that sum to this debit amount
1736            if let Some(matching_subset) =
1737                self.find_partition_subset(&remaining_credits, debit.amount, tolerance)
1738            {
1739                // Create flows for each credit in the matching subset
1740                for (ci, credit) in &matching_subset {
1741                    flows.push(Self::create_flow(
1742                        *flow_id,
1743                        entry.id,
1744                        debit.account_code.clone(),
1745                        credit.account_code.clone(),
1746                        credit.amount,
1747                        entry.posting_date,
1748                        SolvingMethod::MethodC,
1749                        debit.entity_id.clone(),
1750                        credit.entity_id.clone(),
1751                        currency.clone(),
1752                        vec![debit.line_number, credit.line_number],
1753                    ));
1754                    *flow_id += 1;
1755
1756                    // Remove matched credit
1757                    remaining_credits.retain(|(idx, _)| idx != ci);
1758                }
1759            } else {
1760                // Fallback: distribute debit proportionally across remaining credits
1761                let total_remaining: FixedPoint128 = remaining_credits
1762                    .iter()
1763                    .map(|(_, c)| c.amount)
1764                    .fold(FixedPoint128::ZERO, |acc, a| acc + a);
1765
1766                if total_remaining.value > 0 {
1767                    for (_, credit) in &remaining_credits {
1768                        // Proportional allocation
1769                        let proportion = credit.amount.value as f64 / total_remaining.value as f64;
1770                        let allocated = FixedPoint128::from_f64(debit.amount.to_f64() * proportion);
1771
1772                        if allocated.value > 0 {
1773                            let mut flow = Self::create_flow(
1774                                *flow_id,
1775                                entry.id,
1776                                debit.account_code.clone(),
1777                                credit.account_code.clone(),
1778                                allocated,
1779                                entry.posting_date,
1780                                SolvingMethod::MethodC,
1781                                debit.entity_id.clone(),
1782                                credit.entity_id.clone(),
1783                                currency.clone(),
1784                                vec![debit.line_number, credit.line_number],
1785                            );
1786                            // Lower confidence for proportional allocation
1787                            flow.confidence *= 0.9;
1788                            flows.push(flow);
1789                            *flow_id += 1;
1790                        }
1791                    }
1792                }
1793            }
1794        }
1795
1796        flows
1797    }
1798
1799    /// Find a subset of credits that sums to the target amount.
1800    /// Uses dynamic programming for subset sum.
1801    fn find_partition_subset(
1802        &self,
1803        credits: &[(usize, ClassifiedLine)],
1804        target: FixedPoint128,
1805        tolerance: FixedPoint128,
1806    ) -> Option<Vec<(usize, ClassifiedLine)>> {
1807        let n = credits.len();
1808        if n == 0 {
1809            return None;
1810        }
1811
1812        // Check single credit matches
1813        for (idx, credit) in credits {
1814            if credit.amount.approx_eq(target, tolerance) {
1815                return Some(vec![(*idx, credit.clone())]);
1816            }
1817        }
1818
1819        // For small n, try exhaustive subset search
1820        if n <= 12 {
1821            return self.exhaustive_subset_search(credits, target, tolerance);
1822        }
1823
1824        // For larger n, use greedy approximation
1825        self.greedy_subset_search(credits, target, tolerance)
1826    }
1827
1828    /// Exhaustive subset search for small n.
1829    fn exhaustive_subset_search(
1830        &self,
1831        credits: &[(usize, ClassifiedLine)],
1832        target: FixedPoint128,
1833        tolerance: FixedPoint128,
1834    ) -> Option<Vec<(usize, ClassifiedLine)>> {
1835        let n = credits.len();
1836        let max_subsets = (1u32 << n).min(self.config.max_partition_depth as u32);
1837
1838        for mask in 1..max_subsets {
1839            let mut sum = FixedPoint128::ZERO;
1840            let mut subset = Vec::new();
1841
1842            for (i, (idx, credit)) in credits.iter().enumerate() {
1843                if (mask >> i) & 1 == 1 {
1844                    sum += credit.amount;
1845                    subset.push((*idx, credit.clone()));
1846                }
1847            }
1848
1849            if sum.approx_eq(target, tolerance) {
1850                return Some(subset);
1851            }
1852        }
1853
1854        None
1855    }
1856
1857    /// Greedy subset search for larger n.
1858    fn greedy_subset_search(
1859        &self,
1860        credits: &[(usize, ClassifiedLine)],
1861        target: FixedPoint128,
1862        tolerance: FixedPoint128,
1863    ) -> Option<Vec<(usize, ClassifiedLine)>> {
1864        // Sort by amount descending
1865        let mut sorted: Vec<_> = credits.to_vec();
1866        sorted.sort_by(|a, b| b.1.amount.cmp(&a.1.amount));
1867
1868        let mut remaining = target;
1869        let mut subset = Vec::new();
1870
1871        for (idx, credit) in sorted {
1872            if credit.amount <= remaining + tolerance {
1873                remaining -= credit.amount;
1874                subset.push((idx, credit));
1875
1876                if remaining.is_zero(tolerance) {
1877                    return Some(subset);
1878                }
1879            }
1880        }
1881
1882        None
1883    }
1884
1885    // ========================================================================
1886    // Method D: Aggregation
1887    // ========================================================================
1888
1889    /// Method D: Aggregate small flows into larger flows.
1890    /// Used for entries with many lines.
1891    /// Confidence: 0.70
1892    fn solve_method_d(
1893        &self,
1894        entry: &JournalEntry,
1895        debits: &[ClassifiedLine],
1896        credits: &[ClassifiedLine],
1897        flow_id: &mut u64,
1898    ) -> Vec<AccountingFlow> {
1899        let currency = entry
1900            .lines
1901            .first()
1902            .map(|l| l.currency.clone())
1903            .unwrap_or_else(|| "USD".to_string());
1904
1905        // Aggregate debits by account
1906        let mut debit_aggregates: HashMap<String, FixedPoint128> = HashMap::new();
1907        let mut debit_entities: HashMap<String, String> = HashMap::new();
1908        let mut debit_lines: HashMap<String, Vec<u32>> = HashMap::new();
1909
1910        for debit in debits {
1911            *debit_aggregates
1912                .entry(debit.account_code.clone())
1913                .or_default() += debit.amount;
1914            debit_entities
1915                .entry(debit.account_code.clone())
1916                .or_insert_with(|| debit.entity_id.clone());
1917            debit_lines
1918                .entry(debit.account_code.clone())
1919                .or_default()
1920                .push(debit.line_number);
1921        }
1922
1923        // Aggregate credits by account
1924        let mut credit_aggregates: HashMap<String, FixedPoint128> = HashMap::new();
1925        let mut credit_entities: HashMap<String, String> = HashMap::new();
1926        let mut credit_lines: HashMap<String, Vec<u32>> = HashMap::new();
1927
1928        for credit in credits {
1929            *credit_aggregates
1930                .entry(credit.account_code.clone())
1931                .or_default() += credit.amount;
1932            credit_entities
1933                .entry(credit.account_code.clone())
1934                .or_insert_with(|| credit.entity_id.clone());
1935            credit_lines
1936                .entry(credit.account_code.clone())
1937                .or_default()
1938                .push(credit.line_number);
1939        }
1940
1941        // Create flows between aggregated accounts
1942        let mut flows = Vec::new();
1943        let total_credit: FixedPoint128 = credit_aggregates
1944            .values()
1945            .copied()
1946            .fold(FixedPoint128::ZERO, |acc, a| acc + a);
1947
1948        for (debit_account, debit_amount) in &debit_aggregates {
1949            for (credit_account, credit_amount) in &credit_aggregates {
1950                // Allocate proportionally
1951                let proportion = if total_credit.value > 0 {
1952                    credit_amount.value as f64 / total_credit.value as f64
1953                } else {
1954                    1.0 / credit_aggregates.len() as f64
1955                };
1956
1957                let allocated = FixedPoint128::from_f64(debit_amount.to_f64() * proportion);
1958
1959                if allocated.value > 0 {
1960                    let mut source_lines =
1961                        debit_lines.get(debit_account).cloned().unwrap_or_default();
1962                    source_lines.extend(
1963                        credit_lines
1964                            .get(credit_account)
1965                            .cloned()
1966                            .unwrap_or_default(),
1967                    );
1968
1969                    flows.push(Self::create_flow(
1970                        *flow_id,
1971                        entry.id,
1972                        debit_account.clone(),
1973                        credit_account.clone(),
1974                        allocated,
1975                        entry.posting_date,
1976                        SolvingMethod::MethodD,
1977                        debit_entities
1978                            .get(debit_account)
1979                            .cloned()
1980                            .unwrap_or_default(),
1981                        credit_entities
1982                            .get(credit_account)
1983                            .cloned()
1984                            .unwrap_or_default(),
1985                        currency.clone(),
1986                        source_lines,
1987                    ));
1988                    *flow_id += 1;
1989                }
1990            }
1991        }
1992
1993        flows
1994    }
1995
1996    // ========================================================================
1997    // Method E: Decomposition
1998    // ========================================================================
1999
2000    /// Method E: Decompose complex entries by entity.
2001    /// Splits multi-entity entries into separate sub-networks.
2002    /// Confidence: 0.50
2003    fn solve_method_e(
2004        &self,
2005        entry: &JournalEntry,
2006        debits: &[ClassifiedLine],
2007        credits: &[ClassifiedLine],
2008        flow_id: &mut u64,
2009    ) -> Vec<AccountingFlow> {
2010        let currency = entry
2011            .lines
2012            .first()
2013            .map(|l| l.currency.clone())
2014            .unwrap_or_else(|| "USD".to_string());
2015
2016        // Group by entity
2017        let mut entity_debits: HashMap<String, Vec<&ClassifiedLine>> = HashMap::new();
2018        let mut entity_credits: HashMap<String, Vec<&ClassifiedLine>> = HashMap::new();
2019
2020        for debit in debits {
2021            entity_debits
2022                .entry(debit.entity_id.clone())
2023                .or_default()
2024                .push(debit);
2025        }
2026        for credit in credits {
2027            entity_credits
2028                .entry(credit.entity_id.clone())
2029                .or_default()
2030                .push(credit);
2031        }
2032
2033        let mut flows = Vec::new();
2034
2035        // Process each entity's portion
2036        for (entity, entity_debits_list) in &entity_debits {
2037            let entity_credits_list = entity_credits.get(entity);
2038
2039            for debit in entity_debits_list {
2040                // Try to match within entity first
2041                if let Some(credits_list) = entity_credits_list {
2042                    let total_entity_credit: FixedPoint128 = credits_list
2043                        .iter()
2044                        .map(|c| c.amount)
2045                        .fold(FixedPoint128::ZERO, |acc, a| acc + a);
2046
2047                    for credit in credits_list.iter() {
2048                        let proportion = if total_entity_credit.value > 0 {
2049                            credit.amount.value as f64 / total_entity_credit.value as f64
2050                        } else {
2051                            1.0 / credits_list.len() as f64
2052                        };
2053
2054                        let allocated = FixedPoint128::from_f64(debit.amount.to_f64() * proportion);
2055
2056                        if allocated.value > 0 {
2057                            flows.push(Self::create_flow(
2058                                *flow_id,
2059                                entry.id,
2060                                debit.account_code.clone(),
2061                                credit.account_code.clone(),
2062                                allocated,
2063                                entry.posting_date,
2064                                SolvingMethod::MethodE,
2065                                debit.entity_id.clone(),
2066                                credit.entity_id.clone(),
2067                                currency.clone(),
2068                                vec![debit.line_number, credit.line_number],
2069                            ));
2070                            *flow_id += 1;
2071                        }
2072                    }
2073                } else {
2074                    // Cross-entity flow: distribute across all credits
2075                    let all_credits: Vec<_> = credits.iter().collect();
2076                    let total_credit: FixedPoint128 = all_credits
2077                        .iter()
2078                        .map(|c| c.amount)
2079                        .fold(FixedPoint128::ZERO, |acc, a| acc + a);
2080
2081                    for credit in all_credits {
2082                        let proportion = if total_credit.value > 0 {
2083                            credit.amount.value as f64 / total_credit.value as f64
2084                        } else {
2085                            1.0 / credits.len() as f64
2086                        };
2087
2088                        let allocated = FixedPoint128::from_f64(debit.amount.to_f64() * proportion);
2089
2090                        if allocated.value > 0 {
2091                            let mut flow = Self::create_flow(
2092                                *flow_id,
2093                                entry.id,
2094                                debit.account_code.clone(),
2095                                credit.account_code.clone(),
2096                                allocated,
2097                                entry.posting_date,
2098                                SolvingMethod::MethodE,
2099                                debit.entity_id.clone(),
2100                                credit.entity_id.clone(),
2101                                currency.clone(),
2102                                vec![debit.line_number, credit.line_number],
2103                            );
2104                            // Lower confidence for cross-entity flows
2105                            flow.confidence *= 0.8;
2106                            flows.push(flow);
2107                            *flow_id += 1;
2108                        }
2109                    }
2110                }
2111            }
2112        }
2113
2114        flows
2115    }
2116
2117    // ========================================================================
2118    // Suspense Account Routing
2119    // ========================================================================
2120
2121    /// Route unsolvable entries to suspense account.
2122    fn create_suspense_flows(
2123        &self,
2124        entry: &JournalEntry,
2125        debits: &[ClassifiedLine],
2126        credits: &[ClassifiedLine],
2127        flow_id: &mut u64,
2128    ) -> Vec<AccountingFlow> {
2129        let currency = entry
2130            .lines
2131            .first()
2132            .map(|l| l.currency.clone())
2133            .unwrap_or_else(|| "USD".to_string());
2134
2135        let mut flows = Vec::new();
2136
2137        // Route all debits to suspense
2138        for debit in debits {
2139            let mut flow = Self::create_flow(
2140                *flow_id,
2141                entry.id,
2142                debit.account_code.clone(),
2143                self.config.suspense_account.clone(),
2144                debit.amount,
2145                entry.posting_date,
2146                SolvingMethod::Unsolvable,
2147                debit.entity_id.clone(),
2148                "SUSPENSE".to_string(),
2149                currency.clone(),
2150                vec![debit.line_number],
2151            );
2152            flow.confidence = 0.0;
2153            flows.push(flow);
2154            *flow_id += 1;
2155        }
2156
2157        // Route all credits from suspense
2158        for credit in credits {
2159            let mut flow = Self::create_flow(
2160                *flow_id,
2161                entry.id,
2162                self.config.suspense_account.clone(),
2163                credit.account_code.clone(),
2164                credit.amount,
2165                entry.posting_date,
2166                SolvingMethod::Unsolvable,
2167                "SUSPENSE".to_string(),
2168                credit.entity_id.clone(),
2169                currency.clone(),
2170                vec![credit.line_number],
2171            );
2172            flow.confidence = 0.0;
2173            flows.push(flow);
2174            *flow_id += 1;
2175        }
2176
2177        flows
2178    }
2179}
2180
2181impl GpuKernel for NetworkGeneration {
2182    fn metadata(&self) -> &KernelMetadata {
2183        &self.metadata
2184    }
2185}
2186
2187// ============================================================================
2188// Batch Kernel Implementation
2189// ============================================================================
2190
2191/// Input for network generation batch processing.
2192#[derive(Debug, Clone, Serialize, Deserialize)]
2193pub struct NetworkGenerationInput {
2194    /// Journal entries to process.
2195    pub entries: Vec<JournalEntry>,
2196    /// Configuration overrides.
2197    pub config: Option<NetworkGenerationConfig>,
2198}
2199
2200/// Output from network generation batch processing.
2201#[derive(Debug, Clone, Serialize, Deserialize)]
2202pub struct NetworkGenerationOutput {
2203    /// Generated accounting flows.
2204    pub flows: Vec<AccountingFlow>,
2205    /// Statistics.
2206    pub stats: NetworkGenerationStats,
2207}
2208
2209#[async_trait]
2210impl BatchKernel<NetworkGenerationInput, NetworkGenerationOutput> for NetworkGeneration {
2211    async fn execute(&self, input: NetworkGenerationInput) -> Result<NetworkGenerationOutput> {
2212        let kernel = if let Some(config) = input.config {
2213            NetworkGeneration::with_config(config)
2214        } else {
2215            self.clone()
2216        };
2217
2218        let network = kernel.generate(&input.entries);
2219
2220        Ok(NetworkGenerationOutput {
2221            flows: network.flows,
2222            stats: network.stats,
2223        })
2224    }
2225}
2226
2227// ============================================================================
2228// Ring Mode Messages and Handlers
2229// ============================================================================
2230
2231/// Ring message for adding a journal entry to the network.
2232#[derive(Debug, Clone, Serialize, Deserialize)]
2233pub struct AddEntryRing {
2234    /// Request ID.
2235    pub request_id: u64,
2236    /// Journal entry to add.
2237    pub entry_id: u64,
2238    /// Entry posting date.
2239    pub posting_date: u64,
2240    /// Entry lines as serialized data.
2241    pub lines_data: Vec<u8>,
2242}
2243
2244/// Response for adding an entry.
2245#[derive(Debug, Clone, Serialize, Deserialize)]
2246pub struct AddEntryResponse {
2247    /// Request ID.
2248    pub request_id: u64,
2249    /// Entry ID processed.
2250    pub entry_id: u64,
2251    /// Number of flows generated.
2252    pub flow_count: u32,
2253    /// Method used.
2254    pub method: u8,
2255    /// Confidence level (fixed-point, scale 1e6).
2256    pub confidence_fp: u64,
2257}
2258
2259/// Ring message for querying network flows.
2260#[derive(Debug, Clone, Serialize, Deserialize)]
2261pub struct QueryFlowsRing {
2262    /// Request ID.
2263    pub request_id: u64,
2264    /// Account to query (from or to).
2265    pub account: String,
2266    /// Time window start (0 for all).
2267    pub start_time: u64,
2268    /// Time window end (u64::MAX for all).
2269    pub end_time: u64,
2270    /// Maximum results.
2271    pub limit: u32,
2272}
2273
2274/// Response for flow query.
2275#[derive(Debug, Clone, Serialize, Deserialize)]
2276pub struct QueryFlowsResponse {
2277    /// Request ID.
2278    pub request_id: u64,
2279    /// Account queried.
2280    pub account: String,
2281    /// Total matching flows.
2282    pub total_count: u32,
2283    /// Total volume (fixed-point, scale 1e18).
2284    pub total_volume_fp: i128,
2285    /// Weighted confidence (fixed-point, scale 1e6).
2286    pub weighted_confidence_fp: u64,
2287}
2288
2289/// Ring message for network statistics.
2290#[derive(Debug, Clone, Serialize, Deserialize)]
2291pub struct NetworkStatsRing {
2292    /// Request ID.
2293    pub request_id: u64,
2294}
2295
2296/// Response for network statistics.
2297#[derive(Debug, Clone, Serialize, Deserialize)]
2298pub struct NetworkStatsResponse {
2299    /// Request ID.
2300    pub request_id: u64,
2301    /// Total accounts.
2302    pub total_accounts: u32,
2303    /// Total flows.
2304    pub total_flows: u32,
2305    /// Total volume (fixed-point, scale 1e18).
2306    pub total_volume_fp: i128,
2307    /// Method distribution (A, B, C, D, E, Unsolvable).
2308    pub method_counts: [u32; 6],
2309    /// Weighted confidence (fixed-point, scale 1e6).
2310    pub weighted_confidence_fp: u64,
2311}
2312
2313// ============================================================================
2314// Ring Kernel State
2315// ============================================================================
2316
2317/// Stateful network generation kernel for Ring mode.
2318#[derive(Debug)]
2319pub struct NetworkGenerationRing {
2320    metadata: KernelMetadata,
2321    config: NetworkGenerationConfig,
2322    /// Internal network state.
2323    network: std::sync::RwLock<AccountingNetwork>,
2324    /// Next flow ID.
2325    next_flow_id: std::sync::atomic::AtomicU64,
2326}
2327
2328impl Clone for NetworkGenerationRing {
2329    fn clone(&self) -> Self {
2330        Self {
2331            metadata: self.metadata.clone(),
2332            config: self.config.clone(),
2333            network: std::sync::RwLock::new(self.network.read().unwrap().clone()),
2334            next_flow_id: std::sync::atomic::AtomicU64::new(
2335                self.next_flow_id.load(std::sync::atomic::Ordering::SeqCst),
2336            ),
2337        }
2338    }
2339}
2340
2341impl Default for NetworkGenerationRing {
2342    fn default() -> Self {
2343        Self::new()
2344    }
2345}
2346
2347impl NetworkGenerationRing {
2348    /// Create a new Ring mode kernel.
2349    pub fn new() -> Self {
2350        Self::with_config(NetworkGenerationConfig::default())
2351    }
2352
2353    /// Create with custom configuration.
2354    pub fn with_config(config: NetworkGenerationConfig) -> Self {
2355        Self {
2356            metadata: KernelMetadata::ring(
2357                "accounting/network-generation-ring",
2358                Domain::Accounting,
2359            )
2360            .with_description("Streaming accounting network generation")
2361            .with_throughput(100_000)
2362            .with_latency_us(5.0)
2363            .with_gpu_native(true),
2364            config,
2365            network: std::sync::RwLock::new(AccountingNetwork::new()),
2366            next_flow_id: std::sync::atomic::AtomicU64::new(0),
2367        }
2368    }
2369
2370    /// Get current network statistics.
2371    pub fn stats(&self) -> NetworkGenerationStats {
2372        self.network.read().unwrap().stats.clone()
2373    }
2374
2375    /// Get total flow count.
2376    pub fn flow_count(&self) -> usize {
2377        self.network.read().unwrap().flows.len()
2378    }
2379
2380    /// Get total account count.
2381    pub fn account_count(&self) -> usize {
2382        self.network.read().unwrap().accounts.len()
2383    }
2384
2385    /// Add a journal entry and return generated flows.
2386    pub fn add_entry(&self, entry: &JournalEntry) -> Vec<AccountingFlow> {
2387        let batch_kernel = NetworkGeneration::with_config(self.config.clone());
2388        let mut flow_id = self
2389            .next_flow_id
2390            .fetch_add(100, std::sync::atomic::Ordering::SeqCst);
2391
2392        let result = batch_kernel.process_entry(entry, &mut flow_id);
2393
2394        // Update internal state
2395        {
2396            let mut network = self.network.write().unwrap();
2397            for flow in &result.flows {
2398                network.add_flow(flow.clone());
2399            }
2400
2401            // Update stats
2402            network.stats.total_entries += 1;
2403            match result.method {
2404                SolvingMethod::MethodA => network.stats.method_a_count += 1,
2405                SolvingMethod::MethodB => network.stats.method_b_count += 1,
2406                SolvingMethod::MethodC => network.stats.method_c_count += 1,
2407                SolvingMethod::MethodD => network.stats.method_d_count += 1,
2408                SolvingMethod::MethodE => network.stats.method_e_count += 1,
2409                SolvingMethod::Unsolvable => network.stats.unsolvable_count += 1,
2410            }
2411            network.stats.total_flows = network.flows.len();
2412            network.stats.total_volume = network.total_volume();
2413            network.stats.weighted_confidence = network.weighted_confidence();
2414        }
2415
2416        result.flows
2417    }
2418
2419    /// Query flows for an account within a time window.
2420    pub fn query_flows(
2421        &self,
2422        account: &str,
2423        start_time: u64,
2424        end_time: u64,
2425        limit: usize,
2426    ) -> Vec<AccountingFlow> {
2427        let network = self.network.read().unwrap();
2428        let mut flows: Vec<_> = network
2429            .flows
2430            .iter()
2431            .filter(|f| {
2432                (f.from_account == account || f.to_account == account)
2433                    && f.timestamp >= start_time
2434                    && f.timestamp <= end_time
2435            })
2436            .cloned()
2437            .collect();
2438
2439        flows.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
2440        flows.truncate(limit);
2441        flows
2442    }
2443
2444    /// Clear the network state.
2445    pub fn clear(&self) {
2446        let mut network = self.network.write().unwrap();
2447        *network = AccountingNetwork::new();
2448        self.next_flow_id
2449            .store(0, std::sync::atomic::Ordering::SeqCst);
2450    }
2451}
2452
2453impl GpuKernel for NetworkGenerationRing {
2454    fn metadata(&self) -> &KernelMetadata {
2455        &self.metadata
2456    }
2457}
2458
2459// ============================================================================
2460// Tests
2461// ============================================================================
2462
2463#[cfg(test)]
2464mod tests {
2465    use super::*;
2466    use crate::types::JournalStatus;
2467
2468    fn create_simple_entry() -> JournalEntry {
2469        JournalEntry {
2470            id: 1,
2471            date: 1700000000,
2472            posting_date: 1700000000,
2473            document_number: "JE001".to_string(),
2474            lines: vec![
2475                JournalLine {
2476                    line_number: 1,
2477                    account_code: "1000".to_string(), // Cash
2478                    debit: 1000.0,
2479                    credit: 0.0,
2480                    currency: "USD".to_string(),
2481                    entity_id: "CORP".to_string(),
2482                    cost_center: None,
2483                    description: "Cash debit".to_string(),
2484                },
2485                JournalLine {
2486                    line_number: 2,
2487                    account_code: "4000".to_string(), // Revenue
2488                    debit: 0.0,
2489                    credit: 1000.0,
2490                    currency: "USD".to_string(),
2491                    entity_id: "CORP".to_string(),
2492                    cost_center: None,
2493                    description: "Revenue credit".to_string(),
2494                },
2495            ],
2496            status: JournalStatus::Posted,
2497            source_system: "TEST".to_string(),
2498            description: "Simple sale".to_string(),
2499        }
2500    }
2501
2502    fn create_multi_line_entry() -> JournalEntry {
2503        JournalEntry {
2504            id: 2,
2505            date: 1700000000,
2506            posting_date: 1700000000,
2507            document_number: "JE002".to_string(),
2508            lines: vec![
2509                JournalLine {
2510                    line_number: 1,
2511                    account_code: "1000".to_string(),
2512                    debit: 500.0,
2513                    credit: 0.0,
2514                    currency: "USD".to_string(),
2515                    entity_id: "CORP".to_string(),
2516                    cost_center: None,
2517                    description: "Cash 1".to_string(),
2518                },
2519                JournalLine {
2520                    line_number: 2,
2521                    account_code: "1100".to_string(),
2522                    debit: 300.0,
2523                    credit: 0.0,
2524                    currency: "USD".to_string(),
2525                    entity_id: "CORP".to_string(),
2526                    cost_center: None,
2527                    description: "AR".to_string(),
2528                },
2529                JournalLine {
2530                    line_number: 3,
2531                    account_code: "4000".to_string(),
2532                    debit: 0.0,
2533                    credit: 500.0,
2534                    currency: "USD".to_string(),
2535                    entity_id: "CORP".to_string(),
2536                    cost_center: None,
2537                    description: "Revenue 1".to_string(),
2538                },
2539                JournalLine {
2540                    line_number: 4,
2541                    account_code: "4100".to_string(),
2542                    debit: 0.0,
2543                    credit: 300.0,
2544                    currency: "USD".to_string(),
2545                    entity_id: "CORP".to_string(),
2546                    cost_center: None,
2547                    description: "Revenue 2".to_string(),
2548                },
2549            ],
2550            status: JournalStatus::Posted,
2551            source_system: "TEST".to_string(),
2552            description: "Multi-line sale".to_string(),
2553        }
2554    }
2555
2556    fn create_asymmetric_entry() -> JournalEntry {
2557        JournalEntry {
2558            id: 3,
2559            date: 1700000000,
2560            posting_date: 1700000000,
2561            document_number: "JE003".to_string(),
2562            lines: vec![
2563                JournalLine {
2564                    line_number: 1,
2565                    account_code: "1000".to_string(),
2566                    debit: 1000.0,
2567                    credit: 0.0,
2568                    currency: "USD".to_string(),
2569                    entity_id: "CORP".to_string(),
2570                    cost_center: None,
2571                    description: "Cash".to_string(),
2572                },
2573                JournalLine {
2574                    line_number: 2,
2575                    account_code: "4000".to_string(),
2576                    debit: 0.0,
2577                    credit: 600.0,
2578                    currency: "USD".to_string(),
2579                    entity_id: "CORP".to_string(),
2580                    cost_center: None,
2581                    description: "Revenue".to_string(),
2582                },
2583                JournalLine {
2584                    line_number: 3,
2585                    account_code: "4100".to_string(),
2586                    debit: 0.0,
2587                    credit: 400.0,
2588                    currency: "USD".to_string(),
2589                    entity_id: "CORP".to_string(),
2590                    cost_center: None,
2591                    description: "Service revenue".to_string(),
2592                },
2593            ],
2594            status: JournalStatus::Posted,
2595            source_system: "TEST".to_string(),
2596            description: "Asymmetric entry".to_string(),
2597        }
2598    }
2599
2600    #[test]
2601    fn test_fixed_point_arithmetic() {
2602        let a = FixedPoint128::from_f64(100.50);
2603        let b = FixedPoint128::from_f64(50.25);
2604
2605        let sum = a + b;
2606        assert!((sum.to_f64() - 150.75).abs() < 0.0001);
2607
2608        let diff = a - b;
2609        assert!((diff.to_f64() - 50.25).abs() < 0.0001);
2610
2611        assert!(a > b);
2612        assert_eq!(
2613            FixedPoint128::from_f64(100.0).abs(),
2614            FixedPoint128::from_f64(100.0)
2615        );
2616        assert_eq!(
2617            FixedPoint128::from_f64(-100.0).abs(),
2618            FixedPoint128::from_f64(100.0)
2619        );
2620    }
2621
2622    #[test]
2623    fn test_method_a_simple_entry() {
2624        let kernel = NetworkGeneration::new();
2625        let entry = create_simple_entry();
2626        let network = kernel.generate(&[entry]);
2627
2628        assert_eq!(network.flows.len(), 1);
2629        assert_eq!(network.stats.method_a_count, 1);
2630        assert_eq!(network.stats.method_b_count, 0);
2631
2632        let flow = &network.flows[0];
2633        assert_eq!(flow.from_account, "1000");
2634        assert_eq!(flow.to_account, "4000");
2635        assert!((flow.amount_f64 - 1000.0).abs() < 0.01);
2636        assert_eq!(flow.method, SolvingMethod::MethodA);
2637        assert!((flow.confidence - 1.0).abs() < 0.001);
2638    }
2639
2640    #[test]
2641    fn test_method_b_multi_line() {
2642        let kernel = NetworkGeneration::new();
2643        let entry = create_multi_line_entry();
2644        let network = kernel.generate(&[entry]);
2645
2646        assert_eq!(network.flows.len(), 2);
2647        assert_eq!(network.stats.method_b_count, 1);
2648
2649        // Check that amounts match correctly
2650        let total_flow: f64 = network.flows.iter().map(|f| f.amount_f64).sum();
2651        assert!((total_flow - 800.0).abs() < 0.01);
2652    }
2653
2654    #[test]
2655    fn test_method_c_asymmetric() {
2656        let kernel = NetworkGeneration::new();
2657        let entry = create_asymmetric_entry();
2658        let network = kernel.generate(&[entry]);
2659
2660        // 1 debit to 2 credits = Method C
2661        assert!(network.flows.len() >= 2);
2662        assert_eq!(network.stats.method_c_count, 1);
2663
2664        // Total flow should equal the debit amount
2665        let total_flow: f64 = network.flows.iter().map(|f| f.amount_f64).sum();
2666        assert!((total_flow - 1000.0).abs() < 0.01);
2667    }
2668
2669    #[test]
2670    fn test_network_statistics() {
2671        let kernel = NetworkGeneration::new();
2672        let entries = vec![
2673            create_simple_entry(),
2674            create_multi_line_entry(),
2675            create_asymmetric_entry(),
2676        ];
2677
2678        let network = kernel.generate(&entries);
2679
2680        assert_eq!(network.stats.total_entries, 3);
2681        assert!(network.stats.total_flows >= 5);
2682        assert!(network.stats.total_volume > 2000.0);
2683        assert!(network.stats.weighted_confidence > 0.5);
2684    }
2685
2686    #[test]
2687    fn test_temporal_query() {
2688        let kernel = NetworkGeneration::new();
2689        let mut entry1 = create_simple_entry();
2690        entry1.posting_date = 1000;
2691        entry1.id = 1;
2692
2693        let mut entry2 = create_simple_entry();
2694        entry2.posting_date = 2000;
2695        entry2.id = 2;
2696
2697        let mut entry3 = create_simple_entry();
2698        entry3.posting_date = 3000;
2699        entry3.id = 3;
2700
2701        let network = kernel.generate(&[entry1, entry2, entry3]);
2702
2703        // Query middle time window
2704        let flows = network.query_temporal(1500, 2500);
2705        assert_eq!(flows.len(), 1);
2706        assert_eq!(flows[0].entry_id, 2);
2707    }
2708
2709    #[test]
2710    fn test_adjacency_list() {
2711        let kernel = NetworkGeneration::new();
2712        let entry = create_simple_entry();
2713        let network = kernel.generate(&[entry]);
2714
2715        let outgoing = network.outgoing_flows("1000");
2716        assert_eq!(outgoing.len(), 1);
2717        assert_eq!(outgoing[0].to_account, "4000");
2718
2719        let incoming = network.incoming_flows("4000");
2720        assert_eq!(incoming.len(), 1);
2721        assert_eq!(incoming[0].from_account, "1000");
2722    }
2723
2724    #[test]
2725    fn test_ring_mode() {
2726        let ring_kernel = NetworkGenerationRing::new();
2727
2728        // Add entries one by one
2729        let entry1 = create_simple_entry();
2730        let flows1 = ring_kernel.add_entry(&entry1);
2731        assert_eq!(flows1.len(), 1);
2732
2733        let entry2 = create_multi_line_entry();
2734        let flows2 = ring_kernel.add_entry(&entry2);
2735        assert!(flows2.len() >= 2);
2736
2737        // Check accumulated stats
2738        let stats = ring_kernel.stats();
2739        assert_eq!(stats.total_entries, 2);
2740        assert!(stats.total_flows >= 3);
2741    }
2742
2743    #[test]
2744    fn test_ring_query() {
2745        let ring_kernel = NetworkGenerationRing::new();
2746
2747        let entry = create_simple_entry();
2748        ring_kernel.add_entry(&entry);
2749
2750        let flows = ring_kernel.query_flows("1000", 0, u64::MAX, 100);
2751        assert_eq!(flows.len(), 1);
2752        assert_eq!(flows[0].from_account, "1000");
2753    }
2754
2755    #[test]
2756    fn test_solving_method_confidence() {
2757        assert_eq!(SolvingMethod::MethodA.confidence(), 1.00);
2758        assert_eq!(SolvingMethod::MethodB.confidence(), 0.95);
2759        assert_eq!(SolvingMethod::MethodC.confidence(), 0.85);
2760        assert_eq!(SolvingMethod::MethodD.confidence(), 0.70);
2761        assert_eq!(SolvingMethod::MethodE.confidence(), 0.50);
2762        assert_eq!(SolvingMethod::Unsolvable.confidence(), 0.00);
2763    }
2764
2765    #[test]
2766    fn test_unbalanced_entry() {
2767        let kernel = NetworkGeneration::with_config(NetworkGenerationConfig {
2768            strict_balance: true,
2769            ..Default::default()
2770        });
2771
2772        let mut entry = create_simple_entry();
2773        entry.lines[0].debit = 1500.0; // Unbalanced
2774
2775        let network = kernel.generate(&[entry]);
2776
2777        assert!(network.flows.is_empty());
2778        assert_eq!(network.stats.balance_errors, 1);
2779    }
2780
2781    #[test]
2782    fn test_suspense_routing() {
2783        // Create a very complex entry that forces suspense routing
2784        let kernel = NetworkGeneration::with_config(NetworkGenerationConfig {
2785            max_lines_method_b: 2,
2786            max_lines_method_c: 4,
2787            enable_aggregation: false,
2788            enable_decomposition: false,
2789            suspense_account: "SUSPENSE_ACCT".to_string(),
2790            ..Default::default()
2791        });
2792
2793        // Create an entry that can't be solved
2794        let entry = JournalEntry {
2795            id: 99,
2796            date: 1700000000,
2797            posting_date: 1700000000,
2798            document_number: "JE099".to_string(),
2799            lines: vec![
2800                JournalLine {
2801                    line_number: 1,
2802                    account_code: "1000".to_string(),
2803                    debit: 100.0,
2804                    credit: 0.0,
2805                    currency: "USD".to_string(),
2806                    entity_id: "A".to_string(),
2807                    cost_center: None,
2808                    description: "D1".to_string(),
2809                },
2810                JournalLine {
2811                    line_number: 2,
2812                    account_code: "1001".to_string(),
2813                    debit: 100.0,
2814                    credit: 0.0,
2815                    currency: "USD".to_string(),
2816                    entity_id: "A".to_string(),
2817                    cost_center: None,
2818                    description: "D2".to_string(),
2819                },
2820                JournalLine {
2821                    line_number: 3,
2822                    account_code: "1002".to_string(),
2823                    debit: 100.0,
2824                    credit: 0.0,
2825                    currency: "USD".to_string(),
2826                    entity_id: "A".to_string(),
2827                    cost_center: None,
2828                    description: "D3".to_string(),
2829                },
2830                JournalLine {
2831                    line_number: 4,
2832                    account_code: "2000".to_string(),
2833                    debit: 0.0,
2834                    credit: 75.0,
2835                    currency: "USD".to_string(),
2836                    entity_id: "A".to_string(),
2837                    cost_center: None,
2838                    description: "C1".to_string(),
2839                },
2840                JournalLine {
2841                    line_number: 5,
2842                    account_code: "2001".to_string(),
2843                    debit: 0.0,
2844                    credit: 75.0,
2845                    currency: "USD".to_string(),
2846                    entity_id: "A".to_string(),
2847                    cost_center: None,
2848                    description: "C2".to_string(),
2849                },
2850                JournalLine {
2851                    line_number: 6,
2852                    account_code: "2002".to_string(),
2853                    debit: 0.0,
2854                    credit: 75.0,
2855                    currency: "USD".to_string(),
2856                    entity_id: "A".to_string(),
2857                    cost_center: None,
2858                    description: "C3".to_string(),
2859                },
2860                JournalLine {
2861                    line_number: 7,
2862                    account_code: "2003".to_string(),
2863                    debit: 0.0,
2864                    credit: 75.0,
2865                    currency: "USD".to_string(),
2866                    entity_id: "A".to_string(),
2867                    cost_center: None,
2868                    description: "C4".to_string(),
2869                },
2870            ],
2871            status: JournalStatus::Posted,
2872            source_system: "TEST".to_string(),
2873            description: "Complex entry".to_string(),
2874        };
2875
2876        let network = kernel.generate(&[entry]);
2877
2878        assert_eq!(network.stats.unsolvable_count, 1);
2879        // Should have flows to/from suspense account
2880        assert!(
2881            network
2882                .flows
2883                .iter()
2884                .any(|f| f.from_account == "SUSPENSE_ACCT" || f.to_account == "SUSPENSE_ACCT")
2885        );
2886    }
2887
2888    #[test]
2889    fn test_metadata() {
2890        let kernel = NetworkGeneration::new();
2891        assert_eq!(kernel.metadata().id, "accounting/network-generation");
2892        assert_eq!(kernel.metadata().domain, Domain::Accounting);
2893
2894        let ring_kernel = NetworkGenerationRing::new();
2895        assert_eq!(
2896            ring_kernel.metadata().id,
2897            "accounting/network-generation-ring"
2898        );
2899    }
2900
2901    #[test]
2902    fn test_weighted_confidence() {
2903        let kernel = NetworkGeneration::new();
2904        let entries = vec![
2905            create_simple_entry(),     // Method A, conf=1.0, amt=1000
2906            create_asymmetric_entry(), // Method C, conf=0.85, amt=1000
2907        ];
2908
2909        let network = kernel.generate(&entries);
2910
2911        // Weighted confidence should be between 0.85 and 1.0
2912        assert!(network.stats.weighted_confidence >= 0.85);
2913        assert!(network.stats.weighted_confidence <= 1.0);
2914    }
2915
2916    // ========================================================================
2917    // Enhanced Feature Tests
2918    // ========================================================================
2919
2920    #[test]
2921    fn test_account_classification_numeric() {
2922        // Test numeric prefix classification
2923        assert_eq!(AccountClass::from_account_code("1000"), AccountClass::Asset);
2924        assert_eq!(AccountClass::from_account_code("1500"), AccountClass::Asset);
2925        assert_eq!(
2926            AccountClass::from_account_code("2000"),
2927            AccountClass::Liability
2928        );
2929        assert_eq!(
2930            AccountClass::from_account_code("3000"),
2931            AccountClass::Equity
2932        );
2933        assert_eq!(
2934            AccountClass::from_account_code("4000"),
2935            AccountClass::Revenue
2936        );
2937        assert_eq!(AccountClass::from_account_code("5000"), AccountClass::COGS);
2938        assert_eq!(
2939            AccountClass::from_account_code("6000"),
2940            AccountClass::Expense
2941        );
2942        assert_eq!(
2943            AccountClass::from_account_code("7500"),
2944            AccountClass::Expense
2945        );
2946        assert_eq!(
2947            AccountClass::from_account_code("8000"),
2948            AccountClass::OtherIncomeExpense
2949        );
2950        assert_eq!(
2951            AccountClass::from_account_code("9000"),
2952            AccountClass::Suspense
2953        );
2954    }
2955
2956    #[test]
2957    fn test_account_classification_keywords() {
2958        // Test keyword-based classification
2959        assert_eq!(
2960            AccountClass::from_account_code("CASH_ON_HAND"),
2961            AccountClass::Asset
2962        );
2963        assert_eq!(
2964            AccountClass::from_account_code("BANK_ACCOUNT"),
2965            AccountClass::Asset
2966        );
2967        assert_eq!(
2968            AccountClass::from_account_code("ACCOUNTS_RECEIVABLE"),
2969            AccountClass::Asset
2970        );
2971        assert_eq!(
2972            AccountClass::from_account_code("ACCOUNTS_PAYABLE"),
2973            AccountClass::Liability
2974        );
2975        assert_eq!(
2976            AccountClass::from_account_code("VAT_PAYABLE"),
2977            AccountClass::Tax
2978        );
2979        assert_eq!(
2980            AccountClass::from_account_code("GST_RECEIVABLE"),
2981            AccountClass::Tax
2982        );
2983        assert_eq!(
2984            AccountClass::from_account_code("IC_DUE_FROM_SUB"),
2985            AccountClass::Intercompany
2986        );
2987        assert_eq!(
2988            AccountClass::from_account_code("SUSPENSE_CLEARING"),
2989            AccountClass::Suspense
2990        );
2991        assert_eq!(
2992            AccountClass::from_account_code("SALES_REVENUE"),
2993            AccountClass::Revenue
2994        );
2995        assert_eq!(
2996            AccountClass::from_account_code("RENT_EXPENSE"),
2997            AccountClass::Expense
2998        );
2999    }
3000
3001    #[test]
3002    fn test_vat_detection_20_percent() {
3003        let detector = VatDetector::new();
3004
3005        // Test 20% VAT detection (UK standard)
3006        let amounts = vec![
3007            FixedPoint128::from_f64(1200.0), // Gross
3008            FixedPoint128::from_f64(1000.0), // Net
3009        ];
3010
3011        let result = detector.detect_vat_split(&amounts);
3012        assert!(result.is_some());
3013        let (net, tax, rate) = result.unwrap();
3014        assert!((net.to_f64() - 1000.0).abs() < 0.01);
3015        assert!((tax.to_f64() - 200.0).abs() < 0.01);
3016        assert!((rate.rate - 0.20).abs() < 0.01);
3017    }
3018
3019    #[test]
3020    fn test_vat_detection_19_percent() {
3021        let detector = VatDetector::new();
3022
3023        // Test 19% VAT detection (Germany standard)
3024        let amounts = vec![
3025            FixedPoint128::from_f64(1190.0), // Gross
3026            FixedPoint128::from_f64(1000.0), // Net
3027        ];
3028
3029        let result = detector.detect_vat_split(&amounts);
3030        assert!(result.is_some());
3031        let (net, tax, rate) = result.unwrap();
3032        assert!((net.to_f64() - 1000.0).abs() < 0.01);
3033        assert!((tax.to_f64() - 190.0).abs() < 0.01);
3034        assert!((rate.rate - 0.19).abs() < 0.01);
3035    }
3036
3037    #[test]
3038    fn test_sale_with_vat_pattern() {
3039        let kernel = NetworkGeneration::new();
3040
3041        // Create a sale with 20% VAT: DR Cash 1200, CR Revenue 1000, CR VAT Payable 200
3042        let entry = JournalEntry {
3043            id: 100,
3044            date: 1700000000,
3045            posting_date: 1700000000,
3046            document_number: "SALE001".to_string(),
3047            lines: vec![
3048                JournalLine {
3049                    line_number: 1,
3050                    account_code: "1000".to_string(), // Cash (Asset)
3051                    debit: 1200.0,
3052                    credit: 0.0,
3053                    currency: "USD".to_string(),
3054                    entity_id: "CORP".to_string(),
3055                    cost_center: None,
3056                    description: "Cash receipt".to_string(),
3057                },
3058                JournalLine {
3059                    line_number: 2,
3060                    account_code: "4000".to_string(), // Revenue
3061                    debit: 0.0,
3062                    credit: 1000.0,
3063                    currency: "USD".to_string(),
3064                    entity_id: "CORP".to_string(),
3065                    cost_center: None,
3066                    description: "Sales revenue".to_string(),
3067                },
3068                JournalLine {
3069                    line_number: 3,
3070                    account_code: "VAT_OUTPUT".to_string(), // VAT Payable
3071                    debit: 0.0,
3072                    credit: 200.0,
3073                    currency: "USD".to_string(),
3074                    entity_id: "CORP".to_string(),
3075                    cost_center: None,
3076                    description: "Output VAT".to_string(),
3077                },
3078            ],
3079            status: JournalStatus::Posted,
3080            source_system: "TEST".to_string(),
3081            description: "Sale with VAT".to_string(),
3082        };
3083
3084        let network = kernel.generate(&[entry]);
3085
3086        // Should detect VAT pattern
3087        assert_eq!(network.stats.vat_entries_count, 1);
3088        assert!(network.stats.total_vat_amount > 0.0);
3089        assert_eq!(network.stats.sales_pattern_count, 1);
3090
3091        // Check that flows have pattern annotations
3092        assert!(
3093            network
3094                .flows
3095                .iter()
3096                .any(|f| f.pattern == Some(TransactionPattern::SaleWithVat))
3097        );
3098    }
3099
3100    #[test]
3101    fn test_purchase_with_vat_pattern() {
3102        let kernel = NetworkGeneration::new();
3103
3104        // Create a purchase with VAT: DR Expense 1000, DR VAT Receivable 200, CR Cash 1200
3105        let entry = JournalEntry {
3106            id: 101,
3107            date: 1700000000,
3108            posting_date: 1700000000,
3109            document_number: "PURCH001".to_string(),
3110            lines: vec![
3111                JournalLine {
3112                    line_number: 1,
3113                    account_code: "6000".to_string(), // Expense
3114                    debit: 1000.0,
3115                    credit: 0.0,
3116                    currency: "USD".to_string(),
3117                    entity_id: "CORP".to_string(),
3118                    cost_center: None,
3119                    description: "Office supplies".to_string(),
3120                },
3121                JournalLine {
3122                    line_number: 2,
3123                    account_code: "VAT_INPUT".to_string(), // VAT Receivable
3124                    debit: 200.0,
3125                    credit: 0.0,
3126                    currency: "USD".to_string(),
3127                    entity_id: "CORP".to_string(),
3128                    cost_center: None,
3129                    description: "Input VAT".to_string(),
3130                },
3131                JournalLine {
3132                    line_number: 3,
3133                    account_code: "1000".to_string(), // Cash
3134                    debit: 0.0,
3135                    credit: 1200.0,
3136                    currency: "USD".to_string(),
3137                    entity_id: "CORP".to_string(),
3138                    cost_center: None,
3139                    description: "Payment".to_string(),
3140                },
3141            ],
3142            status: JournalStatus::Posted,
3143            source_system: "TEST".to_string(),
3144            description: "Purchase with VAT".to_string(),
3145        };
3146
3147        let network = kernel.generate(&[entry]);
3148
3149        assert_eq!(network.stats.vat_entries_count, 1);
3150        assert_eq!(network.stats.purchase_pattern_count, 1);
3151    }
3152
3153    #[test]
3154    fn test_simple_sale_pattern() {
3155        let kernel = NetworkGeneration::new();
3156
3157        // Simple sale without VAT: DR Cash, CR Revenue
3158        let entry = create_simple_entry();
3159        let network = kernel.generate(&[entry]);
3160
3161        assert_eq!(network.stats.sales_pattern_count, 1);
3162        assert!(
3163            network
3164                .flows
3165                .iter()
3166                .any(|f| f.pattern == Some(TransactionPattern::SimpleSale))
3167        );
3168    }
3169
3170    #[test]
3171    fn test_intercompany_detection() {
3172        let kernel = NetworkGeneration::new();
3173
3174        // Intercompany transaction: different entities
3175        let entry = JournalEntry {
3176            id: 102,
3177            date: 1700000000,
3178            posting_date: 1700000000,
3179            document_number: "IC001".to_string(),
3180            lines: vec![
3181                JournalLine {
3182                    line_number: 1,
3183                    account_code: "1000".to_string(),
3184                    debit: 5000.0,
3185                    credit: 0.0,
3186                    currency: "USD".to_string(),
3187                    entity_id: "CORP_A".to_string(), // Entity A
3188                    cost_center: None,
3189                    description: "IC receivable".to_string(),
3190                },
3191                JournalLine {
3192                    line_number: 2,
3193                    account_code: "4000".to_string(),
3194                    debit: 0.0,
3195                    credit: 5000.0,
3196                    currency: "USD".to_string(),
3197                    entity_id: "CORP_B".to_string(), // Entity B - different!
3198                    cost_center: None,
3199                    description: "IC revenue".to_string(),
3200                },
3201            ],
3202            status: JournalStatus::Posted,
3203            source_system: "TEST".to_string(),
3204            description: "Intercompany sale".to_string(),
3205        };
3206
3207        let network = kernel.generate(&[entry]);
3208
3209        assert_eq!(network.stats.intercompany_count, 1);
3210        assert!(network.flows.iter().any(|f| f.is_intercompany));
3211        assert!(
3212            network
3213                .flows
3214                .iter()
3215                .any(|f| f.pattern == Some(TransactionPattern::Intercompany))
3216        );
3217    }
3218
3219    #[test]
3220    fn test_confidence_boost_from_pattern() {
3221        let kernel = NetworkGeneration::new();
3222        let entry = create_simple_entry();
3223        let network = kernel.generate(&[entry]);
3224
3225        // Simple sale pattern should boost confidence
3226        // Base confidence for Method A is 1.0, boost for SimpleSale is 0.10
3227        // Total should be capped at 1.0
3228        for flow in &network.flows {
3229            assert!(flow.confidence >= 1.0); // Should be at max
3230            assert!(
3231                flow.confidence_factors
3232                    .iter()
3233                    .any(|f| f.contains("pattern"))
3234            );
3235        }
3236    }
3237
3238    #[test]
3239    fn test_account_class_annotations() {
3240        let kernel = NetworkGeneration::new();
3241        let entry = create_simple_entry();
3242        let network = kernel.generate(&[entry]);
3243
3244        // Flows should have account class annotations
3245        for flow in &network.flows {
3246            assert!(flow.from_account_class.is_some());
3247            assert!(flow.to_account_class.is_some());
3248            assert_eq!(flow.from_account_class, Some(AccountClass::Asset)); // 1000 = Asset
3249            assert_eq!(flow.to_account_class, Some(AccountClass::Revenue)); // 4000 = Revenue
3250        }
3251    }
3252
3253    #[test]
3254    fn test_pattern_matcher() {
3255        let matcher = PatternMatcher::new();
3256
3257        // Test sale pattern detection
3258        let debits = vec![ClassifiedLine {
3259            line_number: 1,
3260            account_code: "1000".to_string(),
3261            amount: FixedPoint128::from_f64(1000.0),
3262            is_debit: true,
3263            entity_id: "CORP".to_string(),
3264            cost_center: None,
3265        }];
3266
3267        let credits = vec![ClassifiedLine {
3268            line_number: 2,
3269            account_code: "4000".to_string(),
3270            amount: FixedPoint128::from_f64(1000.0),
3271            is_debit: false,
3272            entity_id: "CORP".to_string(),
3273            cost_center: None,
3274        }];
3275
3276        let (pattern, vat) = matcher.detect_pattern(&debits, &credits);
3277        assert_eq!(pattern, TransactionPattern::SimpleSale);
3278        assert!(vat.is_none());
3279    }
3280
3281    #[test]
3282    fn test_vat_pattern_detection_with_tax_accounts() {
3283        let detector = VatDetector::new();
3284
3285        let lines = vec![
3286            ClassifiedLine {
3287                line_number: 1,
3288                account_code: "1000".to_string(),
3289                amount: FixedPoint128::from_f64(1200.0),
3290                is_debit: true,
3291                entity_id: "CORP".to_string(),
3292                cost_center: None,
3293            },
3294            ClassifiedLine {
3295                line_number: 2,
3296                account_code: "4000".to_string(),
3297                amount: FixedPoint128::from_f64(1000.0),
3298                is_debit: false,
3299                entity_id: "CORP".to_string(),
3300                cost_center: None,
3301            },
3302            ClassifiedLine {
3303                line_number: 3,
3304                account_code: "VAT_OUTPUT".to_string(),
3305                amount: FixedPoint128::from_f64(200.0),
3306                is_debit: false,
3307                entity_id: "CORP".to_string(),
3308                cost_center: None,
3309            },
3310        ];
3311
3312        let result = detector.detect_vat_pattern(&lines);
3313        assert!(result.is_some());
3314        let vat = result.unwrap();
3315        assert!(vat.is_output_vat);
3316        assert!((vat.rate.rate - 0.20).abs() < 0.01);
3317        assert!(vat.tax_accounts.contains(&"VAT_OUTPUT".to_string()));
3318    }
3319
3320    #[test]
3321    fn test_custom_vat_rate() {
3322        let config = NetworkGenerationConfig {
3323            custom_vat_rates: vec![0.15], // Custom 15% rate
3324            ..Default::default()
3325        };
3326        let kernel = NetworkGeneration::with_config(config);
3327
3328        // Create a transaction with 15% VAT
3329        let entry = JournalEntry {
3330            id: 103,
3331            date: 1700000000,
3332            posting_date: 1700000000,
3333            document_number: "VAT15".to_string(),
3334            lines: vec![
3335                JournalLine {
3336                    line_number: 1,
3337                    account_code: "1000".to_string(),
3338                    debit: 1150.0, // Gross with 15% VAT
3339                    credit: 0.0,
3340                    currency: "USD".to_string(),
3341                    entity_id: "CORP".to_string(),
3342                    cost_center: None,
3343                    description: "Cash".to_string(),
3344                },
3345                JournalLine {
3346                    line_number: 2,
3347                    account_code: "4000".to_string(),
3348                    debit: 0.0,
3349                    credit: 1000.0,
3350                    currency: "USD".to_string(),
3351                    entity_id: "CORP".to_string(),
3352                    cost_center: None,
3353                    description: "Revenue".to_string(),
3354                },
3355                JournalLine {
3356                    line_number: 3,
3357                    account_code: "VAT_OUTPUT".to_string(),
3358                    debit: 0.0,
3359                    credit: 150.0,
3360                    currency: "USD".to_string(),
3361                    entity_id: "CORP".to_string(),
3362                    cost_center: None,
3363                    description: "VAT".to_string(),
3364                },
3365            ],
3366            status: JournalStatus::Posted,
3367            source_system: "TEST".to_string(),
3368            description: "Sale with 15% VAT".to_string(),
3369        };
3370
3371        let network = kernel.generate(&[entry]);
3372        assert_eq!(network.stats.vat_entries_count, 1);
3373    }
3374
3375    #[test]
3376    fn test_disable_pattern_matching() {
3377        let config = NetworkGenerationConfig {
3378            enable_pattern_matching: false,
3379            ..Default::default()
3380        };
3381        let kernel = NetworkGeneration::with_config(config);
3382        let entry = create_simple_entry();
3383        let network = kernel.generate(&[entry]);
3384
3385        // Patterns should be Unknown when disabled
3386        for flow in &network.flows {
3387            assert!(flow.pattern.is_none());
3388        }
3389    }
3390
3391    #[test]
3392    fn test_enhanced_stats() {
3393        let kernel = NetworkGeneration::new();
3394
3395        let entries = vec![
3396            create_simple_entry(), // Simple sale
3397        ];
3398
3399        let network = kernel.generate(&entries);
3400
3401        // Check enhanced stats fields
3402        assert_eq!(network.stats.sales_pattern_count, 1);
3403        assert_eq!(network.stats.purchase_pattern_count, 0);
3404        assert_eq!(network.stats.payment_pattern_count, 0);
3405        assert_eq!(network.stats.payroll_pattern_count, 0);
3406        assert_eq!(network.stats.intercompany_count, 0);
3407    }
3408}