datasynth_eval/coherence/
country_packs.rs1use crate::error::EvalResult;
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct CountryPackThresholds {
13 pub min_rate_validity: f64,
15 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#[derive(Debug, Clone)]
30pub struct TaxRateData {
31 pub rate_name: String,
33 pub rate: f64,
35}
36
37#[derive(Debug, Clone)]
39pub struct ApprovalLevelData {
40 pub level: u32,
42 pub threshold: f64,
44}
45
46#[derive(Debug, Clone)]
48pub struct HolidayData {
49 pub name: String,
51 pub activity_multiplier: f64,
53}
54
55#[derive(Debug, Clone)]
57pub struct CountryPackData {
58 pub country_code: String,
60 pub tax_rates: Vec<TaxRateData>,
62 pub approval_levels: Vec<ApprovalLevelData>,
64 pub holidays: Vec<HolidayData>,
66 pub iban_length: Option<u32>,
68 pub fiscal_year_start_month: Option<u32>,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct CountryPackEvaluation {
75 pub tax_rate_validity: f64,
77 pub approval_order_validity: f64,
79 pub holiday_multiplier_validity: f64,
81 pub iban_length_validity: f64,
83 pub fiscal_year_validity: f64,
85 pub total_packs: usize,
87 pub total_tax_rates: usize,
89 pub total_holidays: usize,
91 pub passes: bool,
93 pub issues: Vec<String>,
95}
96
97pub struct CountryPackEvaluator {
99 thresholds: CountryPackThresholds,
100}
101
102impl CountryPackEvaluator {
103 pub fn new() -> Self {
105 Self {
106 thresholds: CountryPackThresholds::default(),
107 }
108 }
109
110 pub fn with_thresholds(thresholds: CountryPackThresholds) -> Self {
112 Self { thresholds }
113 }
114
115 pub fn evaluate(&self, packs: &[CountryPackData]) -> EvalResult<CountryPackEvaluation> {
117 let mut issues = Vec::new();
118
119 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 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 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 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 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 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, }],
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, },
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), 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), }];
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, }],
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}