Skip to main content

automapper_validation/generated/fv2510/
utilmd_strom_conditions_fv2510.rs

1// <auto-generated>
2// Generated by automapper-generator generate-conditions
3// AHB: xml-migs-and-ahbs/FV2510/UTILMD_AHB_Strom_2_1_Fehlerkorrektur_20260302.xml
4// Generated: 2026-09-23T08:11:11Z
5// Delegates unchanged conditions to UtilmdStromConditionEvaluatorFV2504 (FV2504).
6// </auto-generated>
7
8#![allow(clippy::style, clippy::complexity)]
9
10#[allow(unused_imports)]
11use crate::eval::format_validators::*;
12use crate::eval::{ConditionEvaluator, ConditionResult, EvaluationContext};
13use crate::generated::fv2504::UtilmdStromConditionEvaluatorFV2504;
14
15/// Condition evaluator for UTILMD_Strom FV2510.
16///
17/// Delegates to [`UtilmdStromConditionEvaluatorFV2504`] for conditions whose AHB description is
18/// unchanged from that version. Local overrides: [2], [3], [20], [28], [29], [79], [136], [139], [157], [158], [168], [169], [242], [253], [258], [263], [297], [312], [313], [326], [328], [346], [413], [414], [415], [416], [461], [534], [557], [560], [561], [562], [564], [565], [578], [681], [686], [2000], [2016], [2121].
19pub struct UtilmdStromConditionEvaluatorFV2510 {
20    fallback: UtilmdStromConditionEvaluatorFV2504,
21}
22
23impl Default for UtilmdStromConditionEvaluatorFV2510 {
24    fn default() -> Self {
25        Self {
26            fallback: UtilmdStromConditionEvaluatorFV2504::default(),
27        }
28    }
29}
30
31/// Condition IDs whose implementation differs from the fallback.
32const FV2510_OVERRIDES: &[u32] = &[
33    2, 3, 20, 28, 29, 79, 136, 139, 157, 158, 168, 169, 242, 253, 258, 263, 297, 312, 313, 326,
34    328, 346, 413, 414, 415, 416, 461, 534, 557, 560, 561, 562, 564, 565, 578, 681, 686, 2000,
35    2016, 2121,
36];
37
38/// Overrides that require context from outside the message.
39const FV2510_EXTERNAL: &[u32] = &[3, 326, 328];
40
41impl ConditionEvaluator for UtilmdStromConditionEvaluatorFV2510 {
42    fn message_type(&self) -> &str {
43        "UTILMD_Strom"
44    }
45
46    fn format_version(&self) -> &str {
47        "FV2510"
48    }
49
50    fn evaluate(&self, condition: u32, ctx: &EvaluationContext) -> ConditionResult {
51        match condition {
52            2 => self.evaluate_2(ctx),
53            3 => self.evaluate_3(ctx),
54            20 => self.evaluate_20(ctx),
55            28 => self.evaluate_28(ctx),
56            29 => self.evaluate_29(ctx),
57            79 => self.evaluate_79(ctx),
58            136 => self.evaluate_136(ctx),
59            139 => self.evaluate_139(ctx),
60            157 => self.evaluate_157(ctx),
61            158 => self.evaluate_158(ctx),
62            168 => self.evaluate_168(ctx),
63            169 => self.evaluate_169(ctx),
64            242 => self.evaluate_242(ctx),
65            253 => self.evaluate_253(ctx),
66            258 => self.evaluate_258(ctx),
67            263 => self.evaluate_263(ctx),
68            297 => self.evaluate_297(ctx),
69            312 => self.evaluate_312(ctx),
70            313 => self.evaluate_313(ctx),
71            326 => self.evaluate_326(ctx),
72            328 => self.evaluate_328(ctx),
73            346 => self.evaluate_346(ctx),
74            413 => self.evaluate_413(ctx),
75            414 => self.evaluate_414(ctx),
76            415 => self.evaluate_415(ctx),
77            416 => self.evaluate_416(ctx),
78            461 => self.evaluate_461(ctx),
79            534 => self.evaluate_534(ctx),
80            557 => self.evaluate_557(ctx),
81            560 => self.evaluate_560(ctx),
82            561 => self.evaluate_561(ctx),
83            562 => self.evaluate_562(ctx),
84            564 => self.evaluate_564(ctx),
85            565 => self.evaluate_565(ctx),
86            578 => self.evaluate_578(ctx),
87            681 => self.evaluate_681(ctx),
88            686 => self.evaluate_686(ctx),
89            2000 => self.evaluate_2000(ctx),
90            2016 => self.evaluate_2016(ctx),
91            2121 => self.evaluate_2121(ctx),
92            other => self.fallback.evaluate(other, ctx),
93        }
94    }
95
96    fn is_external(&self, condition: u32) -> bool {
97        if FV2510_OVERRIDES.contains(&condition) {
98            FV2510_EXTERNAL.contains(&condition)
99        } else {
100            self.fallback.is_external(condition)
101        }
102    }
103
104    fn is_known(&self, condition: u32) -> bool {
105        FV2510_OVERRIDES.contains(&condition) || self.fallback.is_known(condition)
106    }
107}
108
109impl UtilmdStromConditionEvaluatorFV2510 {
110    /// [2] Wenn UNH DE0070 (Übermittlungsfolgenummer) mit 1 vorhanden
111    fn evaluate_2(&self, ctx: &EvaluationContext) -> ConditionResult {
112        ctx.has_segment_matching("UNH", &[(3, 0, "1")])
113    }
114
115    /// [3] Bei Aufteilung, in der Nachricht mit der höchsten Übermittlungsfolgenummer
116    /// EXTERNAL: Requires context from outside the message.
117    fn evaluate_3(&self, ctx: &EvaluationContext) -> ConditionResult {
118        ctx.external.evaluate("is_highest_sequence_in_split")
119    }
120
121    /// [20] Wenn im SG8 SEQ+Z01 (Daten der Marktlokation) mit der gleichen ID im SG8 RFF+Z18 (Referenz auf die ID der Marktlokation) wie in diesem SG8 das SG10 CCI+Z30++Z07 (Verbrauch) vorhanden
122    fn evaluate_20(&self, ctx: &EvaluationContext) -> ConditionResult {
123        let nav = match ctx.navigator() {
124            Some(n) => n,
125            None => return ctx.has_qualified_value("CCI", 0, "Z30", 2, 0, &["Z07"]),
126        };
127        let sg8_count = nav.group_instance_count(&["SG4", "SG8"]);
128        for i in 0..sg8_count {
129            let seq_segs = nav.find_segments_in_group("SEQ", &["SG4", "SG8"], i);
130            let has_z01 = seq_segs.iter().any(|s| {
131                s.elements
132                    .first()
133                    .and_then(|e| e.first())
134                    .is_some_and(|v| v == "Z01")
135            });
136            if !has_z01 {
137                continue;
138            }
139            let sg10_count = nav.child_group_instance_count(&["SG4", "SG8"], i, "SG10");
140            for j in 0..sg10_count {
141                let ccis = nav.find_segments_in_child_group("CCI", &["SG4", "SG8"], i, "SG10", j);
142                if ccis.iter().any(|s| {
143                    s.elements
144                        .first()
145                        .and_then(|e| e.first())
146                        .is_some_and(|v| v == "Z30")
147                        && s.elements
148                            .get(2)
149                            .and_then(|e| e.first())
150                            .is_some_and(|v| v == "Z07")
151                }) {
152                    return ConditionResult::True;
153                }
154            }
155        }
156        ConditionResult::False
157    }
158
159    /// [28] Wenn in dieser SG4 IDE+24 das STS+E01++A04: E_0017 bzw. A03: E_0052 (Zusätzlicher Datensatz / ergänzte Marktlokation) nicht vorhanden
160    // REVIEW: Within each SG4 containing IDE+24 (Vorgang), check if STS+E01 with Prüfschritt A04:E_0017 (Zusätzlicher Datensatz) or A03:E_0052 (ergänzte Marktlokation) is absent. Uses navigator to scope the check per SG4 instance. STS elements[0][0]="E01", elements[2][0]=Prüfschritt code, elements[2][1]=codelist. Condition is True when this STS is NOT found in the SG4, indicating no 'additional record' status was set. (medium confidence)
161    fn evaluate_28(&self, ctx: &EvaluationContext) -> ConditionResult {
162        // HAND-EDITED: "in dieser SG4 IDE+24 … nicht vorhanden" — the enclosing SG4
163        // only, when the scope reaches one; the message as before otherwise.
164        if let Some(scope) = ctx.enclosing_scope("SG4") {
165            let ide_24 = ctx
166                .find_segments_at(scope, "IDE")
167                .iter()
168                .any(|s| s.get_element(0) == "24");
169            let sts = ctx.find_segments_at(scope, "STS").iter().any(|s| {
170                s.get_element(0) == "E01"
171                    && matches!(
172                        (s.get_component(2, 0), s.get_component(2, 1)),
173                        ("A04", "E_0017") | ("A03", "E_0052")
174                    )
175            });
176            return ConditionResult::from(ide_24 && !sts);
177        }
178        let nav = match ctx.navigator() {
179            Some(n) => n,
180            None => {
181                let sts_segs = ctx.find_segments_with_qualifier("STS", 0, "E01");
182                let found = sts_segs.iter().any(|s| {
183                    let code = s
184                        .elements
185                        .get(2)
186                        .and_then(|e| e.first())
187                        .map(|v| v.as_str());
188                    let list = s.elements.get(2).and_then(|e| e.get(1)).map(|v| v.as_str());
189                    (code == Some("A04") && list == Some("E_0017"))
190                        || (code == Some("A03") && list == Some("E_0052"))
191                });
192                return ConditionResult::from(!found);
193            }
194        };
195        let sg4_count = nav.group_instance_count(&["SG4"]);
196        for i in 0..sg4_count {
197            let ide_segs = nav.find_segments_in_group("IDE", &["SG4"], i);
198            let has_ide_24 = ide_segs.iter().any(|s| {
199                s.elements
200                    .first()
201                    .and_then(|e| e.first())
202                    .is_some_and(|v| v == "24")
203            });
204            if !has_ide_24 {
205                continue;
206            }
207            let sts_segs = nav.find_segments_in_group("STS", &["SG4"], i);
208            let found = sts_segs.iter().any(|s| {
209                let qual = s
210                    .elements
211                    .first()
212                    .and_then(|e| e.first())
213                    .map(|v| v.as_str());
214                let code = s
215                    .elements
216                    .get(2)
217                    .and_then(|e| e.first())
218                    .map(|v| v.as_str());
219                let list = s.elements.get(2).and_then(|e| e.get(1)).map(|v| v.as_str());
220                qual == Some("E01")
221                    && ((code == Some("A04") && list == Some("E_0017"))
222                        || (code == Some("A03") && list == Some("E_0052")))
223            });
224            if !found {
225                return ConditionResult::True;
226            }
227        }
228        ConditionResult::False
229    }
230
231    /// [29] Wenn in dieser SG4 IDE+24 das STS+E01++A06: E_0017 bzw. A05: E_0052 (Zu viele Marktlokationen enthalten / entfallene Marktlokation) nicht vorhanden
232    // REVIEW: Within each SG4 containing IDE+24 (Vorgang), check if STS+E01 with Prüfschritt A06:E_0017 (Zu viele Marktlokationen) or A05:E_0052 (entfallene Marktlokation) is absent. Mirrors condition [28] but for the 'too many / dropped Marktlokation' status codes. True when neither combination is found in the SG4, indicating no 'excess/dropped records' status was set. (medium confidence)
233    fn evaluate_29(&self, ctx: &EvaluationContext) -> ConditionResult {
234        // HAND-EDITED: "in dieser SG4 IDE+24 … nicht vorhanden" — the enclosing SG4
235        // only, when the scope reaches one; the message as before otherwise.
236        if let Some(scope) = ctx.enclosing_scope("SG4") {
237            let ide_24 = ctx
238                .find_segments_at(scope, "IDE")
239                .iter()
240                .any(|s| s.get_element(0) == "24");
241            let sts = ctx.find_segments_at(scope, "STS").iter().any(|s| {
242                s.get_element(0) == "E01"
243                    && matches!(
244                        (s.get_component(2, 0), s.get_component(2, 1)),
245                        ("A06", "E_0017") | ("A05", "E_0052")
246                    )
247            });
248            return ConditionResult::from(ide_24 && !sts);
249        }
250        let nav = match ctx.navigator() {
251            Some(n) => n,
252            None => {
253                let sts_segs = ctx.find_segments_with_qualifier("STS", 0, "E01");
254                let found = sts_segs.iter().any(|s| {
255                    let code = s
256                        .elements
257                        .get(2)
258                        .and_then(|e| e.first())
259                        .map(|v| v.as_str());
260                    let list = s.elements.get(2).and_then(|e| e.get(1)).map(|v| v.as_str());
261                    (code == Some("A06") && list == Some("E_0017"))
262                        || (code == Some("A05") && list == Some("E_0052"))
263                });
264                return ConditionResult::from(!found);
265            }
266        };
267        let sg4_count = nav.group_instance_count(&["SG4"]);
268        for i in 0..sg4_count {
269            let ide_segs = nav.find_segments_in_group("IDE", &["SG4"], i);
270            let has_ide_24 = ide_segs.iter().any(|s| {
271                s.elements
272                    .first()
273                    .and_then(|e| e.first())
274                    .is_some_and(|v| v == "24")
275            });
276            if !has_ide_24 {
277                continue;
278            }
279            let sts_segs = nav.find_segments_in_group("STS", &["SG4"], i);
280            let found = sts_segs.iter().any(|s| {
281                let qual = s
282                    .elements
283                    .first()
284                    .and_then(|e| e.first())
285                    .map(|v| v.as_str());
286                let code = s
287                    .elements
288                    .get(2)
289                    .and_then(|e| e.first())
290                    .map(|v| v.as_str());
291                let list = s.elements.get(2).and_then(|e| e.get(1)).map(|v| v.as_str());
292                qual == Some("E01")
293                    && ((code == Some("A06") && list == Some("E_0017"))
294                        || (code == Some("A05") && list == Some("E_0052")))
295            });
296            if !found {
297                return ConditionResult::True;
298            }
299        }
300        ConditionResult::False
301    }
302
303    /// [79] Wenn SG4 STS+7++Z33 (Auszug wegen Stilllegung) vorhanden
304    fn evaluate_79(&self, ctx: &EvaluationContext) -> ConditionResult {
305        // STS+7++Z33: elements[0][0]="7" (Transaktionsgrund), elements[2][0]="Z33" (Auszug wegen Stilllegung).
306        // FV2504's text read STS+7+++Z33 (element 3); since the 2026-03-02 AHB it is element 2, as in [157].
307        ctx.has_segment_matching("STS", &[(0, 0, "7"), (2, 0, "Z33")])
308    }
309
310    /// [136] Wenn SG10 CCI+++E13 (Zählertyp) CAV+EHZ (elektronischer Haushaltszähler) in dieser SG8 vorhanden
311    fn evaluate_136(&self, ctx: &EvaluationContext) -> ConditionResult {
312        // HAND-EDITED: "in dieser/derselben SG8" — the enclosing SG8 only, when the
313        // scope reaches one; the message as before otherwise.
314        if let Some(_scope) = ctx.enclosing_scope("SG8") {
315            return ConditionResult::from(
316                ctx.enclosing_child_segments("SG8", "SG10", &["CCI", "CAV"])
317                    .unwrap_or_default()
318                    .iter()
319                    .any(|segs| {
320                        segs.iter()
321                            .any(|s| s.id == "CCI" && s.get_component(2, 0) == "E13")
322                            && segs
323                                .iter()
324                                .any(|s| s.id == "CAV" && s.get_element(0) == "EHZ")
325                    }),
326            );
327        }
328        let nav = match ctx.navigator() {
329            Some(n) => n,
330            None => return ctx.has_qualified_value("CAV", 0, "EHZ", 0, 0, &["EHZ"]),
331        };
332        let sg8_count = nav.group_instance_count(&["SG4", "SG8"]);
333        for i in 0..sg8_count {
334            let sg10_count = nav.child_group_instance_count(&["SG4", "SG8"], i, "SG10");
335            for j in 0..sg10_count {
336                let ccis = nav.find_segments_in_child_group("CCI", &["SG4", "SG8"], i, "SG10", j);
337                let has_cci_e13 = ccis.iter().any(|s| {
338                    s.elements
339                        .get(2)
340                        .and_then(|e| e.first())
341                        .is_some_and(|v| v == "E13")
342                });
343                if has_cci_e13 {
344                    let cavs =
345                        nav.find_segments_in_child_group("CAV", &["SG4", "SG8"], i, "SG10", j);
346                    if cavs.iter().any(|s| {
347                        s.elements
348                            .first()
349                            .and_then(|e| e.first())
350                            .is_some_and(|v| v == "EHZ")
351                    }) {
352                        return ConditionResult::True;
353                    }
354                }
355            }
356        }
357        ConditionResult::False
358    }
359
360    /// [139] Wenn SG10 CAV+IVA (Individuelle Abstimmung) in dieser SG8 nicht vorhanden
361    // REVIEW: True when no SG10 child of any SG8 contains a CAV with elements[0][0]=='IVA'. Iterates all SG8 instances and their SG10 children; returns False as soon as IVA is found, True if absent throughout. 'In dieser SG8' is evaluated message-wide since no specific parent qualifier filter is given. (medium confidence)
362    fn evaluate_139(&self, ctx: &EvaluationContext) -> ConditionResult {
363        // HAND-EDITED: "in dieser/derselben SG8" — the enclosing SG8 only, when the
364        // scope reaches one; the message as before otherwise.
365        if let Some(scope) = ctx.enclosing_scope("SG8") {
366            return ConditionResult::from(
367                !ctx.find_segments_at(scope, "CAV")
368                    .iter()
369                    .any(|s| s.get_element(0) == "IVA"),
370            );
371        }
372        let nav = match ctx.navigator() {
373            Some(n) => n,
374            None => return ctx.lacks_qualifier("CAV", 0, "IVA"),
375        };
376        let sg8_count = nav.group_instance_count(&["SG4", "SG8"]);
377        for i in 0..sg8_count {
378            let sg10_count = nav.child_group_instance_count(&["SG4", "SG8"], i, "SG10");
379            for j in 0..sg10_count {
380                let cavs = nav.find_segments_in_child_group("CAV", &["SG4", "SG8"], i, "SG10", j);
381                if cavs.iter().any(|s| {
382                    s.elements
383                        .first()
384                        .and_then(|e| e.first())
385                        .is_some_and(|v| v == "IVA")
386                }) {
387                    return ConditionResult::False;
388                }
389            }
390        }
391        ConditionResult::True
392    }
393
394    /// [157] Wenn SG4 STS+7++Z33+ZZB nicht vorhanden
395    fn evaluate_157(&self, ctx: &EvaluationContext) -> ConditionResult {
396        let result = ctx.has_segment_matching("STS", &[(0, 0, "7"), (2, 0, "Z33"), (3, 0, "ZZB")]);
397        match result {
398            ConditionResult::True => ConditionResult::False,
399            ConditionResult::False => ConditionResult::True,
400            ConditionResult::Unknown => ConditionResult::Unknown,
401        }
402    }
403
404    /// [158] Wenn im selben Segment im DE2379 der Code 106 vorhanden ist
405    fn evaluate_158(&self, ctx: &EvaluationContext) -> ConditionResult {
406        ConditionResult::from(ctx.self_segment_value_equals("DTM", 0, 2, "106"))
407    }
408
409    /// [168] Wenn in dieser SG8 im SG8 PIA+Z02 (Gruppenartikel- ID / Artikel-ID), die Artikel-ID 1-02-0-002/ 003/ 004 vorhanden
410    fn evaluate_168(&self, ctx: &EvaluationContext) -> ConditionResult {
411        // HAND-EDITED: "in dieser/derselben SG8" — scoped to the enclosing group.
412        ctx.this_group_has_qualified_value(
413            "PIA",
414            0,
415            "Z02",
416            1,
417            0,
418            &["1-02-0-002", "1-02-0-003", "1-02-0-004"],
419            &["SG4", "SG8"],
420        )
421    }
422
423    /// [169] Wenn zwei SG8 SEQ+Z45 (Netznutzungsabrechnungsdaten der Marktlokation), mit derselben Zeitraum-ID im DE1050, mit Artikel-ID im SG8 PIA+Z02 (Gruppenartikel-ID / Artikel-ID), eine SG8 mit 1-02-0-002 ...
424    // REVIEW: Complex cross-group condition. Collects all SG8 instances having SEQ+Z45 (Netznutzungsabrechnungsdaten), extracting the Zeitraum-ID from SEQ elements[1][0] (DE1050) and the PIA+Z02 article ID from elements[1][0]. Then checks if two such SG8 instances share the same Zeitraum-ID where one has article 1-02-0-002 and the other has 1-02-0-003 or 1-02-0-004. Medium confidence because the article ID string format ('1-02-0-002') must match exactly as stored in the EDIFACT value. (medium confidence)
425    fn evaluate_169(&self, ctx: &EvaluationContext) -> ConditionResult {
426        let nav = match ctx.navigator() {
427            Some(n) => n,
428            None => return ConditionResult::Unknown,
429        };
430        let sg8_count = nav.group_instance_count(&["SG4", "SG8"]);
431        let mut sg8_data: Vec<(String, String)> = Vec::new();
432        for i in 0..sg8_count {
433            let seqs = nav.find_segments_in_group("SEQ", &["SG4", "SG8"], i);
434            let zeitraum_id = seqs
435                .iter()
436                .find(|s| {
437                    s.elements
438                        .first()
439                        .and_then(|e| e.first())
440                        .is_some_and(|v| v == "Z45")
441                })
442                .and_then(|s| s.elements.get(1))
443                .and_then(|e| e.first())
444                .filter(|v| !v.is_empty())
445                .cloned();
446            if let Some(zid) = zeitraum_id {
447                let pias = nav.find_segments_in_group("PIA", &["SG4", "SG8"], i);
448                let article_id = pias
449                    .iter()
450                    .find(|s| {
451                        s.elements
452                            .first()
453                            .and_then(|e| e.first())
454                            .is_some_and(|v| v == "Z02")
455                    })
456                    .and_then(|s| s.elements.get(1))
457                    .and_then(|e| e.first())
458                    .cloned()
459                    .unwrap_or_default();
460                sg8_data.push((zid, article_id));
461            }
462        }
463        for i in 0..sg8_data.len() {
464            for j in (i + 1)..sg8_data.len() {
465                if sg8_data[i].0 == sg8_data[j].0 {
466                    let (id_i, id_j) = (&sg8_data[i].1, &sg8_data[j].1);
467                    let i_is_002 = id_i == "1-02-0-002";
468                    let j_is_002 = id_j == "1-02-0-002";
469                    let i_is_003_004 = id_i == "1-02-0-003" || id_i == "1-02-0-004";
470                    let j_is_003_004 = id_j == "1-02-0-003" || id_j == "1-02-0-004";
471                    if (i_is_002 && j_is_003_004) || (j_is_002 && i_is_003_004) {
472                        return ConditionResult::True;
473                    }
474                }
475            }
476        }
477        if sg8_data.is_empty() {
478            ConditionResult::Unknown
479        } else {
480            ConditionResult::False
481        }
482    }
483
484    /// [242] Wenn mehr als ein SG8 (Referenz auf die Lokationsbündelstruktur) mit der selben Zeitraum-ID im DE1050 vorhanden
485    fn evaluate_242(&self, ctx: &EvaluationContext) -> ConditionResult {
486        let nav = match ctx.navigator() {
487            Some(n) => n,
488            None => return ConditionResult::Unknown,
489        };
490        let sg8_count = nav.group_instance_count(&["SG4", "SG8"]);
491        let mut ids: Vec<String> = Vec::new();
492        for i in 0..sg8_count {
493            let seq_segs = nav.find_segments_in_group("SEQ", &["SG4", "SG8"], i);
494            for seq in &seq_segs {
495                let qual = seq
496                    .elements
497                    .first()
498                    .and_then(|e| e.first())
499                    .map(|s| s.as_str());
500                if matches!(qual, Some("Z78") | Some("ZC7") | Some("ZC8") | Some("ZD5")) {
501                    if let Some(id) = seq.elements.get(1).and_then(|e| e.first()) {
502                        if !id.is_empty() {
503                            ids.push(id.clone());
504                        }
505                    }
506                }
507            }
508        }
509        if ids.is_empty() {
510            return ConditionResult::Unknown;
511        }
512        let mut seen = std::collections::HashSet::new();
513        ConditionResult::from(ids.iter().any(|id| !seen.insert(id.as_str())))
514    }
515
516    /// [253] Wenn SG4 STS+E01++ZC9 (Ablehnung (keine Zuordnung möglich)) nicht vorhanden
517    fn evaluate_253(&self, ctx: &EvaluationContext) -> ConditionResult {
518        match ctx.has_segment_matching("STS", &[(0, 0, "E01"), (2, 0, "ZC9")]) {
519            ConditionResult::True => ConditionResult::False,
520            ConditionResult::False => ConditionResult::True,
521            ConditionResult::Unknown => ConditionResult::Unknown,
522        }
523    }
524
525    /// [258] Wenn in dieser SG8 SEQ+Z01 (Daten der Marktlokation) SG10 CCI+Z30++Z07 (Verbrauch) vorhanden
526    fn evaluate_258(&self, ctx: &EvaluationContext) -> ConditionResult {
527        // HAND-EDITED: "in dieser SG8 SEQ+Z01 SG10 CCI+Z30++Z07" — the enclosing SG8
528        // only, when the scope reaches one.
529        if let (Some(sg8), Some(nav)) = (ctx.enclosing_scope("SG8"), ctx.navigator()) {
530            let is_z01 = nav
531                .find_segments_in_instance("SEQ", sg8)
532                .iter()
533                .any(|s| s.get_element(0) == "Z01");
534            let verbrauch = nav
535                .find_segments_in_subtree("CCI", sg8)
536                .iter()
537                .any(|s| s.get_element(0) == "Z30" && s.get_component(2, 0) == "Z07");
538            return ConditionResult::from(is_z01 && verbrauch);
539        }
540        let nav = match ctx.navigator() {
541            Some(n) => n,
542            None => return ctx.has_qualified_value("CCI", 0, "Z30", 2, 0, &["Z07"]),
543        };
544        let sg8_count = nav.group_instance_count(&["SG4", "SG8"]);
545        for i in 0..sg8_count {
546            let seq_segs = nav.find_segments_in_group("SEQ", &["SG4", "SG8"], i);
547            let has_z01 = seq_segs.iter().any(|s| {
548                s.elements
549                    .first()
550                    .and_then(|e| e.first())
551                    .is_some_and(|v| v == "Z01")
552            });
553            if has_z01 {
554                let sg10_count = nav.child_group_instance_count(&["SG4", "SG8"], i, "SG10");
555                for j in 0..sg10_count {
556                    let ccis =
557                        nav.find_segments_in_child_group("CCI", &["SG4", "SG8"], i, "SG10", j);
558                    if ccis.iter().any(|s| {
559                        s.elements
560                            .first()
561                            .and_then(|e| e.first())
562                            .is_some_and(|v| v == "Z30")
563                            && s.elements
564                                .get(2)
565                                .and_then(|e| e.first())
566                                .is_some_and(|v| v == "Z07")
567                    }) {
568                        return ConditionResult::True;
569                    }
570                }
571            }
572        }
573        ConditionResult::False
574    }
575
576    /// [263] Wenn SG8 SEQ+Z01 (Daten der Marktlokation) SG10 CCI+Z30++Z07 (Verbrauch) vorhanden
577    // Message-wide: any SG8 SEQ+Z01 with an SG10 CCI+Z30++Z07. This is FV2504's [20] text, renumbered.
578    fn evaluate_263(&self, ctx: &EvaluationContext) -> ConditionResult {
579        let nav = match ctx.navigator() {
580            Some(n) => n,
581            None => return ctx.has_qualified_value("CCI", 0, "Z30", 2, 0, &["Z07"]),
582        };
583        let sg8_count = nav.group_instance_count(&["SG4", "SG8"]);
584        for i in 0..sg8_count {
585            let seq_segs = nav.find_segments_in_group("SEQ", &["SG4", "SG8"], i);
586            let has_z01 = seq_segs.iter().any(|s| {
587                s.elements
588                    .first()
589                    .and_then(|e| e.first())
590                    .is_some_and(|v| v == "Z01")
591            });
592            if !has_z01 {
593                continue;
594            }
595            let sg10_count = nav.child_group_instance_count(&["SG4", "SG8"], i, "SG10");
596            for j in 0..sg10_count {
597                let ccis = nav.find_segments_in_child_group("CCI", &["SG4", "SG8"], i, "SG10", j);
598                if ccis.iter().any(|s| {
599                    s.elements
600                        .first()
601                        .and_then(|e| e.first())
602                        .is_some_and(|v| v == "Z30")
603                        && s.elements
604                            .get(2)
605                            .and_then(|e| e.first())
606                            .is_some_and(|v| v == "Z07")
607                }) {
608                    return ConditionResult::True;
609                }
610            }
611        }
612        ConditionResult::False
613    }
614
615    /// [297] Wenn mehr als eine SG8 SEQ+Z02/ ZE3 (OBIS- Daten der Marktlokation) mit einer OBIS-Kennzahl für Energiemenge im PIA+5, mit gleicher Zeitraum-ID und identischem Code im DE1229 des SG8 SEQ+Z02/ZA1/Z...
616    // REVIEW: Find SG8 instances with SEQ qualifier Z02 or ZE3 that also have a PIA+5 segment. Collect (de1229_code, zeitraum_id) tuples. Condition is True when more than one such SG8 shares the same combination. Note: 'OBIS-Kennzahl für Energiemenge' filtering is not enforced here (would require knowledge of valid OBIS patterns), so presence of PIA+5 is used as a proxy. (medium confidence)
617    fn evaluate_297(&self, ctx: &EvaluationContext) -> ConditionResult {
618        let nav = match ctx.navigator() {
619            Some(n) => n,
620            None => return ConditionResult::Unknown,
621        };
622        let sg8_count = nav.group_instance_count(&["SG4", "SG8"]);
623        // Collect (seq_qualifier, zeitraum_id) tuples from SG8 instances with SEQ+Z02/ZE3 that also have PIA+5
624        let mut keys: Vec<(String, String)> = Vec::new();
625        for i in 0..sg8_count {
626            let seq_segs = nav.find_segments_in_group("SEQ", &["SG4", "SG8"], i);
627            for seq in &seq_segs {
628                let qual = seq
629                    .elements
630                    .first()
631                    .and_then(|e| e.first())
632                    .map(|s| s.as_str());
633                if matches!(qual, Some("Z02") | Some("ZE3")) {
634                    let pia_segs = nav.find_segments_in_group("PIA", &["SG4", "SG8"], i);
635                    let has_pia5 = pia_segs.iter().any(|s| {
636                        s.elements
637                            .first()
638                            .and_then(|e| e.first())
639                            .is_some_and(|v| v == "5")
640                    });
641                    if has_pia5 {
642                        let zeitraum_id = seq
643                            .elements
644                            .get(1)
645                            .and_then(|e| e.first())
646                            .cloned()
647                            .unwrap_or_default();
648                        if !zeitraum_id.is_empty() {
649                            keys.push((qual.unwrap_or_default().to_string(), zeitraum_id));
650                        }
651                    }
652                }
653            }
654        }
655        if keys.is_empty() {
656            return ConditionResult::Unknown;
657        }
658        let mut seen = std::collections::HashSet::new();
659        ConditionResult::from(keys.iter().any(|k| !seen.insert(k)))
660    }
661
662    /// [312] Wenn DTM+137 (Nachrichtendatum) im DE2380 &lt; 202603312200?+00
663    fn evaluate_312(&self, ctx: &EvaluationContext) -> ConditionResult {
664        ctx.dtm_lt("137", "202603312200+00")
665    }
666
667    /// [313] Wenn DTM+137 (Nachrichtendatum) im DE2380 ≥ 202603312200?+00
668    fn evaluate_313(&self, ctx: &EvaluationContext) -> ConditionResult {
669        ctx.dtm_ge("137", "202603312200+00")
670    }
671
672    /// [326] Wenn die Marktlokation nicht stillgelegt wird
673    /// EXTERNAL: Requires context from outside the message.
674    fn evaluate_326(&self, ctx: &EvaluationContext) -> ConditionResult {
675        ctx.external.evaluate("market_location_not_decommissioned")
676    }
677
678    /// [328] Wenn die Marktlokation stillgelegt wird
679    /// EXTERNAL: Requires context from outside the message.
680    fn evaluate_328(&self, ctx: &EvaluationContext) -> ConditionResult {
681        ctx.external.evaluate("market_location_decommissioned")
682    }
683
684    /// [346] Wenn SG10 CCI+++E13 (Zählertyp) CAV+MME (mME) in dieser SG8 vorhanden
685    fn evaluate_346(&self, ctx: &EvaluationContext) -> ConditionResult {
686        // HAND-EDITED: "in dieser/derselben SG8" — the enclosing SG8 only, when the
687        // scope reaches one; the message as before otherwise.
688        if let Some(_scope) = ctx.enclosing_scope("SG8") {
689            return ConditionResult::from(
690                ctx.enclosing_child_segments("SG8", "SG10", &["CCI", "CAV"])
691                    .unwrap_or_default()
692                    .iter()
693                    .any(|segs| {
694                        segs.iter()
695                            .any(|s| s.id == "CCI" && s.get_component(2, 0) == "E13")
696                            && segs
697                                .iter()
698                                .any(|s| s.id == "CAV" && s.get_element(0) == "MME")
699                    }),
700            );
701        }
702        let nav = match ctx.navigator() {
703            Some(n) => n,
704            None => {
705                // Fallback: message-wide check for both CCI+E13 and CAV+MME
706                let has_cci = ctx.has_qualifier("CCI", 2, "E13");
707                let has_cav = ctx.has_qualifier("CAV", 0, "MME");
708                return match (has_cci, has_cav) {
709                    (ConditionResult::True, ConditionResult::True) => ConditionResult::True,
710                    (ConditionResult::False, _) | (_, ConditionResult::False) => {
711                        ConditionResult::False
712                    }
713                    _ => ConditionResult::Unknown,
714                };
715            }
716        };
717        let sg8_count = nav.group_instance_count(&["SG4", "SG8"]);
718        for i in 0..sg8_count {
719            let sg10_count = nav.child_group_instance_count(&["SG4", "SG8"], i, "SG10");
720            for j in 0..sg10_count {
721                let ccis = nav.find_segments_in_child_group("CCI", &["SG4", "SG8"], i, "SG10", j);
722                let has_cci_e13 = ccis.iter().any(|s| {
723                    s.elements
724                        .get(2)
725                        .and_then(|e| e.first())
726                        .is_some_and(|v| v == "E13")
727                });
728                if has_cci_e13 {
729                    let cavs =
730                        nav.find_segments_in_child_group("CAV", &["SG4", "SG8"], i, "SG10", j);
731                    if cavs.iter().any(|s| {
732                        s.elements
733                            .first()
734                            .and_then(|e| e.first())
735                            .is_some_and(|v| v == "MME")
736                    }) {
737                        return ConditionResult::True;
738                    }
739                }
740            }
741        }
742        ConditionResult::False
743    }
744
745    /// [413] Wenn eine SG8 SEQ+Z02/ZA1/ZA2 (OBIS-Daten der Marktlokation) mit einer OBIS-Kennzahl für Wirkarbeit und kumuliert im PIA+5, mit gleicher Zeitraum-ID und identischem Code im DE1229 des SG8 SEQ+Z02/...
746    fn evaluate_413(&self, ctx: &EvaluationContext) -> ConditionResult {
747        // At least one SG8 SEQ+{Z02|ZA1|ZA2} with matching DE1229 qualifier
748        // and matching DE1050 Zeitraum-ID exists whose PIA+5 OBIS is
749        // Wirkarbeit-kumuliert (C ∈ {1,2}, D ∈ {8,9}).
750        ConditionResult::from(ctx.count_sg8_obis_matches(is_obis_wirkarbeit_kumuliert) >= 1)
751    }
752
753    /// [414] Wenn keine SG8 SEQ+Z02/ZA1/ZA2 (OBIS-Daten der Marktlokation) mit einer OBIS-Kennzahl für Wirkarbeit und kumuliert im PIA+5, mit gleicher Zeitraum-ID und identischem Code im DE1229 des SG8 SEQ+Z02...
754    fn evaluate_414(&self, ctx: &EvaluationContext) -> ConditionResult {
755        // Negation of [413] — no matching kumuliert SG8 exists.
756        ConditionResult::from(ctx.count_sg8_obis_matches(is_obis_wirkarbeit_kumuliert) == 0)
757    }
758
759    /// [415] Wenn eine weitere SG8 SEQ+Z02/ZA1/ZA2 (OBIS-Daten der Marktlokation) mit einer OBIS-Kennzahl für Wirkarbeit und 1/4 Stunde im PIA+5, mit gleicher Zeitraum-ID und identischem Code im DE1229 des SG8...
760    fn evaluate_415(&self, ctx: &EvaluationContext) -> ConditionResult {
761        // "eine weitere" = at least one BESIDES the current → count >= 2.
762        // OBIS predicate: Wirkarbeit 1/4 Stunde (C ∈ {1,2}, D = 29).
763        ConditionResult::from(ctx.count_sg8_obis_matches(is_obis_wirkarbeit_quarter_hour) >= 2)
764    }
765
766    /// [416] Wenn keine SG8 SEQ+Z02/ZA1/ZA2 (OBIS-Daten der Marktlokation) mit einer OBIS-Kennzahl für Wirkarbeit und 1/4 Stunde im PIA+5, mit gleicher Zeitraum-ID und identischem Code im DE1229 des SG8 SEQ+Z0...
767    fn evaluate_416(&self, ctx: &EvaluationContext) -> ConditionResult {
768        // No SG8 matching current's Zeitraum-ID/qualifier has a 1/4 Stunde
769        // Wirkarbeit OBIS in its PIA+5.
770        ConditionResult::from(ctx.count_sg8_obis_matches(is_obis_wirkarbeit_quarter_hour) == 0)
771    }
772
773    /// [461] Wenn in derselben SG10 das CCI (Art und Nutzung der Technischen Ressource) im CAV+ZG8 (Technischen Ressource fällt unter § 14a EnWG) vorhanden
774    fn evaluate_461(&self, ctx: &EvaluationContext) -> ConditionResult {
775        // HAND-EDITED: "in dieser/derselben SG10" — scoped to the enclosing group.
776        // Wenn in derselben SG10 das CCI (Art und Nutzung der Technischen Ressource)
777        // im CAV+ZG8 (Technische Ressource fällt unter § 14a EnWG) vorhanden
778        // CCI is the SG10 entry segment; condition reduces to: any SG10 has CAV with ZG8
779        ctx.this_group_has_qualifier("CAV", 0, "ZG8", &["SG4", "SG8", "SG10"])
780    }
781
782    /// [534] Hinweis: Aufgrund der Wiederholbarkeit des SG4 STS Status der Antwort ist die Angabe von max. acht Verwendungszeiträumen möglich
783    fn evaluate_534(&self, _ctx: &EvaluationContext) -> ConditionResult {
784        // Hinweis: Aufgrund der Wiederholbarkeit des SG4 STS Status der Antwort ist
785        // die Angabe von max. acht Verwendungszeiträumen möglich
786        // — informational note about cardinality, always applies
787        ConditionResult::True
788    }
789
790    /// [557] Hinweis: Wurden vom Verantwortlichen mehr als acht Verwendungszeiträume gesendet, so werden acht beliebige Verwendungszeiträume beantwortet
791    fn evaluate_557(&self, _ctx: &EvaluationContext) -> ConditionResult {
792        // Hinweis: Wurden vom Verantwortlichen mehr als acht Verwendungszeiträume
793        // gesendet, so werden acht beliebige Verwendungszeiträume beantwortet
794        // — informational processing note, always applies
795        ConditionResult::True
796    }
797
798    /// [560] Hinweis: Dies entspricht dem Geschäftsvorfall A
799    fn evaluate_560(&self, _ctx: &EvaluationContext) -> ConditionResult {
800        // Hinweis: Dies entspricht dem Geschäftsvorfall A — informational note, always applies
801        ConditionResult::True
802    }
803
804    /// [561] Hinweis: Dies entspricht dem Geschäftsvorfall B
805    fn evaluate_561(&self, _ctx: &EvaluationContext) -> ConditionResult {
806        // Hinweis: Dies entspricht dem Geschäftsvorfall B — informational note, always applies
807        ConditionResult::True
808    }
809
810    /// [562] Hinweis: Bei Marktlokationen, welche auf Basis von Werten bilanziert werden, wird der erste Tag des Jahres angegeben.
811    fn evaluate_562(&self, _ctx: &EvaluationContext) -> ConditionResult {
812        // Hinweis: Bei Marktlokationen, welche auf Basis von Werten bilanziert werden, wird der erste Tag des Jahres angegeben — informational note, always applies
813        ConditionResult::True
814    }
815
816    /// [564] Hinweis: Das IDE+Z01 (Liste) definiert den Geschäftsvorfall. Alle aufgelisteten IDE+24 sind Bestandteil des Geschäftsvorfalls
817    fn evaluate_564(&self, _ctx: &EvaluationContext) -> ConditionResult {
818        // Hinweis: Das IDE+Z01 (Liste) definiert den Geschäftsvorfall. Alle aufgelisteten IDE+24 sind Bestandteil des Geschäftsvorfalls — informational note, always applies
819        ConditionResult::True
820    }
821
822    /// [565] Hinweis: Das IDE+24 beschreibt einen Bestandteil einer Liste, wenn ein IDE+Z01 vorhanden ist. Es beschreibt nicht den Geschäftsvorfall
823    fn evaluate_565(&self, _ctx: &EvaluationContext) -> ConditionResult {
824        // Hinweis: Das IDE+24 beschreibt einen Bestandteil einer Liste, wenn ein IDE+Z01 vorhanden ist. Es beschreibt nicht den Geschäftsvorfall — informational note, always applies
825        ConditionResult::True
826    }
827
828    /// [578] Hinweis: Dies gilt für den Zeitraum welcher mit Z53 (keine Daten) definiert ist
829    fn evaluate_578(&self, _ctx: &EvaluationContext) -> ConditionResult {
830        // Hinweis: Dies gilt für den Zeitraum welcher mit Z53 (keine Daten) definiert ist — informational note, always applies
831        ConditionResult::True
832    }
833
834    /// [681] Hinweis: Übergangsversorgung gibt es nur bei Marktlokationen, die unter § 38a EnWG fallen. Grundlage ist eine bilaterale Vereinbarung
835    fn evaluate_681(&self, _ctx: &EvaluationContext) -> ConditionResult {
836        // Hinweis: Übergangsversorgung applies only to Marktlokationen under § 38a EnWG based on bilateral agreement — informational note, always applies
837        ConditionResult::True
838    }
839
840    /// [686] Hinweis: Zu verwenden bei der Abmeldung der Übergangsversorgung.
841    fn evaluate_686(&self, _ctx: &EvaluationContext) -> ConditionResult {
842        // Hinweis: Zu verwenden bei der Abmeldung der Übergangsversorgung — informational note, always applies
843        ConditionResult::True
844    }
845
846    /// [2000] Je SG5 LOC+Z16 (Marktlokation) ist genau einmal die Segmentgruppe anzugeben
847    // REVIEW: Checks that per SG4 transaction group, exactly one SG5 child group contains a LOC segment with qualifier Z16 (Marktlokation). Uses parent-child navigator to iterate SG4 instances and count LOC+Z16 occurrences in their SG5 children. Falls back to message-wide count when no navigator is available. (medium confidence)
848    fn evaluate_2000(&self, ctx: &EvaluationContext) -> ConditionResult {
849        // Je SG5 LOC+Z16 (Marktlokation) ist genau einmal die Segmentgruppe anzugeben
850        // Per SG4 transaction group, exactly one SG5 instance should contain LOC+Z16
851        let nav = match ctx.navigator() {
852            Some(n) => n,
853            None => {
854                let count = ctx.count_qualified_in_group("LOC", 0, "Z16", &["SG4", "SG5"]);
855                return ConditionResult::from(count == 1);
856            }
857        };
858        let sg4_count = nav.group_instance_count(&["SG4"]);
859        if sg4_count == 0 {
860            return ConditionResult::Unknown;
861        }
862        for i in 0..sg4_count {
863            let sg5_count = nav.child_group_instance_count(&["SG4"], i, "SG5");
864            let mut loc_z16_count = 0usize;
865            for j in 0..sg5_count {
866                let locs = nav.find_segments_in_child_group("LOC", &["SG4"], i, "SG5", j);
867                for loc in &locs {
868                    if loc
869                        .elements
870                        .first()
871                        .and_then(|e| e.first())
872                        .is_some_and(|v| v == "Z16")
873                    {
874                        loc_z16_count += 1;
875                    }
876                }
877            }
878            if loc_z16_count != 1 {
879                return ConditionResult::False;
880            }
881        }
882        ConditionResult::True
883    }
884
885    /// [2016] Segmentgruppe ist mind. einmal für jede Zeitraum-ID aus dem DE1156 der SG6 RFF+Z49 / Z53 (Verwendungszeitraum der Daten: "Gültige Daten" / "Keine Daten") aus der Anfragennachricht aus SG6 RFF+TN ...
886    // REVIEW: Cross-group Zeitraum-ID correlation: collect DE1156 values from SG6 RFF+Z49/Z53, verify each is covered by at least one SG8 SEQ's DE1050 reference. The mention of 'Anfragenachricht' (request message) and RFF+TN adds ambiguity about whether the source is this message or an external one; implemented as intra-message check on the most direct interpretation. (medium confidence)
887    fn evaluate_2016(&self, ctx: &EvaluationContext) -> ConditionResult {
888        // Segmentgruppe ist mind. einmal für jede Zeitraum-ID aus DE1156 der SG6 RFF+Z49/Z53 anzugeben
889        // Collect Zeitraum-IDs from SG6 RFF+Z49 and Z53 (DE1156 = elements[0][2])
890        // Then verify each required Zeitraum-ID is referenced by at least one SG8 SEQ (DE1050 = elements[1][0])
891        let nav = match ctx.navigator() {
892            Some(n) => n,
893            None => return ConditionResult::Unknown,
894        };
895        let sg6_count = nav.group_instance_count(&["SG4", "SG6"]);
896        let mut required_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
897        for i in 0..sg6_count {
898            let rffs = nav.find_segments_in_group("RFF", &["SG4", "SG6"], i);
899            for rff in &rffs {
900                let qual = rff
901                    .elements
902                    .first()
903                    .and_then(|e| e.first())
904                    .map(|s| s.as_str());
905                if matches!(qual, Some("Z49") | Some("Z53")) {
906                    if let Some(zeitraum_id) = rff.elements.first().and_then(|e| e.get(2)) {
907                        if !zeitraum_id.is_empty() {
908                            required_ids.insert(zeitraum_id.clone());
909                        }
910                    }
911                }
912            }
913        }
914        if required_ids.is_empty() {
915            return ConditionResult::Unknown;
916        }
917        // Check that for each required Zeitraum-ID at least one SG8 SEQ references it via DE1050
918        let sg8_count = nav.group_instance_count(&["SG4", "SG8"]);
919        let mut found_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
920        for i in 0..sg8_count {
921            let seqs = nav.find_segments_in_group("SEQ", &["SG4", "SG8"], i);
922            for seq in &seqs {
923                if let Some(ref_id) = seq.elements.get(1).and_then(|e| e.first()) {
924                    if !ref_id.is_empty() {
925                        found_ids.insert(ref_id.clone());
926                    }
927                }
928            }
929        }
930        ConditionResult::from(required_ids.iter().all(|id| found_ids.contains(id)))
931    }
932
933    /// [2121] Segmentgruppe ist genau einmal je SG8 SEQ+Z01/ Z80/Z81 (Daten der Marktlokation/ Erwartete Daten der Marktlokation/ Im System vorhandene Daten der Marktlokation) anzugeben
934    // REVIEW: Cardinality condition: this segment group must appear exactly once per qualifying SG8 (those with SEQ+Z01/Z80/Z81 — Daten/Erwartete Daten/Im System vorhandene Daten der Marktlokation). As a boolean predicate the condition evaluates True when such qualifying SG8 instances exist, signalling that the subject segment group is required. Full cardinality validation (exactly-once enforcement) requires the broader validator framework. (medium confidence)
935    fn evaluate_2121(&self, ctx: &EvaluationContext) -> ConditionResult {
936        // Segmentgruppe ist genau einmal je SG8 SEQ+Z01/Z80/Z81 anzugeben
937        // True when at least one SG8 with SEQ+Z01, Z80, or Z81 exists (cardinality trigger)
938        ctx.any_group_has_any_qualifier("SEQ", 0, &["Z01", "Z80", "Z81"], &["SG4", "SG8"])
939    }
940}