1use serde::Deserialize;
30use std::collections::HashMap;
31use std::sync::OnceLock;
32
33use super::{ConditionResult, EvaluationContext};
34use crate::eval::format_validators::{
35 is_edifact_number, validate_max_decimal_places, validate_numeric,
36 validate_technische_ressource_id,
37};
38
39const EMBEDDED_JSON: &str = include_str!("../../data/konfigurationen_abrechnungsdaten.json");
40const ANMELDUNG_JSON: &str = include_str!("../../data/konfigurationen_anmeldung.json");
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum Edition {
45 V1_3c,
47 V1_4,
49}
50
51impl Edition {
52 pub fn of_format_version(fv: &str) -> Option<Self> {
54 match fv {
55 "FV2504" | "FV2510" | "FV2604" => Some(Self::V1_3c),
56 "FV2610" => Some(Self::V1_4),
57 _ => None,
58 }
59 }
60
61 fn key(self) -> &'static str {
62 match self {
63 Self::V1_3c => "1.3c",
64 Self::V1_4 => "1.4",
65 }
66 }
67}
68
69#[derive(Debug, Deserialize)]
70struct RawFile {
71 versions: HashMap<String, RawVersion>,
72}
73
74#[derive(Debug, Deserialize)]
75struct RawVersion {
76 products: Vec<Product>,
77}
78
79#[derive(Debug, Deserialize)]
81pub struct Product {
82 pub code: String,
84 pub name: String,
85 pub property_codes: Vec<PropertyCode>,
87 pub wertedetails: Option<Wertedetails>,
89 #[serde(default)]
91 pub pids: Vec<String>,
92 #[serde(default)]
94 pub chapter: Option<String>,
95 #[serde(default)]
97 pub max_repetitions: Option<u32>,
98}
99
100impl Product {
101 pub fn orderable_by(&self, pid: &str) -> bool {
104 self.pids.is_empty() || self.pids.iter().any(|p| p == pid)
105 }
106}
107
108#[derive(Debug, Deserialize)]
109pub struct PropertyCode {
110 pub code: String,
111 pub name: String,
112}
113
114#[derive(Debug, Deserialize)]
117pub struct Wertedetails {
118 pub name: String,
119 pub conditions: Vec<u32>,
120 #[serde(default)]
124 pub variants: Vec<WertedetailsVariant>,
125}
126
127#[derive(Debug, Deserialize)]
129pub struct WertedetailsVariant {
130 pub with_property: String,
131 pub conditions: Vec<u32>,
132}
133
134pub const EXAMPLE_BILANZKREIS: &str = "11XBILANZKREIS-O";
137
138impl Wertedetails {
139 pub fn fits_beside(&self, value: &str, property: Option<&str>) -> ConditionResult {
144 if self.variants.is_empty() {
145 return holds(&self.conditions, value);
146 }
147 if let Some(v) = self
148 .variants
149 .iter()
150 .find(|v| Some(v.with_property.as_str()) == property)
151 {
152 return holds(&v.conditions, value);
153 }
154 let results = self.variants.iter().map(|v| holds(&v.conditions, value));
155 results.fold(ConditionResult::False, |best, r| match (best, r) {
156 (ConditionResult::True, _) | (_, ConditionResult::True) => ConditionResult::True,
157 (ConditionResult::Unknown, _) | (_, ConditionResult::Unknown) => {
158 ConditionResult::Unknown
159 }
160 _ => ConditionResult::False,
161 })
162 }
163
164 pub fn fits(&self, value: &str) -> ConditionResult {
167 self.fits_beside(value, None)
168 }
169
170 pub fn needed_beside(&self, property: Option<&str>) -> bool {
174 self.variants.is_empty()
175 || self
176 .variants
177 .iter()
178 .any(|v| Some(v.with_property.as_str()) == property)
179 }
180
181 pub fn suits_sample(&self, value: &str, property: Option<&str>) -> bool {
185 self.fits_beside(value, property) == ConditionResult::True
186 && (!self.conditions.contains(&970) || is_eic(value))
187 }
188
189 pub fn example_value(&self, property: Option<&str>) -> Option<&'static str> {
193 [EXAMPLE_BILANZKREIS, "D1234567890", "4000", "50", "1.125"]
194 .into_iter()
195 .filter(|v| !self.conditions.contains(&970) || *v == EXAMPLE_BILANZKREIS)
196 .find(|v| self.fits_beside(v, property) == ConditionResult::True)
197 }
198}
199
200pub fn is_eic(value: &str) -> bool {
204 let digit = |c: char| match c {
205 '0'..='9' => Some(c as u32 - '0' as u32),
206 'A'..='Z' => Some(c as u32 - 'A' as u32 + 10),
207 '-' => Some(36),
208 _ => None,
209 };
210 let Some(digits) = value.chars().map(digit).collect::<Option<Vec<u32>>>() else {
211 return false;
212 };
213 if digits.len() != 16 {
214 return false;
215 }
216 let sum: u32 = digits[..15]
217 .iter()
218 .enumerate()
219 .map(|(i, d)| d * (16 - i as u32))
220 .sum();
221 let check = 36 - (sum + 36) % 37;
222 check != 36 && digits[15] == check
223}
224
225fn holds(conditions: &[u32], value: &str) -> ConditionResult {
227 let mut result = ConditionResult::True;
228 for condition in conditions {
229 let holds = match condition {
230 970 => ConditionResult::from(value.chars().count() <= 17),
232 909 => ConditionResult::from(
234 is_edifact_number(value) && !value.trim_start().starts_with('-'),
235 ),
236 937 => ConditionResult::from(is_edifact_number(value) && !value.contains(['.', ','])),
238 906 => validate_max_decimal_places(value, 3),
240 914 => validate_numeric(value, ">", 0.0),
242 922 => validate_technische_ressource_id(value),
244 930 => validate_max_decimal_places(value, 2),
246 955 => validate_numeric(value, "<", 100.0),
248 _ => ConditionResult::Unknown,
249 };
250 match holds {
251 ConditionResult::False => return ConditionResult::False,
252 ConditionResult::Unknown => result = ConditionResult::Unknown,
253 ConditionResult::True => {}
254 }
255 }
256 result
257}
258
259#[derive(Debug)]
261pub struct AbrechnungsdatenProdukte {
262 editions: HashMap<String, Vec<Product>>,
263}
264
265impl AbrechnungsdatenProdukte {
266 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
267 let raw: RawFile = serde_json::from_str(json)?;
268 Ok(Self {
269 editions: raw
270 .versions
271 .into_iter()
272 .map(|(version, v)| (version, v.products))
273 .collect(),
274 })
275 }
276
277 pub fn embedded() -> &'static Self {
278 static CELL: OnceLock<AbrechnungsdatenProdukte> = OnceLock::new();
279 CELL.get_or_init(|| {
280 AbrechnungsdatenProdukte::from_json(EMBEDDED_JSON)
281 .expect("embedded konfigurationen_abrechnungsdaten.json is malformed")
282 })
283 }
284
285 pub fn anmeldung() -> &'static Self {
287 static CELL: OnceLock<AbrechnungsdatenProdukte> = OnceLock::new();
288 CELL.get_or_init(|| {
289 AbrechnungsdatenProdukte::from_json(ANMELDUNG_JSON)
290 .expect("embedded konfigurationen_anmeldung.json is malformed")
291 })
292 }
293
294 pub fn products(&self, edition: Edition) -> &[Product] {
296 self.editions
297 .get(edition.key())
298 .map_or(&[], |products| products.as_slice())
299 }
300
301 pub fn product(&self, edition: Edition, code: &str) -> Option<&Product> {
303 self.products(edition).iter().find(|p| p.code == code)
304 }
305}
306
307pub fn chapter6_product(edition: Edition, code: &str) -> Option<&'static Product> {
311 AbrechnungsdatenProdukte::anmeldung()
312 .product(edition, code)
313 .or_else(|| AbrechnungsdatenProdukte::embedded().product(edition, code))
314}
315
316fn ordered_product(ctx: &EvaluationContext) -> Option<String> {
320 ctx.scoped_find_segments_in("SG29", "PIA")
321 .into_iter()
322 .find(|s| s.get_element(0) == "5")
323 .and_then(|s| s.elements.get(1)?.first().cloned())
324 .filter(|code| !code.is_empty())
325}
326
327fn of_product(
330 ctx: &EvaluationContext,
331 edition: Edition,
332 answer: impl Fn(&Product) -> ConditionResult,
333) -> ConditionResult {
334 let Some(code) = ordered_product(ctx) else {
335 return ConditionResult::Unknown;
336 };
337 match AbrechnungsdatenProdukte::embedded().product(edition, &code) {
338 Some(product) => answer(product),
339 None => ConditionResult::False,
340 }
341}
342
343pub fn product_is_listed(ctx: &EvaluationContext, edition: Edition) -> ConditionResult {
347 let code = match ctx.resolved_value {
348 Some(value) => value.to_string(),
349 None => match ordered_product(ctx) {
350 Some(code) => code,
351 None => return ConditionResult::Unknown,
352 },
353 };
354 ConditionResult::from(
355 AbrechnungsdatenProdukte::embedded()
356 .product(edition, &code)
357 .is_some(),
358 )
359}
360
361pub fn has_property_codes(ctx: &EvaluationContext, edition: Edition) -> ConditionResult {
364 of_product(ctx, edition, |p| {
365 ConditionResult::from(!p.property_codes.is_empty())
366 })
367}
368
369pub fn property_code_allowed(ctx: &EvaluationContext, edition: Edition) -> ConditionResult {
372 let Some(value) = ctx.resolved_value else {
373 return ConditionResult::Unknown;
374 };
375 of_product(ctx, edition, |p| {
376 ConditionResult::from(p.property_codes.iter().any(|c| c.code == value))
377 })
378}
379
380pub fn wertedetails_value_fits(ctx: &EvaluationContext, edition: Edition) -> ConditionResult {
384 let Some(value) = ctx.resolved_value else {
385 return ConditionResult::Unknown;
386 };
387 of_product(ctx, edition, |p| match &p.wertedetails {
388 Some(w) => w.fits(value),
389 None => ConditionResult::False,
390 })
391}
392
393pub fn has_wertedetails(ctx: &EvaluationContext, edition: Edition) -> ConditionResult {
398 of_product(ctx, edition, |p| {
399 ConditionResult::from(p.wertedetails.is_some())
400 })
401}
402
403fn anmeldung_product(ctx: &EvaluationContext) -> Option<String> {
407 let mut codes: Vec<String> = ctx
408 .scoped_find_segments_in("SG8", "PIA")
409 .into_iter()
410 .filter(|s| s.get_element(0) == "5" && s.get_component(1, 1) == "Z11")
411 .filter_map(|s| s.elements.get(1)?.first().cloned())
412 .filter(|code| !code.is_empty())
413 .collect();
414 codes.dedup();
415 match codes.as_slice() {
416 [one] => Some(one.clone()),
417 _ => None,
418 }
419}
420
421fn anmeldung_property(ctx: &EvaluationContext) -> Option<String> {
424 ctx.scoped_find_segments_in("SG8", "CAV")
425 .into_iter()
426 .find(|s| s.get_component(0, 0) == "ZH9")
427 .map(|s| s.get_component(0, 3).to_string())
428 .filter(|code| !code.is_empty())
429}
430
431fn of_anmeldung_product(
434 ctx: &EvaluationContext,
435 edition: Edition,
436 answer: impl Fn(&Product) -> ConditionResult,
437) -> ConditionResult {
438 let Some(code) = anmeldung_product(ctx) else {
439 return ConditionResult::Unknown;
440 };
441 match AbrechnungsdatenProdukte::anmeldung().product(edition, &code) {
442 Some(product) => answer(product),
443 None => ConditionResult::False,
444 }
445}
446
447pub fn anmeldung_product_allowed(ctx: &EvaluationContext, edition: Edition) -> ConditionResult {
453 let code = match ctx.resolved_value {
454 Some(value) => value.to_string(),
455 None => match anmeldung_product(ctx) {
456 Some(code) => code,
457 None => return ConditionResult::Unknown,
458 },
459 };
460 let table = AbrechnungsdatenProdukte::anmeldung().products(edition);
461 let pid = ctx.pruefidentifikator;
462 let named = table.iter().any(|p| p.pids.iter().any(|q| q == pid));
463 ConditionResult::from(
464 table
465 .iter()
466 .any(|p| p.code == code && (!named || p.orderable_by(pid))),
467 )
468}
469
470pub fn anmeldung_has_property_codes(ctx: &EvaluationContext, edition: Edition) -> ConditionResult {
473 of_anmeldung_product(ctx, edition, |p| {
474 ConditionResult::from(!p.property_codes.is_empty())
475 })
476}
477
478pub fn anmeldung_property_code_allowed(
481 ctx: &EvaluationContext,
482 edition: Edition,
483) -> ConditionResult {
484 let Some(value) = ctx.resolved_value else {
485 return ConditionResult::Unknown;
486 };
487 of_anmeldung_product(ctx, edition, |p| {
488 ConditionResult::from(p.property_codes.iter().any(|c| c.code == value))
489 })
490}
491
492pub fn anmeldung_has_wertedetails(ctx: &EvaluationContext, edition: Edition) -> ConditionResult {
498 let property = anmeldung_property(ctx);
499 of_anmeldung_product(ctx, edition, |p| {
500 ConditionResult::from(
501 p.wertedetails
502 .as_ref()
503 .is_some_and(|w| w.needed_beside(property.as_deref())),
504 )
505 })
506}
507
508pub fn anmeldung_wertedetails_value_fits(
512 ctx: &EvaluationContext,
513 edition: Edition,
514) -> ConditionResult {
515 let Some(value) = ctx.resolved_value else {
516 return ConditionResult::Unknown;
517 };
518 let property = anmeldung_property(ctx);
519 of_anmeldung_product(ctx, edition, |p| match &p.wertedetails {
520 Some(w) => w.fits_beside(value, property.as_deref()),
521 None => ConditionResult::False,
522 })
523}
524
525pub fn candidate_values(message_type: &str, fv: &str, condition: u32) -> Vec<String> {
530 let Some(edition) = Edition::of_format_version(fv).filter(|_| message_type == "ORDERS") else {
531 return Vec::new();
532 };
533 let products = AbrechnungsdatenProdukte::embedded().products(edition);
534 match condition {
535 158 => products.iter().map(|p| p.code.clone()).collect(),
536 160 => products
537 .iter()
538 .flat_map(|p| p.property_codes.iter().map(|c| c.code.clone()))
539 .collect(),
540 _ => Vec::new(),
541 }
542}
543
544#[cfg(test)]
545mod tests {
546 use super::*;
547
548 #[test]
549 fn both_editions_load() {
550 let table = AbrechnungsdatenProdukte::embedded();
551 assert_eq!(table.products(Edition::V1_3c).len(), 9);
552 assert_eq!(table.products(Edition::V1_4).len(), 10);
553 assert!(table.product(Edition::V1_3c, "9991000003246").is_none());
554 let arbeit = table.product(Edition::V1_4, "9991000003246").expect("1.4");
555 assert_eq!(arbeit.name, "spezifische Arbeit");
556 }
557
558 #[test]
559 fn wertedetails_hold_their_formats() {
560 let table = AbrechnungsdatenProdukte::embedded();
561 let w = |code: &str| {
562 table
563 .product(Edition::V1_4, code)
564 .and_then(|p| p.wertedetails.as_ref())
565 .expect("a Wertedetail")
566 };
567 let bilanzkreis = w("9991000002553");
568 assert_eq!(bilanzkreis.fits("11XLIEFERANT2--E"), ConditionResult::True);
569 assert_eq!(
570 bilanzkreis.fits("11XLIEFERANT2--E-TOO-LONG"),
571 ConditionResult::False
572 );
573 let prognose = w("9991000002561");
574 assert_eq!(prognose.fits("4000"), ConditionResult::True);
575 assert_eq!(prognose.fits("4000.5"), ConditionResult::False);
576 assert_eq!(prognose.fits("-1"), ConditionResult::False);
577 assert_eq!(prognose.fits("11XLIEFERANT2--E"), ConditionResult::False);
578 let arbeit = w("9991000003246");
579 assert_eq!(arbeit.fits("1.125"), ConditionResult::True);
580 assert_eq!(arbeit.fits("1.1255"), ConditionResult::False);
581 assert_eq!(arbeit.fits("-0.5"), ConditionResult::False);
582 }
583
584 #[test]
585 fn anmeldung_products_name_their_pids() {
586 let table = AbrechnungsdatenProdukte::anmeldung();
587 assert_eq!(table.products(Edition::V1_3c).len(), 18);
588 assert_eq!(table.products(Edition::V1_4).len(), 26);
589 let bilanzkreis = table
590 .product(Edition::V1_3c, "9991000002082")
591 .expect("Bilanzkreis");
592 assert_eq!(bilanzkreis.chapter.as_deref(), Some("6.1.1"));
593 assert!(bilanzkreis.property_codes.is_empty());
594 assert!(bilanzkreis.orderable_by("55014"));
595 let for_55014: Vec<&str> = table
597 .products(Edition::V1_3c)
598 .iter()
599 .filter(|p| p.orderable_by("55014"))
600 .map(|p| p.code.as_str())
601 .collect();
602 assert_eq!(for_55014, ["9991000002082"]);
603 let w = bilanzkreis.wertedetails.as_ref().expect("a Wertedetail");
604 assert_eq!(w.example_value(None), Some(EXAMPLE_BILANZKREIS));
605 let verbrauchsart = table
606 .product(Edition::V1_3c, "9991000002727")
607 .expect("Verbrauchsart");
608 assert_eq!(verbrauchsart.max_repetitions, Some(5));
609 assert!(verbrauchsart.wertedetails.is_none());
610 }
611
612 #[test]
613 fn tranchengroesse_takes_its_value_beside_its_property() {
614 let table = AbrechnungsdatenProdukte::anmeldung();
615 let w = |edition| {
616 table
617 .product(edition, "9991000002090")
618 .and_then(|p| p.wertedetails.as_ref())
619 .expect("Tranchengröße")
620 };
621 let share = Some("9991000003014");
622 let factor = Some("9991000003022");
623 let tr = Some("9991000003220");
624 let old = w(Edition::V1_3c);
625 assert!(old.needed_beside(share));
626 assert!(!old.needed_beside(factor));
627 assert_eq!(old.fits_beside("50.25", share), ConditionResult::True);
628 assert_eq!(old.fits_beside("100", share), ConditionResult::False);
629 assert_eq!(old.fits_beside("0", share), ConditionResult::False);
630 assert_eq!(old.fits_beside("50.125", share), ConditionResult::False);
631 assert_eq!(old.example_value(share), Some("50"));
632 let new = w(Edition::V1_4);
633 assert!(new.needed_beside(tr));
634 assert_eq!(new.example_value(tr), Some("D1234567890"));
635 assert_eq!(new.fits_beside("50", tr), ConditionResult::False);
636 assert_eq!(new.fits_beside("D1234567890", None), ConditionResult::True);
637 }
638
639 #[test]
640 fn example_bilanzkreis_is_a_valid_eic() {
641 assert!(is_eic(EXAMPLE_BILANZKREIS));
642 assert!(is_eic("11XLIEFERANT2--E"));
643 assert!(is_eic("10YDE-RWENET---I"));
644 assert!(!is_eic("10YDE-RWENET---J"));
645 assert!(!is_eic("DKWP98GBANI7"));
646 }
647}