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)]
239#[allow(clippy::unwrap_used)]
240mod tests {
241    use super::*;
242
243    #[test]
244    fn test_valid_country_pack() {
245        let evaluator = CountryPackEvaluator::new();
246        let packs = vec![CountryPackData {
247            country_code: "DE".to_string(),
248            tax_rates: vec![
249                TaxRateData {
250                    rate_name: "standard_vat".to_string(),
251                    rate: 0.19,
252                },
253                TaxRateData {
254                    rate_name: "reduced_vat".to_string(),
255                    rate: 0.07,
256                },
257            ],
258            approval_levels: vec![
259                ApprovalLevelData {
260                    level: 1,
261                    threshold: 1000.0,
262                },
263                ApprovalLevelData {
264                    level: 2,
265                    threshold: 5000.0,
266                },
267                ApprovalLevelData {
268                    level: 3,
269                    threshold: 25000.0,
270                },
271            ],
272            holidays: vec![
273                HolidayData {
274                    name: "New Year".to_string(),
275                    activity_multiplier: 0.0,
276                },
277                HolidayData {
278                    name: "Christmas Eve".to_string(),
279                    activity_multiplier: 0.3,
280                },
281            ],
282            iban_length: Some(22),
283            fiscal_year_start_month: Some(1),
284        }];
285
286        let result = evaluator.evaluate(&packs).unwrap();
287        assert!(result.passes);
288        assert_eq!(result.total_packs, 1);
289        assert_eq!(result.total_tax_rates, 2);
290        assert_eq!(result.total_holidays, 2);
291    }
292
293    #[test]
294    fn test_invalid_tax_rate() {
295        let evaluator = CountryPackEvaluator::new();
296        let packs = vec![CountryPackData {
297            country_code: "XX".to_string(),
298            tax_rates: vec![TaxRateData {
299                rate_name: "bad_rate".to_string(),
300                rate: 1.5, // Invalid: > 1.0
301            }],
302            approval_levels: vec![],
303            holidays: vec![],
304            iban_length: None,
305            fiscal_year_start_month: None,
306        }];
307
308        let result = evaluator.evaluate(&packs).unwrap();
309        assert!(!result.passes);
310        assert!(result.issues.iter().any(|i| i.contains("Tax rate")));
311    }
312
313    #[test]
314    fn test_unordered_approval_levels() {
315        let evaluator = CountryPackEvaluator::new();
316        let packs = vec![CountryPackData {
317            country_code: "XX".to_string(),
318            tax_rates: vec![],
319            approval_levels: vec![
320                ApprovalLevelData {
321                    level: 1,
322                    threshold: 5000.0,
323                },
324                ApprovalLevelData {
325                    level: 2,
326                    threshold: 1000.0, // Wrong: lower than level 1
327                },
328            ],
329            holidays: vec![],
330            iban_length: None,
331            fiscal_year_start_month: None,
332        }];
333
334        let result = evaluator.evaluate(&packs).unwrap();
335        assert!(!result.passes);
336        assert!(result.issues.iter().any(|i| i.contains("Approval level")));
337    }
338
339    #[test]
340    fn test_invalid_iban_length() {
341        let evaluator = CountryPackEvaluator::new();
342        let packs = vec![CountryPackData {
343            country_code: "XX".to_string(),
344            tax_rates: vec![],
345            approval_levels: vec![],
346            holidays: vec![],
347            iban_length: Some(10), // Invalid: < 15
348            fiscal_year_start_month: None,
349        }];
350
351        let result = evaluator.evaluate(&packs).unwrap();
352        assert!(!result.passes);
353        assert!(result.issues.iter().any(|i| i.contains("IBAN length")));
354    }
355
356    #[test]
357    fn test_invalid_fiscal_year_month() {
358        let evaluator = CountryPackEvaluator::new();
359        let packs = vec![CountryPackData {
360            country_code: "XX".to_string(),
361            tax_rates: vec![],
362            approval_levels: vec![],
363            holidays: vec![],
364            iban_length: None,
365            fiscal_year_start_month: Some(13), // Invalid: > 12
366        }];
367
368        let result = evaluator.evaluate(&packs).unwrap();
369        assert!(!result.passes);
370        assert!(result.issues.iter().any(|i| i.contains("Fiscal year")));
371    }
372
373    #[test]
374    fn test_invalid_holiday_multiplier() {
375        let evaluator = CountryPackEvaluator::new();
376        let packs = vec![CountryPackData {
377            country_code: "XX".to_string(),
378            tax_rates: vec![],
379            approval_levels: vec![],
380            holidays: vec![HolidayData {
381                name: "Bad Holiday".to_string(),
382                activity_multiplier: 1.5, // Invalid: > 1.0
383            }],
384            iban_length: None,
385            fiscal_year_start_month: None,
386        }];
387
388        let result = evaluator.evaluate(&packs).unwrap();
389        assert!(!result.passes);
390        assert!(result
391            .issues
392            .iter()
393            .any(|i| i.contains("Holiday multiplier")));
394    }
395
396    #[test]
397    fn test_empty_data() {
398        let evaluator = CountryPackEvaluator::new();
399        let result = evaluator.evaluate(&[]).unwrap();
400        assert!(result.passes);
401    }
402}