Skip to main content

automapper_validation/eval/
abrechnungsdaten.rs

1//! The products of chapter 6.2 of the Codeliste der Konfigurationen,
2//! "Produkte zur Bestellung einer Änderung von Abrechnungsdaten": what an
3//! ORDERS 17133 may order in PIA+5 DE7140, and what each product takes in its
4//! SG30 — a Produkteigenschaft (CAV+ZH9 DE7110) from its own list, or a
5//! Wertedetail (CAV+ZV4 DE7110) of its own format.
6//!
7//! The AHB conditions reading it: `[158]` (the product is one of them),
8//! `[159]` (it has Produkteigenschaften), `[160]` (the CAV+ZH9 code is one of
9//! its own), `[162]` (the CAV+ZV4 value fits its format) and `[180]` (it has a
10//! Wertedetail). Bilanzkreis takes an EIC (`[970]` an..17),
11//! Jahresverbrauchsprognose a whole number (`[909]` ∧ `[937]`).
12//!
13//! Chapter 6.1, "Produkte zur Anmeldung einer Zuordnung des LFN (UTILMD)",
14//! has the same columns for the Anmeldungen (55001, 55077, 55014, ...: SG8
15//! SEQ+Z79 PIA+5, SG10 CAV+ZH9 / CAV+ZV4, `[292]`, `[36]`, `[38]`, `[39]`,
16//! `[40]`) and is [`AbrechnungsdatenProdukte::anmeldung`]. It adds the
17//! Prüfidentifikatoren that may order each product (the Bilanzkreis, which
18//! every Produktpaket must name, is the one product 55014 may order), how often
19//! one may stand in a Produktpaket, and Wertedetails needed only beside one
20//! Produkteigenschaft (Tranchengröße: a share beside `9991000003014`, in 1.4 a
21//! TR-ID beside `9991000003220`).
22//!
23//! Data source: the Codeliste's .docx editions, extracted with
24//! `scripts/extract_konfigurationen_abrechnungsdaten.py` (`--chapter 6.1` for
25//! `konfigurationen_anmeldung.json`). FV2504 to FV2604 read version 1.3c,
26//! FV2610 version 1.4 (which adds spezifische Arbeit to 6.2 and the
27//! Ausschluss products to 6.1).
28
29use 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/// The edition of the Codeliste der Konfigurationen a format version reads.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum Edition {
45    /// Version 1.3c: FV2504, FV2510, FV2604.
46    V1_3c,
47    /// Version 1.4: FV2610.
48    V1_4,
49}
50
51impl Edition {
52    /// The edition `fv` reads; `None` for a format version before FV2504.
53    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/// One product of chapter 6.2 or 6.1.
80#[derive(Debug, Deserialize)]
81pub struct Product {
82    /// The product code, PIA+5 DE7140.
83    pub code: String,
84    pub name: String,
85    /// The codes its CAV+ZH9 DE7110 may carry; empty when it has none.
86    pub property_codes: Vec<PropertyCode>,
87    /// What its CAV+ZV4 DE7110 carries; `None` when it has none.
88    pub wertedetails: Option<Wertedetails>,
89    /// The Prüfidentifikatoren that may order it.
90    #[serde(default)]
91    pub pids: Vec<String>,
92    /// The subchapter listing it (`6.1.1`); `None` in a chapter without.
93    #[serde(default)]
94    pub chapter: Option<String>,
95    /// How often it may stand in one Produktpaket (6.1 only).
96    #[serde(default)]
97    pub max_repetitions: Option<u32>,
98}
99
100impl Product {
101    /// Whether `pid` may order it. True when the table names no
102    /// Prüfidentifikator.
103    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/// A product's Wertedetail: its name and the format conditions its value
115/// must hold (`[970]`).
116#[derive(Debug, Deserialize)]
117pub struct Wertedetails {
118    pub name: String,
119    pub conditions: Vec<u32>,
120    /// Where the Wertedetail is needed only beside a Produkteigenschaft, one
121    /// entry per such code with the formats its value then holds. Empty when
122    /// it stands beside any (or none).
123    #[serde(default)]
124    pub variants: Vec<WertedetailsVariant>,
125}
126
127/// The Wertedetail beside one Produkteigenschaft.
128#[derive(Debug, Deserialize)]
129pub struct WertedetailsVariant {
130    pub with_property: String,
131    pub conditions: Vec<u32>,
132}
133
134/// A valid EIC of a Bilanzkreis (`11X...`, check character included), for a
135/// sample's Wertedetail.
136pub const EXAMPLE_BILANZKREIS: &str = "11XBILANZKREIS-O";
137
138impl Wertedetails {
139    /// Whether `value` holds the format conditions: those of the variant
140    /// beside `property` where the Wertedetail has variants (any variant's
141    /// when `property` is `None` or none matches), else all of them. Unknown
142    /// for a condition the table names but this code does not know.
143    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    /// Whether `value` holds the format conditions, beside any
165    /// Produkteigenschaft.
166    pub fn fits(&self, value: &str) -> ConditionResult {
167        self.fits_beside(value, None)
168    }
169
170    /// Whether the Wertedetail is needed beside `property` (the product's
171    /// CAV+ZH9 code, `None` without one): always, unless it is tied to other
172    /// Produkteigenschaften.
173    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    /// Whether `value` is what a sample should carry beside `property`: it
182    /// holds the formats, and a Bilanzkreis (`[970]`, which says only
183    /// an..17) is an EIC.
184    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    /// A value holding the formats beside `property`, for a sample: a
190    /// Bilanzkreis EIC, a TR-ID, a whole number, a share. `None` when none
191    /// of these does.
192    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
200/// Whether `value` is an Energy Identification Code: 16 characters of
201/// digits, capitals and `-`, the last the check character of the first 15
202/// (weights 16 down to 2, modulo 37).
203pub 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
225/// Whether `value` holds every format condition of `conditions`.
226fn holds(conditions: &[u32], value: &str) -> ConditionResult {
227    let mut result = ConditionResult::True;
228    for condition in conditions {
229        let holds = match condition {
230            // Format: an..17
231            970 => ConditionResult::from(value.chars().count() <= 17),
232            // Format: Mögliche Werte: 0 bis n
233            909 => ConditionResult::from(
234                is_edifact_number(value) && !value.trim_start().starts_with('-'),
235            ),
236            // Format: keine Nachkommastelle
237            937 => ConditionResult::from(is_edifact_number(value) && !value.contains(['.', ','])),
238            // Format: max. 3 Nachkommastellen
239            906 => validate_max_decimal_places(value, 3),
240            // Format: Möglicher Wert: > 0
241            914 => validate_numeric(value, ">", 0.0),
242            // Format: TR-ID
243            922 => validate_technische_ressource_id(value),
244            // Format: max. 2 Nachkommastellen
245            930 => validate_max_decimal_places(value, 2),
246            // Format: Möglicher Wert: < 100
247            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/// Chapter 6.2 (or 6.1) in every edition the format versions read.
260#[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    /// Chapter 6.1, the products of the UTILMD Anmeldungen.
286    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    /// The products of `edition`, in the table's order.
295    pub fn products(&self, edition: Edition) -> &[Product] {
296        self.editions
297            .get(edition.key())
298            .map_or(&[], |products| products.as_slice())
299    }
300
301    /// The product `code` of `edition`, if the table lists it.
302    pub fn product(&self, edition: Edition, code: &str) -> Option<&Product> {
303        self.products(edition).iter().find(|p| p.code == code)
304    }
305}
306
307/// The product `code` of chapter 6.1 or 6.2 of `edition`, if either lists
308/// it. Their codes are apart (the Bilanzkreis is `9991000002082` in 6.1,
309/// `9991000002553` in 6.2).
310pub 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
316/// The product code ordered in PIA+5 DE7140 of the SG29 the field sits in,
317/// or of the message's first PIA+5 without a scope (17133 sends one SG29,
318/// `[2050]`). `None` when there is none.
319fn 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
327/// What `answer` makes of the ordered product: Unknown when there is none,
328/// False for a product the table does not list.
329fn 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
343/// `[158]` Es sind nur die Produkt-Codes erlaubt, die in Kapitel 6.2 genannt
344/// sind: judged on the element's own value (PIA+5 DE7140), else on the
345/// ordered product.
346pub 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
361/// `[159]` Wenn der Produkt-Code in der Spalte "Code der Produkteigenschaft"
362/// mit einem Code befüllt ist.
363pub 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
369/// `[160]` Nur die Codes der Produkteigenschaft des Produkts: the value of
370/// CAV+ZH9 DE7110 is one of its codes.
371pub 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
380/// `[162]` Nur der Wertebereich der Spalte "Wertedetails für Position": the
381/// value of CAV+ZV4 DE7110 holds the product's format conditions. False for a
382/// product without a Wertedetail.
383pub 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
393/// `[180]` Wenn der Produkt-Code in der Spalte "Wertedetails für Position"
394/// eine Angabe hat, deren ggf. enthaltene Bedingung erfüllt ist. The
395/// Bedingungen there are the value's formats, which `[162]` beside it judges;
396/// here, that the product has a Wertedetail.
397pub fn has_wertedetails(ctx: &EvaluationContext, edition: Edition) -> ConditionResult {
398    of_product(ctx, edition, |p| {
399        ConditionResult::from(p.wertedetails.is_some())
400    })
401}
402
403/// The product of the SG8 SEQ+Z79 the field sits in: its PIA+5 DE7140 beside
404/// DE7143 `Z11`. Without a scope, the message's one such product; `None` when
405/// there is none, or several without a scope to choose.
406fn 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
421/// The Produkteigenschaft of the same SG8 (SG10 CAV+ZH9 DE7110), if one is
422/// sent.
423fn 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
431/// What `answer` makes of the SG8's product in chapter 6.1: Unknown when there
432/// is none, False for a product the chapter does not list.
433fn 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
447/// UTILMD `[292]` Es sind nur die Produkt-Codes erlaubt, die in Kapitel 6.1
448/// mit dem Prüfidentifikator des Anwendungsfalls gekennzeichnet sind: judged
449/// on the element's own value (PIA+5 DE7140), else on the SG8's product. A
450/// Prüfidentifikator no row names (FV2610 55003, the Ablehnung naming the
451/// products of the Anmeldung it refuses) may name any product of the chapter.
452pub 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
470/// UTILMD `[36]` Wenn der Produkt-Code derselben SG8 in Kapitel 6.1 in der
471/// Spalte "Code der Produkteigenschaft" mit einem Code befüllt ist.
472pub 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
478/// UTILMD `[38]` Nur die Codes der Produkteigenschaft des Produkts derselben
479/// SG8: the value of CAV+ZH9 DE7110 is one of them.
480pub 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
492/// UTILMD `[39]` Wenn der Produkt-Code derselben SG8 in Kapitel 6.1 in der
493/// Spalte "Wertedetails für Position" eine Angabe hat, deren ggf. enthaltene
494/// Bedingung erfüllt ist: that the product has a Wertedetail, and where it is
495/// needed only beside a Produkteigenschaft (Tranchengröße), that the SG8's
496/// CAV+ZH9 names it.
497pub 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
508/// UTILMD `[40]` Nur der Wertebereich der Spalte "Wertedetails für Position":
509/// the value of CAV+ZV4 DE7110 holds the formats of the product's Wertedetail
510/// beside the SG8's Produkteigenschaft. False for a product without one.
511pub 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
525/// Values that satisfy the ORDERS `condition` of `fv` for some product of
526/// chapter 6.2 — every product code for `[158]`, every Produkteigenschaft for
527/// `[160]` — for a sample generator to offer an element the condition judges.
528/// Empty for any other condition or message type.
529pub 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        // 55014 may order the Bilanzkreis and nothing else.
596        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}