Skip to main content

datasynth_eval/coherence/
country_packs.rs

1//! Country pack coherence evaluator.
2//!
3//! Validates country pack configuration data including tax rate ranges,
4//! approval level ordering, holiday multiplier ranges, IBAN lengths,
5//! and fiscal year configuration.
6
7use crate::error::EvalResult;
8use serde::{Deserialize, Serialize};
9
10/// Thresholds for country pack evaluation.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct CountryPackThresholds {
13    /// Minimum fraction of valid tax rates.
14    pub min_rate_validity: f64,
15    /// Minimum fraction of valid format/config fields.
16    pub min_format_validity: f64,
17}
18
19impl Default for CountryPackThresholds {
20    fn default() -> Self {
21        Self {
22            min_rate_validity: 0.99,
23            min_format_validity: 0.99,
24        }
25    }
26}
27
28/// Tax rate data for range validation.
29#[derive(Debug, Clone)]
30pub struct TaxRateData {
31    /// Rate name/description.
32    pub rate_name: String,
33    /// Tax rate value.
34    pub rate: f64,
35}
36
37/// Approval level data for ordering validation.
38#[derive(Debug, Clone)]
39pub struct ApprovalLevelData {
40    /// Approval level number.
41    pub level: u32,
42    /// Threshold amount for this level.
43    pub threshold: f64,
44}
45
46/// Holiday data for multiplier validation.
47#[derive(Debug, Clone)]
48pub struct HolidayData {
49    /// Holiday name.
50    pub name: String,
51    /// Activity multiplier (0.0 = no activity, 1.0 = normal activity).
52    pub activity_multiplier: f64,
53}
54
55/// Country pack data combining all validation inputs.
56#[derive(Debug, Clone)]
57pub struct CountryPackData {
58    /// ISO country code.
59    pub country_code: String,
60    /// Tax rates defined for this country.
61    pub tax_rates: Vec<TaxRateData>,
62    /// Approval levels defined for this country.
63    pub approval_levels: Vec<ApprovalLevelData>,
64    /// Holidays defined for this country.
65    pub holidays: Vec<HolidayData>,
66    /// IBAN length (if applicable).
67    pub iban_length: Option<u32>,
68    /// Fiscal year start month (1-12).
69    pub fiscal_year_start_month: Option<u32>,
70}
71
72/// Results of country pack coherence evaluation.
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct CountryPackEvaluation {
75    /// Fraction of tax rates in [0.0, 1.0].
76    pub tax_rate_validity: f64,
77    /// Fraction of country packs with correctly ordered approval levels.
78    pub approval_order_validity: f64,
79    /// Fraction of holiday multipliers in [0.0, 1.0].
80    pub holiday_multiplier_validity: f64,
81    /// Fraction of IBAN lengths in valid range [15, 34].
82    pub iban_length_validity: f64,
83    /// Fraction of fiscal year months in [1, 12].
84    pub fiscal_year_validity: f64,
85    /// Total country packs evaluated.
86    pub total_packs: usize,
87    /// Total tax rates evaluated.
88    pub total_tax_rates: usize,
89    /// Total holidays evaluated.
90    pub total_holidays: usize,
91    /// Overall pass/fail.
92    pub passes: bool,
93    /// Issues found.
94    pub issues: Vec<String>,
95}
96
97/// Evaluator for country pack coherence.
98pub struct CountryPackEvaluator {
99    thresholds: CountryPackThresholds,
100}
101
102impl CountryPackEvaluator {
103    /// Create a new evaluator with default thresholds.
104    pub fn new() -> Self {
105        Self {
106            thresholds: CountryPackThresholds::default(),
107        }
108    }
109
110    /// Create with custom thresholds.
111    pub fn with_thresholds(thresholds: CountryPackThresholds) -> Self {
112        Self { thresholds }
113    }
114
115    /// Evaluate country pack data coherence.
116    pub fn evaluate(&self, packs: &[CountryPackData]) -> EvalResult<CountryPackEvaluation> {
117        let mut issues = Vec::new();
118
119        // 1. Tax rates in [0.0, 1.0]
120        let all_rates: Vec<&TaxRateData> = packs.iter().flat_map(|p| p.tax_rates.iter()).collect();
121        let rate_ok = all_rates
122            .iter()
123            .filter(|r| (0.0..=1.0).contains(&r.rate))
124            .count();
125        let tax_rate_validity = if all_rates.is_empty() {
126            1.0
127        } else {
128            rate_ok as f64 / all_rates.len() as f64
129        };
130
131        // 2. Approval levels in ascending threshold order
132        let order_ok = packs
133            .iter()
134            .filter(|p| {
135                if p.approval_levels.len() <= 1 {
136                    return true;
137                }
138                let mut sorted = p.approval_levels.clone();
139                sorted.sort_by_key(|a| a.level);
140                sorted.windows(2).all(|w| w[0].threshold <= w[1].threshold)
141            })
142            .count();
143        let approval_order_validity = if packs.is_empty() {
144            1.0
145        } else {
146            order_ok as f64 / packs.len() as f64
147        };
148
149        // 3. Holiday multipliers in [0.0, 1.0]
150        let all_holidays: Vec<&HolidayData> =
151            packs.iter().flat_map(|p| p.holidays.iter()).collect();
152        let holiday_ok = all_holidays
153            .iter()
154            .filter(|h| (0.0..=1.0).contains(&h.activity_multiplier))
155            .count();
156        let holiday_multiplier_validity = if all_holidays.is_empty() {
157            1.0
158        } else {
159            holiday_ok as f64 / all_holidays.len() as f64
160        };
161
162        // 4. IBAN length in [15, 34]
163        let ibans: Vec<u32> = packs.iter().filter_map(|p| p.iban_length).collect();
164        let iban_ok = ibans.iter().filter(|&&l| (15..=34).contains(&l)).count();
165        let iban_length_validity = if ibans.is_empty() {
166            1.0
167        } else {
168            iban_ok as f64 / ibans.len() as f64
169        };
170
171        // 5. Fiscal year start month in [1, 12]
172        let fy_months: Vec<u32> = packs
173            .iter()
174            .filter_map(|p| p.fiscal_year_start_month)
175            .collect();
176        let fy_ok = fy_months.iter().filter(|&&m| (1..=12).contains(&m)).count();
177        let fiscal_year_validity = if fy_months.is_empty() {
178            1.0
179        } else {
180            fy_ok as f64 / fy_months.len() as f64
181        };
182
183        // Check thresholds
184        if tax_rate_validity < self.thresholds.min_rate_validity {
185            issues.push(format!(
186                "Tax rate validity {:.4} < {:.4}",
187                tax_rate_validity, self.thresholds.min_rate_validity
188            ));
189        }
190        if approval_order_validity < self.thresholds.min_format_validity {
191            issues.push(format!(
192                "Approval level ordering validity {:.4} < {:.4}",
193                approval_order_validity, self.thresholds.min_format_validity
194            ));
195        }
196        if holiday_multiplier_validity < self.thresholds.min_rate_validity {
197            issues.push(format!(
198                "Holiday multiplier validity {:.4} < {:.4}",
199                holiday_multiplier_validity, self.thresholds.min_rate_validity
200            ));
201        }
202        if iban_length_validity < self.thresholds.min_format_validity {
203            issues.push(format!(
204                "IBAN length validity {:.4} < {:.4}",
205                iban_length_validity, self.thresholds.min_format_validity
206            ));
207        }
208        if fiscal_year_validity < self.thresholds.min_format_validity {
209            issues.push(format!(
210                "Fiscal year month validity {:.4} < {:.4}",
211                fiscal_year_validity, self.thresholds.min_format_validity
212            ));
213        }
214
215        let passes = issues.is_empty();
216
217        Ok(CountryPackEvaluation {
218            tax_rate_validity,
219            approval_order_validity,
220            holiday_multiplier_validity,
221            iban_length_validity,
222            fiscal_year_validity,
223            total_packs: packs.len(),
224            total_tax_rates: all_rates.len(),
225            total_holidays: all_holidays.len(),
226            passes,
227            issues,
228        })
229    }
230}
231
232impl Default for CountryPackEvaluator {
233    fn default() -> Self {
234        Self::new()
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    #[test]
243    fn test_valid_country_pack() {
244        let evaluator = CountryPackEvaluator::new();
245        let packs = vec![CountryPackData {
246            country_code: "DE".to_string(),
247            tax_rates: vec![
248                TaxRateData {
249                    rate_name: "standard_vat".to_string(),
250                    rate: 0.19,
251                },
252                TaxRateData {
253                    rate_name: "reduced_vat".to_string(),
254                    rate: 0.07,
255                },
256            ],
257            approval_levels: vec![
258                ApprovalLevelData {
259                    level: 1,
260                    threshold: 1000.0,
261                },
262                ApprovalLevelData {
263                    level: 2,
264                    threshold: 5000.0,
265                },
266                ApprovalLevelData {
267                    level: 3,
268                    threshold: 25000.0,
269                },
270            ],
271            holidays: vec![
272                HolidayData {
273                    name: "New Year".to_string(),
274                    activity_multiplier: 0.0,
275                },
276                HolidayData {
277                    name: "Christmas Eve".to_string(),
278                    activity_multiplier: 0.3,
279                },
280            ],
281            iban_length: Some(22),
282            fiscal_year_start_month: Some(1),
283        }];
284
285        let result = evaluator.evaluate(&packs).unwrap();
286        assert!(result.passes);
287        assert_eq!(result.total_packs, 1);
288        assert_eq!(result.total_tax_rates, 2);
289        assert_eq!(result.total_holidays, 2);
290    }
291
292    #[test]
293    fn test_invalid_tax_rate() {
294        let evaluator = CountryPackEvaluator::new();
295        let packs = vec![CountryPackData {
296            country_code: "XX".to_string(),
297            tax_rates: vec![TaxRateData {
298                rate_name: "bad_rate".to_string(),
299                rate: 1.5, // Invalid: > 1.0
300            }],
301            approval_levels: vec![],
302            holidays: vec![],
303            iban_length: None,
304            fiscal_year_start_month: None,
305        }];
306
307        let result = evaluator.evaluate(&packs).unwrap();
308        assert!(!result.passes);
309        assert!(result.issues.iter().any(|i| i.contains("Tax rate")));
310    }
311
312    #[test]
313    fn test_unordered_approval_levels() {
314        let evaluator = CountryPackEvaluator::new();
315        let packs = vec![CountryPackData {
316            country_code: "XX".to_string(),
317            tax_rates: vec![],
318            approval_levels: vec![
319                ApprovalLevelData {
320                    level: 1,
321                    threshold: 5000.0,
322                },
323                ApprovalLevelData {
324                    level: 2,
325                    threshold: 1000.0, // Wrong: lower than level 1
326                },
327            ],
328            holidays: vec![],
329            iban_length: None,
330            fiscal_year_start_month: None,
331        }];
332
333        let result = evaluator.evaluate(&packs).unwrap();
334        assert!(!result.passes);
335        assert!(result.issues.iter().any(|i| i.contains("Approval level")));
336    }
337
338    #[test]
339    fn test_invalid_iban_length() {
340        let evaluator = CountryPackEvaluator::new();
341        let packs = vec![CountryPackData {
342            country_code: "XX".to_string(),
343            tax_rates: vec![],
344            approval_levels: vec![],
345            holidays: vec![],
346            iban_length: Some(10), // Invalid: < 15
347            fiscal_year_start_month: None,
348        }];
349
350        let result = evaluator.evaluate(&packs).unwrap();
351        assert!(!result.passes);
352        assert!(result.issues.iter().any(|i| i.contains("IBAN length")));
353    }
354
355    #[test]
356    fn test_invalid_fiscal_year_month() {
357        let evaluator = CountryPackEvaluator::new();
358        let packs = vec![CountryPackData {
359            country_code: "XX".to_string(),
360            tax_rates: vec![],
361            approval_levels: vec![],
362            holidays: vec![],
363            iban_length: None,
364            fiscal_year_start_month: Some(13), // Invalid: > 12
365        }];
366
367        let result = evaluator.evaluate(&packs).unwrap();
368        assert!(!result.passes);
369        assert!(result.issues.iter().any(|i| i.contains("Fiscal year")));
370    }
371
372    #[test]
373    fn test_invalid_holiday_multiplier() {
374        let evaluator = CountryPackEvaluator::new();
375        let packs = vec![CountryPackData {
376            country_code: "XX".to_string(),
377            tax_rates: vec![],
378            approval_levels: vec![],
379            holidays: vec![HolidayData {
380                name: "Bad Holiday".to_string(),
381                activity_multiplier: 1.5, // Invalid: > 1.0
382            }],
383            iban_length: None,
384            fiscal_year_start_month: None,
385        }];
386
387        let result = evaluator.evaluate(&packs).unwrap();
388        assert!(!result.passes);
389        assert!(result
390            .issues
391            .iter()
392            .any(|i| i.contains("Holiday multiplier")));
393    }
394
395    #[test]
396    fn test_empty_data() {
397        let evaluator = CountryPackEvaluator::new();
398        let result = evaluator.evaluate(&[]).unwrap();
399        assert!(result.passes);
400    }
401}