qualia_core_db/domains/financial/
tax_schema.rs1pub 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 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, },
27 TaxRule {
28 match_category: "Expense".to_string(),
29 calculation_fn: |amount| amount * -0.10, },
31 ],
32 }
33 }
34
35 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 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 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 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
85pub struct TaxLineItem<'a> {
89 pub jurisdiction_id: &'a str,
90 pub category: &'a str,
91 pub amount: f64,
92}
93
94pub struct JurisdictionLiability {
96 pub jurisdiction_id: String,
97 pub liability: f64,
98}
99
100pub struct ClearingResult {
103 pub per_jurisdiction: Vec<JurisdictionLiability>,
104 pub net_liability: f64,
105}
106
107pub 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 pub fn with_schema(mut self, schema: TaxRuleSchema) -> Self {
127 self.schemas.push(schema);
128 self
129 }
130
131 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 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 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 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 }, TaxLineItem {
211 jurisdiction_id: "AU_GST_2026",
212 category: "Expense",
213 amount: 400.0,
214 }, TaxLineItem {
216 jurisdiction_id: "EU_VAT_2026",
217 category: "Income",
218 amount: 500.0,
219 }, ];
221 let result = house.clear_batch(&items);
222 assert!(
224 (result.net_liability - 160.0).abs() < 1e-9,
225 "net {}",
226 result.net_liability
227 );
228 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}