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        let nav = match ctx.navigator() {
163            Some(n) => n,
164            None => {
165                let sts_segs = ctx.find_segments_with_qualifier("STS", 0, "E01");
166                let found = sts_segs.iter().any(|s| {
167                    let code = s
168                        .elements
169                        .get(2)
170                        .and_then(|e| e.first())
171                        .map(|v| v.as_str());
172                    let list = s.elements.get(2).and_then(|e| e.get(1)).map(|v| v.as_str());
173                    (code == Some("A04") && list == Some("E_0017"))
174                        || (code == Some("A03") && list == Some("E_0052"))
175                });
176                return ConditionResult::from(!found);
177            }
178        };
179        let sg4_count = nav.group_instance_count(&["SG4"]);
180        for i in 0..sg4_count {
181            let ide_segs = nav.find_segments_in_group("IDE", &["SG4"], i);
182            let has_ide_24 = ide_segs.iter().any(|s| {
183                s.elements
184                    .first()
185                    .and_then(|e| e.first())
186                    .is_some_and(|v| v == "24")
187            });
188            if !has_ide_24 {
189                continue;
190            }
191            let sts_segs = nav.find_segments_in_group("STS", &["SG4"], i);
192            let found = sts_segs.iter().any(|s| {
193                let qual = s
194                    .elements
195                    .first()
196                    .and_then(|e| e.first())
197                    .map(|v| v.as_str());
198                let code = s
199                    .elements
200                    .get(2)
201                    .and_then(|e| e.first())
202                    .map(|v| v.as_str());
203                let list = s.elements.get(2).and_then(|e| e.get(1)).map(|v| v.as_str());
204                qual == Some("E01")
205                    && ((code == Some("A04") && list == Some("E_0017"))
206                        || (code == Some("A03") && list == Some("E_0052")))
207            });
208            if !found {
209                return ConditionResult::True;
210            }
211        }
212        ConditionResult::False
213    }
214
215    /// [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
216    // 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)
217    fn evaluate_29(&self, ctx: &EvaluationContext) -> ConditionResult {
218        let nav = match ctx.navigator() {
219            Some(n) => n,
220            None => {
221                let sts_segs = ctx.find_segments_with_qualifier("STS", 0, "E01");
222                let found = sts_segs.iter().any(|s| {
223                    let code = s
224                        .elements
225                        .get(2)
226                        .and_then(|e| e.first())
227                        .map(|v| v.as_str());
228                    let list = s.elements.get(2).and_then(|e| e.get(1)).map(|v| v.as_str());
229                    (code == Some("A06") && list == Some("E_0017"))
230                        || (code == Some("A05") && list == Some("E_0052"))
231                });
232                return ConditionResult::from(!found);
233            }
234        };
235        let sg4_count = nav.group_instance_count(&["SG4"]);
236        for i in 0..sg4_count {
237            let ide_segs = nav.find_segments_in_group("IDE", &["SG4"], i);
238            let has_ide_24 = ide_segs.iter().any(|s| {
239                s.elements
240                    .first()
241                    .and_then(|e| e.first())
242                    .is_some_and(|v| v == "24")
243            });
244            if !has_ide_24 {
245                continue;
246            }
247            let sts_segs = nav.find_segments_in_group("STS", &["SG4"], i);
248            let found = sts_segs.iter().any(|s| {
249                let qual = s
250                    .elements
251                    .first()
252                    .and_then(|e| e.first())
253                    .map(|v| v.as_str());
254                let code = s
255                    .elements
256                    .get(2)
257                    .and_then(|e| e.first())
258                    .map(|v| v.as_str());
259                let list = s.elements.get(2).and_then(|e| e.get(1)).map(|v| v.as_str());
260                qual == Some("E01")
261                    && ((code == Some("A06") && list == Some("E_0017"))
262                        || (code == Some("A05") && list == Some("E_0052")))
263            });
264            if !found {
265                return ConditionResult::True;
266            }
267        }
268        ConditionResult::False
269    }
270
271    /// [79] Wenn SG4 STS+7++Z33 (Auszug wegen Stilllegung) vorhanden
272    fn evaluate_79(&self, ctx: &EvaluationContext) -> ConditionResult {
273        // STS+7++Z33: elements[0][0]="7" (Transaktionsgrund), elements[2][0]="Z33" (Auszug wegen Stilllegung).
274        // FV2504's text read STS+7+++Z33 (element 3); since the 2026-03-02 AHB it is element 2, as in [157].
275        ctx.has_segment_matching("STS", &[(0, 0, "7"), (2, 0, "Z33")])
276    }
277
278    /// [136] Wenn SG10 CCI+++E13 (Zählertyp) CAV+EHZ (elektronischer Haushaltszähler) in dieser SG8 vorhanden
279    fn evaluate_136(&self, ctx: &EvaluationContext) -> ConditionResult {
280        let nav = match ctx.navigator() {
281            Some(n) => n,
282            None => return ctx.has_qualified_value("CAV", 0, "EHZ", 0, 0, &["EHZ"]),
283        };
284        let sg8_count = nav.group_instance_count(&["SG4", "SG8"]);
285        for i in 0..sg8_count {
286            let sg10_count = nav.child_group_instance_count(&["SG4", "SG8"], i, "SG10");
287            for j in 0..sg10_count {
288                let ccis = nav.find_segments_in_child_group("CCI", &["SG4", "SG8"], i, "SG10", j);
289                let has_cci_e13 = ccis.iter().any(|s| {
290                    s.elements
291                        .get(2)
292                        .and_then(|e| e.first())
293                        .is_some_and(|v| v == "E13")
294                });
295                if has_cci_e13 {
296                    let cavs =
297                        nav.find_segments_in_child_group("CAV", &["SG4", "SG8"], i, "SG10", j);
298                    if cavs.iter().any(|s| {
299                        s.elements
300                            .first()
301                            .and_then(|e| e.first())
302                            .is_some_and(|v| v == "EHZ")
303                    }) {
304                        return ConditionResult::True;
305                    }
306                }
307            }
308        }
309        ConditionResult::False
310    }
311
312    /// [139] Wenn SG10 CAV+IVA (Individuelle Abstimmung) in dieser SG8 nicht vorhanden
313    // 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)
314    fn evaluate_139(&self, ctx: &EvaluationContext) -> ConditionResult {
315        let nav = match ctx.navigator() {
316            Some(n) => n,
317            None => return ctx.lacks_qualifier("CAV", 0, "IVA"),
318        };
319        let sg8_count = nav.group_instance_count(&["SG4", "SG8"]);
320        for i in 0..sg8_count {
321            let sg10_count = nav.child_group_instance_count(&["SG4", "SG8"], i, "SG10");
322            for j in 0..sg10_count {
323                let cavs = nav.find_segments_in_child_group("CAV", &["SG4", "SG8"], i, "SG10", j);
324                if cavs.iter().any(|s| {
325                    s.elements
326                        .first()
327                        .and_then(|e| e.first())
328                        .is_some_and(|v| v == "IVA")
329                }) {
330                    return ConditionResult::False;
331                }
332            }
333        }
334        ConditionResult::True
335    }
336
337    /// [157] Wenn SG4 STS+7++Z33+ZZB nicht vorhanden
338    fn evaluate_157(&self, ctx: &EvaluationContext) -> ConditionResult {
339        let result = ctx.has_segment_matching("STS", &[(0, 0, "7"), (2, 0, "Z33"), (3, 0, "ZZB")]);
340        match result {
341            ConditionResult::True => ConditionResult::False,
342            ConditionResult::False => ConditionResult::True,
343            ConditionResult::Unknown => ConditionResult::Unknown,
344        }
345    }
346
347    /// [158] Wenn im selben Segment im DE2379 der Code 106 vorhanden ist
348    fn evaluate_158(&self, ctx: &EvaluationContext) -> ConditionResult {
349        ConditionResult::from(ctx.self_segment_value_equals("DTM", 0, 2, "106"))
350    }
351
352    /// [168] Wenn in dieser SG8 im SG8 PIA+Z02 (Gruppenartikel- ID / Artikel-ID), die Artikel-ID 1-02-0-002/ 003/ 004 vorhanden
353    fn evaluate_168(&self, ctx: &EvaluationContext) -> ConditionResult {
354        // HAND-EDITED: "in dieser/derselben SG8" — scoped to the enclosing group.
355        ctx.this_group_has_qualified_value(
356            "PIA",
357            0,
358            "Z02",
359            1,
360            0,
361            &["1-02-0-002", "1-02-0-003", "1-02-0-004"],
362            &["SG4", "SG8"],
363        )
364    }
365
366    /// [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 ...
367    // 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)
368    fn evaluate_169(&self, ctx: &EvaluationContext) -> ConditionResult {
369        let nav = match ctx.navigator() {
370            Some(n) => n,
371            None => return ConditionResult::Unknown,
372        };
373        let sg8_count = nav.group_instance_count(&["SG4", "SG8"]);
374        let mut sg8_data: Vec<(String, String)> = Vec::new();
375        for i in 0..sg8_count {
376            let seqs = nav.find_segments_in_group("SEQ", &["SG4", "SG8"], i);
377            let zeitraum_id = seqs
378                .iter()
379                .find(|s| {
380                    s.elements
381                        .first()
382                        .and_then(|e| e.first())
383                        .is_some_and(|v| v == "Z45")
384                })
385                .and_then(|s| s.elements.get(1))
386                .and_then(|e| e.first())
387                .filter(|v| !v.is_empty())
388                .cloned();
389            if let Some(zid) = zeitraum_id {
390                let pias = nav.find_segments_in_group("PIA", &["SG4", "SG8"], i);
391                let article_id = pias
392                    .iter()
393                    .find(|s| {
394                        s.elements
395                            .first()
396                            .and_then(|e| e.first())
397                            .is_some_and(|v| v == "Z02")
398                    })
399                    .and_then(|s| s.elements.get(1))
400                    .and_then(|e| e.first())
401                    .cloned()
402                    .unwrap_or_default();
403                sg8_data.push((zid, article_id));
404            }
405        }
406        for i in 0..sg8_data.len() {
407            for j in (i + 1)..sg8_data.len() {
408                if sg8_data[i].0 == sg8_data[j].0 {
409                    let (id_i, id_j) = (&sg8_data[i].1, &sg8_data[j].1);
410                    let i_is_002 = id_i == "1-02-0-002";
411                    let j_is_002 = id_j == "1-02-0-002";
412                    let i_is_003_004 = id_i == "1-02-0-003" || id_i == "1-02-0-004";
413                    let j_is_003_004 = id_j == "1-02-0-003" || id_j == "1-02-0-004";
414                    if (i_is_002 && j_is_003_004) || (j_is_002 && i_is_003_004) {
415                        return ConditionResult::True;
416                    }
417                }
418            }
419        }
420        if sg8_data.is_empty() {
421            ConditionResult::Unknown
422        } else {
423            ConditionResult::False
424        }
425    }
426
427    /// [242] Wenn mehr als ein SG8 (Referenz auf die Lokationsbündelstruktur) mit der selben Zeitraum-ID im DE1050 vorhanden
428    fn evaluate_242(&self, ctx: &EvaluationContext) -> ConditionResult {
429        let nav = match ctx.navigator() {
430            Some(n) => n,
431            None => return ConditionResult::Unknown,
432        };
433        let sg8_count = nav.group_instance_count(&["SG4", "SG8"]);
434        let mut ids: Vec<String> = Vec::new();
435        for i in 0..sg8_count {
436            let seq_segs = nav.find_segments_in_group("SEQ", &["SG4", "SG8"], i);
437            for seq in &seq_segs {
438                let qual = seq
439                    .elements
440                    .first()
441                    .and_then(|e| e.first())
442                    .map(|s| s.as_str());
443                if matches!(qual, Some("Z78") | Some("ZC7") | Some("ZC8") | Some("ZD5")) {
444                    if let Some(id) = seq.elements.get(1).and_then(|e| e.first()) {
445                        if !id.is_empty() {
446                            ids.push(id.clone());
447                        }
448                    }
449                }
450            }
451        }
452        if ids.is_empty() {
453            return ConditionResult::Unknown;
454        }
455        let mut seen = std::collections::HashSet::new();
456        ConditionResult::from(ids.iter().any(|id| !seen.insert(id.as_str())))
457    }
458
459    /// [253] Wenn SG4 STS+E01++ZC9 (Ablehnung (keine Zuordnung möglich)) nicht vorhanden
460    fn evaluate_253(&self, ctx: &EvaluationContext) -> ConditionResult {
461        match ctx.has_segment_matching("STS", &[(0, 0, "E01"), (2, 0, "ZC9")]) {
462            ConditionResult::True => ConditionResult::False,
463            ConditionResult::False => ConditionResult::True,
464            ConditionResult::Unknown => ConditionResult::Unknown,
465        }
466    }
467
468    /// [258] Wenn in dieser SG8 SEQ+Z01 (Daten der Marktlokation) SG10 CCI+Z30++Z07 (Verbrauch) vorhanden
469    fn evaluate_258(&self, ctx: &EvaluationContext) -> ConditionResult {
470        let nav = match ctx.navigator() {
471            Some(n) => n,
472            None => return ctx.has_qualified_value("CCI", 0, "Z30", 2, 0, &["Z07"]),
473        };
474        let sg8_count = nav.group_instance_count(&["SG4", "SG8"]);
475        for i in 0..sg8_count {
476            let seq_segs = nav.find_segments_in_group("SEQ", &["SG4", "SG8"], i);
477            let has_z01 = seq_segs.iter().any(|s| {
478                s.elements
479                    .first()
480                    .and_then(|e| e.first())
481                    .is_some_and(|v| v == "Z01")
482            });
483            if has_z01 {
484                let sg10_count = nav.child_group_instance_count(&["SG4", "SG8"], i, "SG10");
485                for j in 0..sg10_count {
486                    let ccis =
487                        nav.find_segments_in_child_group("CCI", &["SG4", "SG8"], i, "SG10", j);
488                    if ccis.iter().any(|s| {
489                        s.elements
490                            .first()
491                            .and_then(|e| e.first())
492                            .is_some_and(|v| v == "Z30")
493                            && s.elements
494                                .get(2)
495                                .and_then(|e| e.first())
496                                .is_some_and(|v| v == "Z07")
497                    }) {
498                        return ConditionResult::True;
499                    }
500                }
501            }
502        }
503        ConditionResult::False
504    }
505
506    /// [263] Wenn SG8 SEQ+Z01 (Daten der Marktlokation) SG10 CCI+Z30++Z07 (Verbrauch) vorhanden
507    // Message-wide: any SG8 SEQ+Z01 with an SG10 CCI+Z30++Z07. This is FV2504's [20] text, renumbered.
508    fn evaluate_263(&self, ctx: &EvaluationContext) -> ConditionResult {
509        let nav = match ctx.navigator() {
510            Some(n) => n,
511            None => return ctx.has_qualified_value("CCI", 0, "Z30", 2, 0, &["Z07"]),
512        };
513        let sg8_count = nav.group_instance_count(&["SG4", "SG8"]);
514        for i in 0..sg8_count {
515            let seq_segs = nav.find_segments_in_group("SEQ", &["SG4", "SG8"], i);
516            let has_z01 = seq_segs.iter().any(|s| {
517                s.elements
518                    .first()
519                    .and_then(|e| e.first())
520                    .is_some_and(|v| v == "Z01")
521            });
522            if !has_z01 {
523                continue;
524            }
525            let sg10_count = nav.child_group_instance_count(&["SG4", "SG8"], i, "SG10");
526            for j in 0..sg10_count {
527                let ccis = nav.find_segments_in_child_group("CCI", &["SG4", "SG8"], i, "SG10", j);
528                if ccis.iter().any(|s| {
529                    s.elements
530                        .first()
531                        .and_then(|e| e.first())
532                        .is_some_and(|v| v == "Z30")
533                        && s.elements
534                            .get(2)
535                            .and_then(|e| e.first())
536                            .is_some_and(|v| v == "Z07")
537                }) {
538                    return ConditionResult::True;
539                }
540            }
541        }
542        ConditionResult::False
543    }
544
545    /// [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...
546    // 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)
547    fn evaluate_297(&self, ctx: &EvaluationContext) -> ConditionResult {
548        let nav = match ctx.navigator() {
549            Some(n) => n,
550            None => return ConditionResult::Unknown,
551        };
552        let sg8_count = nav.group_instance_count(&["SG4", "SG8"]);
553        // Collect (seq_qualifier, zeitraum_id) tuples from SG8 instances with SEQ+Z02/ZE3 that also have PIA+5
554        let mut keys: Vec<(String, String)> = Vec::new();
555        for i in 0..sg8_count {
556            let seq_segs = nav.find_segments_in_group("SEQ", &["SG4", "SG8"], i);
557            for seq in &seq_segs {
558                let qual = seq
559                    .elements
560                    .first()
561                    .and_then(|e| e.first())
562                    .map(|s| s.as_str());
563                if matches!(qual, Some("Z02") | Some("ZE3")) {
564                    let pia_segs = nav.find_segments_in_group("PIA", &["SG4", "SG8"], i);
565                    let has_pia5 = pia_segs.iter().any(|s| {
566                        s.elements
567                            .first()
568                            .and_then(|e| e.first())
569                            .is_some_and(|v| v == "5")
570                    });
571                    if has_pia5 {
572                        let zeitraum_id = seq
573                            .elements
574                            .get(1)
575                            .and_then(|e| e.first())
576                            .cloned()
577                            .unwrap_or_default();
578                        if !zeitraum_id.is_empty() {
579                            keys.push((qual.unwrap_or_default().to_string(), zeitraum_id));
580                        }
581                    }
582                }
583            }
584        }
585        if keys.is_empty() {
586            return ConditionResult::Unknown;
587        }
588        let mut seen = std::collections::HashSet::new();
589        ConditionResult::from(keys.iter().any(|k| !seen.insert(k)))
590    }
591
592    /// [312] Wenn DTM+137 (Nachrichtendatum) im DE2380 &lt; 202603312200?+00
593    fn evaluate_312(&self, ctx: &EvaluationContext) -> ConditionResult {
594        ctx.dtm_lt("137", "202603312200+00")
595    }
596
597    /// [313] Wenn DTM+137 (Nachrichtendatum) im DE2380 ≥ 202603312200?+00
598    fn evaluate_313(&self, ctx: &EvaluationContext) -> ConditionResult {
599        ctx.dtm_ge("137", "202603312200+00")
600    }
601
602    /// [326] Wenn die Marktlokation nicht stillgelegt wird
603    /// EXTERNAL: Requires context from outside the message.
604    fn evaluate_326(&self, ctx: &EvaluationContext) -> ConditionResult {
605        ctx.external.evaluate("market_location_not_decommissioned")
606    }
607
608    /// [328] Wenn die Marktlokation stillgelegt wird
609    /// EXTERNAL: Requires context from outside the message.
610    fn evaluate_328(&self, ctx: &EvaluationContext) -> ConditionResult {
611        ctx.external.evaluate("market_location_decommissioned")
612    }
613
614    /// [346] Wenn SG10 CCI+++E13 (Zählertyp) CAV+MME (mME) in dieser SG8 vorhanden
615    fn evaluate_346(&self, ctx: &EvaluationContext) -> ConditionResult {
616        let nav = match ctx.navigator() {
617            Some(n) => n,
618            None => {
619                // Fallback: message-wide check for both CCI+E13 and CAV+MME
620                let has_cci = ctx.has_qualifier("CCI", 2, "E13");
621                let has_cav = ctx.has_qualifier("CAV", 0, "MME");
622                return match (has_cci, has_cav) {
623                    (ConditionResult::True, ConditionResult::True) => ConditionResult::True,
624                    (ConditionResult::False, _) | (_, ConditionResult::False) => {
625                        ConditionResult::False
626                    }
627                    _ => ConditionResult::Unknown,
628                };
629            }
630        };
631        let sg8_count = nav.group_instance_count(&["SG4", "SG8"]);
632        for i in 0..sg8_count {
633            let sg10_count = nav.child_group_instance_count(&["SG4", "SG8"], i, "SG10");
634            for j in 0..sg10_count {
635                let ccis = nav.find_segments_in_child_group("CCI", &["SG4", "SG8"], i, "SG10", j);
636                let has_cci_e13 = ccis.iter().any(|s| {
637                    s.elements
638                        .get(2)
639                        .and_then(|e| e.first())
640                        .is_some_and(|v| v == "E13")
641                });
642                if has_cci_e13 {
643                    let cavs =
644                        nav.find_segments_in_child_group("CAV", &["SG4", "SG8"], i, "SG10", j);
645                    if cavs.iter().any(|s| {
646                        s.elements
647                            .first()
648                            .and_then(|e| e.first())
649                            .is_some_and(|v| v == "MME")
650                    }) {
651                        return ConditionResult::True;
652                    }
653                }
654            }
655        }
656        ConditionResult::False
657    }
658
659    /// [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/...
660    fn evaluate_413(&self, ctx: &EvaluationContext) -> ConditionResult {
661        // At least one SG8 SEQ+{Z02|ZA1|ZA2} with matching DE1229 qualifier
662        // and matching DE1050 Zeitraum-ID exists whose PIA+5 OBIS is
663        // Wirkarbeit-kumuliert (C ∈ {1,2}, D ∈ {8,9}).
664        ConditionResult::from(ctx.count_sg8_obis_matches(is_obis_wirkarbeit_kumuliert) >= 1)
665    }
666
667    /// [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...
668    fn evaluate_414(&self, ctx: &EvaluationContext) -> ConditionResult {
669        // Negation of [413] — no matching kumuliert SG8 exists.
670        ConditionResult::from(ctx.count_sg8_obis_matches(is_obis_wirkarbeit_kumuliert) == 0)
671    }
672
673    /// [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...
674    fn evaluate_415(&self, ctx: &EvaluationContext) -> ConditionResult {
675        // "eine weitere" = at least one BESIDES the current → count >= 2.
676        // OBIS predicate: Wirkarbeit 1/4 Stunde (C ∈ {1,2}, D = 29).
677        ConditionResult::from(ctx.count_sg8_obis_matches(is_obis_wirkarbeit_quarter_hour) >= 2)
678    }
679
680    /// [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...
681    fn evaluate_416(&self, ctx: &EvaluationContext) -> ConditionResult {
682        // No SG8 matching current's Zeitraum-ID/qualifier has a 1/4 Stunde
683        // Wirkarbeit OBIS in its PIA+5.
684        ConditionResult::from(ctx.count_sg8_obis_matches(is_obis_wirkarbeit_quarter_hour) == 0)
685    }
686
687    /// [461] Wenn in derselben SG10 das CCI (Art und Nutzung der Technischen Ressource) im CAV+ZG8 (Technischen Ressource fällt unter § 14a EnWG) vorhanden
688    fn evaluate_461(&self, ctx: &EvaluationContext) -> ConditionResult {
689        // HAND-EDITED: "in dieser/derselben SG10" — scoped to the enclosing group.
690        // Wenn in derselben SG10 das CCI (Art und Nutzung der Technischen Ressource)
691        // im CAV+ZG8 (Technische Ressource fällt unter § 14a EnWG) vorhanden
692        // CCI is the SG10 entry segment; condition reduces to: any SG10 has CAV with ZG8
693        ctx.this_group_has_qualifier("CAV", 0, "ZG8", &["SG4", "SG8", "SG10"])
694    }
695
696    /// [534] Hinweis: Aufgrund der Wiederholbarkeit des SG4 STS Status der Antwort ist die Angabe von max. acht Verwendungszeiträumen möglich
697    fn evaluate_534(&self, _ctx: &EvaluationContext) -> ConditionResult {
698        // Hinweis: Aufgrund der Wiederholbarkeit des SG4 STS Status der Antwort ist
699        // die Angabe von max. acht Verwendungszeiträumen möglich
700        // — informational note about cardinality, always applies
701        ConditionResult::True
702    }
703
704    /// [557] Hinweis: Wurden vom Verantwortlichen mehr als acht Verwendungszeiträume gesendet, so werden acht beliebige Verwendungszeiträume beantwortet
705    fn evaluate_557(&self, _ctx: &EvaluationContext) -> ConditionResult {
706        // Hinweis: Wurden vom Verantwortlichen mehr als acht Verwendungszeiträume
707        // gesendet, so werden acht beliebige Verwendungszeiträume beantwortet
708        // — informational processing note, always applies
709        ConditionResult::True
710    }
711
712    /// [560] Hinweis: Dies entspricht dem Geschäftsvorfall A
713    fn evaluate_560(&self, _ctx: &EvaluationContext) -> ConditionResult {
714        // Hinweis: Dies entspricht dem Geschäftsvorfall A — informational note, always applies
715        ConditionResult::True
716    }
717
718    /// [561] Hinweis: Dies entspricht dem Geschäftsvorfall B
719    fn evaluate_561(&self, _ctx: &EvaluationContext) -> ConditionResult {
720        // Hinweis: Dies entspricht dem Geschäftsvorfall B — informational note, always applies
721        ConditionResult::True
722    }
723
724    /// [562] Hinweis: Bei Marktlokationen, welche auf Basis von Werten bilanziert werden, wird der erste Tag des Jahres angegeben.
725    fn evaluate_562(&self, _ctx: &EvaluationContext) -> ConditionResult {
726        // Hinweis: Bei Marktlokationen, welche auf Basis von Werten bilanziert werden, wird der erste Tag des Jahres angegeben — informational note, always applies
727        ConditionResult::True
728    }
729
730    /// [564] Hinweis: Das IDE+Z01 (Liste) definiert den Geschäftsvorfall. Alle aufgelisteten IDE+24 sind Bestandteil des Geschäftsvorfalls
731    fn evaluate_564(&self, _ctx: &EvaluationContext) -> ConditionResult {
732        // Hinweis: Das IDE+Z01 (Liste) definiert den Geschäftsvorfall. Alle aufgelisteten IDE+24 sind Bestandteil des Geschäftsvorfalls — informational note, always applies
733        ConditionResult::True
734    }
735
736    /// [565] Hinweis: Das IDE+24 beschreibt einen Bestandteil einer Liste, wenn ein IDE+Z01 vorhanden ist. Es beschreibt nicht den Geschäftsvorfall
737    fn evaluate_565(&self, _ctx: &EvaluationContext) -> ConditionResult {
738        // 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
739        ConditionResult::True
740    }
741
742    /// [578] Hinweis: Dies gilt für den Zeitraum welcher mit Z53 (keine Daten) definiert ist
743    fn evaluate_578(&self, _ctx: &EvaluationContext) -> ConditionResult {
744        // Hinweis: Dies gilt für den Zeitraum welcher mit Z53 (keine Daten) definiert ist — informational note, always applies
745        ConditionResult::True
746    }
747
748    /// [681] Hinweis: Übergangsversorgung gibt es nur bei Marktlokationen, die unter § 38a EnWG fallen. Grundlage ist eine bilaterale Vereinbarung
749    fn evaluate_681(&self, _ctx: &EvaluationContext) -> ConditionResult {
750        // Hinweis: Übergangsversorgung applies only to Marktlokationen under § 38a EnWG based on bilateral agreement — informational note, always applies
751        ConditionResult::True
752    }
753
754    /// [686] Hinweis: Zu verwenden bei der Abmeldung der Übergangsversorgung.
755    fn evaluate_686(&self, _ctx: &EvaluationContext) -> ConditionResult {
756        // Hinweis: Zu verwenden bei der Abmeldung der Übergangsversorgung — informational note, always applies
757        ConditionResult::True
758    }
759
760    /// [2000] Je SG5 LOC+Z16 (Marktlokation) ist genau einmal die Segmentgruppe anzugeben
761    // 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)
762    fn evaluate_2000(&self, ctx: &EvaluationContext) -> ConditionResult {
763        // Je SG5 LOC+Z16 (Marktlokation) ist genau einmal die Segmentgruppe anzugeben
764        // Per SG4 transaction group, exactly one SG5 instance should contain LOC+Z16
765        let nav = match ctx.navigator() {
766            Some(n) => n,
767            None => {
768                let count = ctx.count_qualified_in_group("LOC", 0, "Z16", &["SG4", "SG5"]);
769                return ConditionResult::from(count == 1);
770            }
771        };
772        let sg4_count = nav.group_instance_count(&["SG4"]);
773        if sg4_count == 0 {
774            return ConditionResult::Unknown;
775        }
776        for i in 0..sg4_count {
777            let sg5_count = nav.child_group_instance_count(&["SG4"], i, "SG5");
778            let mut loc_z16_count = 0usize;
779            for j in 0..sg5_count {
780                let locs = nav.find_segments_in_child_group("LOC", &["SG4"], i, "SG5", j);
781                for loc in &locs {
782                    if loc
783                        .elements
784                        .first()
785                        .and_then(|e| e.first())
786                        .is_some_and(|v| v == "Z16")
787                    {
788                        loc_z16_count += 1;
789                    }
790                }
791            }
792            if loc_z16_count != 1 {
793                return ConditionResult::False;
794            }
795        }
796        ConditionResult::True
797    }
798
799    /// [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 ...
800    // 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)
801    fn evaluate_2016(&self, ctx: &EvaluationContext) -> ConditionResult {
802        // Segmentgruppe ist mind. einmal für jede Zeitraum-ID aus DE1156 der SG6 RFF+Z49/Z53 anzugeben
803        // Collect Zeitraum-IDs from SG6 RFF+Z49 and Z53 (DE1156 = elements[0][2])
804        // Then verify each required Zeitraum-ID is referenced by at least one SG8 SEQ (DE1050 = elements[1][0])
805        let nav = match ctx.navigator() {
806            Some(n) => n,
807            None => return ConditionResult::Unknown,
808        };
809        let sg6_count = nav.group_instance_count(&["SG4", "SG6"]);
810        let mut required_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
811        for i in 0..sg6_count {
812            let rffs = nav.find_segments_in_group("RFF", &["SG4", "SG6"], i);
813            for rff in &rffs {
814                let qual = rff
815                    .elements
816                    .first()
817                    .and_then(|e| e.first())
818                    .map(|s| s.as_str());
819                if matches!(qual, Some("Z49") | Some("Z53")) {
820                    if let Some(zeitraum_id) = rff.elements.first().and_then(|e| e.get(2)) {
821                        if !zeitraum_id.is_empty() {
822                            required_ids.insert(zeitraum_id.clone());
823                        }
824                    }
825                }
826            }
827        }
828        if required_ids.is_empty() {
829            return ConditionResult::Unknown;
830        }
831        // Check that for each required Zeitraum-ID at least one SG8 SEQ references it via DE1050
832        let sg8_count = nav.group_instance_count(&["SG4", "SG8"]);
833        let mut found_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
834        for i in 0..sg8_count {
835            let seqs = nav.find_segments_in_group("SEQ", &["SG4", "SG8"], i);
836            for seq in &seqs {
837                if let Some(ref_id) = seq.elements.get(1).and_then(|e| e.first()) {
838                    if !ref_id.is_empty() {
839                        found_ids.insert(ref_id.clone());
840                    }
841                }
842            }
843        }
844        ConditionResult::from(required_ids.iter().all(|id| found_ids.contains(id)))
845    }
846
847    /// [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
848    // 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)
849    fn evaluate_2121(&self, ctx: &EvaluationContext) -> ConditionResult {
850        // Segmentgruppe ist genau einmal je SG8 SEQ+Z01/Z80/Z81 anzugeben
851        // True when at least one SG8 with SEQ+Z01, Z80, or Z81 exists (cardinality trigger)
852        ctx.any_group_has_any_qualifier("SEQ", 0, &["Z01", "Z80", "Z81"], &["SG4", "SG8"])
853    }
854}