Skip to main content

qualia_core_db/domains/financial/
tax_schema.rs

1/// Defines modular tax and jurisdictional rulesets to be loaded into the Webizen VM.
2/// These rules are applied to the immutable Quins to compute dynamic, mutable
3/// tax liabilities without polluting the underlying graph.
4
5pub struct TaxRuleSchema {
6    pub jurisdiction_id: String,
7    pub description: String,
8    pub rules: Vec<TaxRule>,
9}
10
11pub struct TaxRule {
12    pub match_category: String,
13    pub calculation_fn: fn(f64) -> f64,
14}
15
16impl TaxRuleSchema {
17    /// Mock AU GST schema (10% GST on income, 10% credit on expenses)
18    pub fn new_au_gst() -> Self {
19        TaxRuleSchema {
20            jurisdiction_id: "AU_GST_2026".to_string(),
21            description: "Australian Goods and Services Tax (10%)".to_string(),
22            rules: vec![
23                TaxRule {
24                    match_category: "Income".to_string(),
25                    calculation_fn: |amount| amount * 0.10, // 10% GST Owed
26                },
27                TaxRule {
28                    match_category: "Expense".to_string(),
29                    calculation_fn: |amount| amount * -0.10, // 10% GST Credit
30                },
31            ],
32        }
33    }
34
35    /// EU VAT (standard 20%): 20% owed on income, 20% creditable on expenses.
36    pub fn new_eu_vat() -> Self {
37        TaxRuleSchema {
38            jurisdiction_id: "EU_VAT_2026".to_string(),
39            description: "EU Value Added Tax (standard 20%)".to_string(),
40            rules: vec![
41                TaxRule {
42                    match_category: "Income".to_string(),
43                    calculation_fn: |amount| amount * 0.20,
44                },
45                TaxRule {
46                    match_category: "Expense".to_string(),
47                    calculation_fn: |amount| amount * -0.20,
48                },
49            ],
50        }
51    }
52
53    /// US combined sales tax (illustrative ~7% on sales; expenses are not creditable).
54    pub fn new_us_sales_tax() -> Self {
55        TaxRuleSchema {
56            jurisdiction_id: "US_SALES_2026".to_string(),
57            description: "US combined sales tax (~7%)".to_string(),
58            rules: vec![TaxRule {
59                match_category: "Income".to_string(),
60                calculation_fn: |amount| amount * 0.07,
61            }],
62        }
63    }
64
65    /// Zero-rated / exempt jurisdiction — no liability on any category.
66    pub fn new_zero_rated() -> Self {
67        TaxRuleSchema {
68            jurisdiction_id: "ZERO_RATED".to_string(),
69            description: "Zero-rated / exempt".to_string(),
70            rules: Vec::new(),
71        }
72    }
73
74    /// Evaluates a given amount and category against the active ruleset
75    pub fn evaluate(&self, category: &str, amount: f64) -> f64 {
76        for rule in &self.rules {
77            if rule.match_category == category {
78                return (rule.calculation_fn)(amount);
79            }
80        }
81        0.0
82    }
83}
84
85/// A single transaction line to clear — typically projected from a transaction Quin
86/// (`jurisdiction`, `category`, `amount`), so clearing happens "at the nquin level"
87/// without mutating the immutable graph.
88pub struct TaxLineItem<'a> {
89    pub jurisdiction_id: &'a str,
90    pub category: &'a str,
91    pub amount: f64,
92}
93
94/// Net cleared liability for one jurisdiction.
95pub struct JurisdictionLiability {
96    pub jurisdiction_id: String,
97    pub liability: f64,
98}
99
100/// The result of clearing a batch across jurisdictions: per-jurisdiction net
101/// liabilities plus the grand net.
102pub struct ClearingResult {
103    pub per_jurisdiction: Vec<JurisdictionLiability>,
104    pub net_liability: f64,
105}
106
107/// Multi-jurisdiction "Information Banking" tax clearing house.
108///
109/// Holds the active regional schemas and clears transaction line items to
110/// per-jurisdiction net liabilities, applying the **correct regional schema per item**
111/// — the jurisdiction-aware clearing the resilience-economics scope calls for. It
112/// derives mutable liabilities over immutable transaction quins without polluting the
113/// graph. Cold-path config (heap, consistent with [`TaxRuleSchema`]).
114pub struct TaxClearingHouse {
115    schemas: Vec<TaxRuleSchema>,
116}
117
118impl TaxClearingHouse {
119    pub fn new() -> Self {
120        Self {
121            schemas: Vec::new(),
122        }
123    }
124
125    /// Register a jurisdiction schema (builder style).
126    pub fn with_schema(mut self, schema: TaxRuleSchema) -> Self {
127        self.schemas.push(schema);
128        self
129    }
130
131    /// A clearing house pre-loaded with the standard AU-GST / EU-VAT / US-sales /
132    /// zero-rated schemas.
133    pub fn with_standard_schemas() -> Self {
134        Self::new()
135            .with_schema(TaxRuleSchema::new_au_gst())
136            .with_schema(TaxRuleSchema::new_eu_vat())
137            .with_schema(TaxRuleSchema::new_us_sales_tax())
138            .with_schema(TaxRuleSchema::new_zero_rated())
139    }
140
141    fn schema_for(&self, jurisdiction_id: &str) -> Option<&TaxRuleSchema> {
142        self.schemas
143            .iter()
144            .find(|s| s.jurisdiction_id == jurisdiction_id)
145    }
146
147    /// Clear one line item: apply the matching jurisdiction's schema (0 if the
148    /// jurisdiction is unknown — fail-safe, never guesses a foreign rate).
149    pub fn clear_item(&self, jurisdiction_id: &str, category: &str, amount: f64) -> f64 {
150        self.schema_for(jurisdiction_id)
151            .map_or(0.0, |s| s.evaluate(category, amount))
152    }
153
154    /// Clear a batch of line items into per-jurisdiction net liabilities + grand net.
155    pub fn clear_batch(&self, items: &[TaxLineItem]) -> ClearingResult {
156        let mut per: Vec<JurisdictionLiability> = Vec::new();
157        let mut net = 0.0;
158        for item in items {
159            let liability = self.clear_item(item.jurisdiction_id, item.category, item.amount);
160            net += liability;
161            if let Some(j) = per
162                .iter_mut()
163                .find(|j| j.jurisdiction_id == item.jurisdiction_id)
164            {
165                j.liability += liability;
166            } else {
167                per.push(JurisdictionLiability {
168                    jurisdiction_id: item.jurisdiction_id.to_string(),
169                    liability,
170                });
171            }
172        }
173        ClearingResult {
174            per_jurisdiction: per,
175            net_liability: net,
176        }
177    }
178}
179
180impl Default for TaxClearingHouse {
181    fn default() -> Self {
182        Self::new()
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    #[test]
191    fn per_jurisdiction_rates_apply() {
192        let house = TaxClearingHouse::with_standard_schemas();
193        assert!((house.clear_item("AU_GST_2026", "Income", 1000.0) - 100.0).abs() < 1e-9);
194        assert!((house.clear_item("EU_VAT_2026", "Income", 1000.0) - 200.0).abs() < 1e-9);
195        assert!((house.clear_item("US_SALES_2026", "Income", 1000.0) - 70.0).abs() < 1e-9);
196        assert_eq!(house.clear_item("ZERO_RATED", "Income", 1000.0), 0.0);
197        // Unknown jurisdiction → 0 (fail-safe, never invents a rate).
198        assert_eq!(house.clear_item("XX_UNKNOWN", "Income", 1000.0), 0.0);
199    }
200
201    #[test]
202    fn batch_clears_net_per_jurisdiction() {
203        let house = TaxClearingHouse::with_standard_schemas();
204        let items = [
205            TaxLineItem {
206                jurisdiction_id: "AU_GST_2026",
207                category: "Income",
208                amount: 1000.0,
209            }, // +100
210            TaxLineItem {
211                jurisdiction_id: "AU_GST_2026",
212                category: "Expense",
213                amount: 400.0,
214            }, // −40
215            TaxLineItem {
216                jurisdiction_id: "EU_VAT_2026",
217                category: "Income",
218                amount: 500.0,
219            }, // +100
220        ];
221        let result = house.clear_batch(&items);
222        // Net = 100 − 40 + 100 = 160.
223        assert!(
224            (result.net_liability - 160.0).abs() < 1e-9,
225            "net {}",
226            result.net_liability
227        );
228        // Two distinct jurisdictions cleared.
229        assert_eq!(result.per_jurisdiction.len(), 2);
230        let au = result
231            .per_jurisdiction
232            .iter()
233            .find(|j| j.jurisdiction_id == "AU_GST_2026")
234            .unwrap();
235        assert!(
236            (au.liability - 60.0).abs() < 1e-9,
237            "AU net {}",
238            au.liability
239        );
240        let eu = result
241            .per_jurisdiction
242            .iter()
243            .find(|j| j.jurisdiction_id == "EU_VAT_2026")
244            .unwrap();
245        assert!(
246            (eu.liability - 100.0).abs() < 1e-9,
247            "EU net {}",
248            eu.liability
249        );
250    }
251}