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        ctx.any_group_has_qualified_value(
355            "PIA",
356            0,
357            "Z02",
358            1,
359            0,
360            &["1-02-0-002", "1-02-0-003", "1-02-0-004"],
361            &["SG4", "SG8"],
362        )
363    }
364
365    /// [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 ...
366    // 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)
367    fn evaluate_169(&self, ctx: &EvaluationContext) -> ConditionResult {
368        let nav = match ctx.navigator() {
369            Some(n) => n,
370            None => return ConditionResult::Unknown,
371        };
372        let sg8_count = nav.group_instance_count(&["SG4", "SG8"]);
373        let mut sg8_data: Vec<(String, String)> = Vec::new();
374        for i in 0..sg8_count {
375            let seqs = nav.find_segments_in_group("SEQ", &["SG4", "SG8"], i);
376            let zeitraum_id = seqs
377                .iter()
378                .find(|s| {
379                    s.elements
380                        .first()
381                        .and_then(|e| e.first())
382                        .is_some_and(|v| v == "Z45")
383                })
384                .and_then(|s| s.elements.get(1))
385                .and_then(|e| e.first())
386                .filter(|v| !v.is_empty())
387                .cloned();
388            if let Some(zid) = zeitraum_id {
389                let pias = nav.find_segments_in_group("PIA", &["SG4", "SG8"], i);
390                let article_id = pias
391                    .iter()
392                    .find(|s| {
393                        s.elements
394                            .first()
395                            .and_then(|e| e.first())
396                            .is_some_and(|v| v == "Z02")
397                    })
398                    .and_then(|s| s.elements.get(1))
399                    .and_then(|e| e.first())
400                    .cloned()
401                    .unwrap_or_default();
402                sg8_data.push((zid, article_id));
403            }
404        }
405        for i in 0..sg8_data.len() {
406            for j in (i + 1)..sg8_data.len() {
407                if sg8_data[i].0 == sg8_data[j].0 {
408                    let (id_i, id_j) = (&sg8_data[i].1, &sg8_data[j].1);
409                    let i_is_002 = id_i == "1-02-0-002";
410                    let j_is_002 = id_j == "1-02-0-002";
411                    let i_is_003_004 = id_i == "1-02-0-003" || id_i == "1-02-0-004";
412                    let j_is_003_004 = id_j == "1-02-0-003" || id_j == "1-02-0-004";
413                    if (i_is_002 && j_is_003_004) || (j_is_002 && i_is_003_004) {
414                        return ConditionResult::True;
415                    }
416                }
417            }
418        }
419        if sg8_data.is_empty() {
420            ConditionResult::Unknown
421        } else {
422            ConditionResult::False
423        }
424    }
425
426    /// [242] Wenn mehr als ein SG8 (Referenz auf die Lokationsbündelstruktur) mit der selben Zeitraum-ID im DE1050 vorhanden
427    fn evaluate_242(&self, ctx: &EvaluationContext) -> ConditionResult {
428        let nav = match ctx.navigator() {
429            Some(n) => n,
430            None => return ConditionResult::Unknown,
431        };
432        let sg8_count = nav.group_instance_count(&["SG4", "SG8"]);
433        let mut ids: Vec<String> = Vec::new();
434        for i in 0..sg8_count {
435            let seq_segs = nav.find_segments_in_group("SEQ", &["SG4", "SG8"], i);
436            for seq in &seq_segs {
437                let qual = seq
438                    .elements
439                    .first()
440                    .and_then(|e| e.first())
441                    .map(|s| s.as_str());
442                if matches!(qual, Some("Z78") | Some("ZC7") | Some("ZC8") | Some("ZD5")) {
443                    if let Some(id) = seq.elements.get(1).and_then(|e| e.first()) {
444                        if !id.is_empty() {
445                            ids.push(id.clone());
446                        }
447                    }
448                }
449            }
450        }
451        if ids.is_empty() {
452            return ConditionResult::Unknown;
453        }
454        let mut seen = std::collections::HashSet::new();
455        ConditionResult::from(ids.iter().any(|id| !seen.insert(id.as_str())))
456    }
457
458    /// [253] Wenn SG4 STS+E01++ZC9 (Ablehnung (keine Zuordnung möglich)) nicht vorhanden
459    fn evaluate_253(&self, ctx: &EvaluationContext) -> ConditionResult {
460        match ctx.has_segment_matching("STS", &[(0, 0, "E01"), (2, 0, "ZC9")]) {
461            ConditionResult::True => ConditionResult::False,
462            ConditionResult::False => ConditionResult::True,
463            ConditionResult::Unknown => ConditionResult::Unknown,
464        }
465    }
466
467    /// [258] Wenn in dieser SG8 SEQ+Z01 (Daten der Marktlokation) SG10 CCI+Z30++Z07 (Verbrauch) vorhanden
468    fn evaluate_258(&self, ctx: &EvaluationContext) -> ConditionResult {
469        let nav = match ctx.navigator() {
470            Some(n) => n,
471            None => return ctx.has_qualified_value("CCI", 0, "Z30", 2, 0, &["Z07"]),
472        };
473        let sg8_count = nav.group_instance_count(&["SG4", "SG8"]);
474        for i in 0..sg8_count {
475            let seq_segs = nav.find_segments_in_group("SEQ", &["SG4", "SG8"], i);
476            let has_z01 = seq_segs.iter().any(|s| {
477                s.elements
478                    .first()
479                    .and_then(|e| e.first())
480                    .is_some_and(|v| v == "Z01")
481            });
482            if has_z01 {
483                let sg10_count = nav.child_group_instance_count(&["SG4", "SG8"], i, "SG10");
484                for j in 0..sg10_count {
485                    let ccis =
486                        nav.find_segments_in_child_group("CCI", &["SG4", "SG8"], i, "SG10", j);
487                    if ccis.iter().any(|s| {
488                        s.elements
489                            .first()
490                            .and_then(|e| e.first())
491                            .is_some_and(|v| v == "Z30")
492                            && s.elements
493                                .get(2)
494                                .and_then(|e| e.first())
495                                .is_some_and(|v| v == "Z07")
496                    }) {
497                        return ConditionResult::True;
498                    }
499                }
500            }
501        }
502        ConditionResult::False
503    }
504
505    /// [263] Wenn SG8 SEQ+Z01 (Daten der Marktlokation) SG10 CCI+Z30++Z07 (Verbrauch) vorhanden
506    // Message-wide: any SG8 SEQ+Z01 with an SG10 CCI+Z30++Z07. This is FV2504's [20] text, renumbered.
507    fn evaluate_263(&self, ctx: &EvaluationContext) -> ConditionResult {
508        let nav = match ctx.navigator() {
509            Some(n) => n,
510            None => return ctx.has_qualified_value("CCI", 0, "Z30", 2, 0, &["Z07"]),
511        };
512        let sg8_count = nav.group_instance_count(&["SG4", "SG8"]);
513        for i in 0..sg8_count {
514            let seq_segs = nav.find_segments_in_group("SEQ", &["SG4", "SG8"], i);
515            let has_z01 = seq_segs.iter().any(|s| {
516                s.elements
517                    .first()
518                    .and_then(|e| e.first())
519                    .is_some_and(|v| v == "Z01")
520            });
521            if !has_z01 {
522                continue;
523            }
524            let sg10_count = nav.child_group_instance_count(&["SG4", "SG8"], i, "SG10");
525            for j in 0..sg10_count {
526                let ccis = nav.find_segments_in_child_group("CCI", &["SG4", "SG8"], i, "SG10", j);
527                if ccis.iter().any(|s| {
528                    s.elements
529                        .first()
530                        .and_then(|e| e.first())
531                        .is_some_and(|v| v == "Z30")
532                        && s.elements
533                            .get(2)
534                            .and_then(|e| e.first())
535                            .is_some_and(|v| v == "Z07")
536                }) {
537                    return ConditionResult::True;
538                }
539            }
540        }
541        ConditionResult::False
542    }
543
544    /// [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...
545    // 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)
546    fn evaluate_297(&self, ctx: &EvaluationContext) -> ConditionResult {
547        let nav = match ctx.navigator() {
548            Some(n) => n,
549            None => return ConditionResult::Unknown,
550        };
551        let sg8_count = nav.group_instance_count(&["SG4", "SG8"]);
552        // Collect (seq_qualifier, zeitraum_id) tuples from SG8 instances with SEQ+Z02/ZE3 that also have PIA+5
553        let mut keys: Vec<(String, String)> = Vec::new();
554        for i in 0..sg8_count {
555            let seq_segs = nav.find_segments_in_group("SEQ", &["SG4", "SG8"], i);
556            for seq in &seq_segs {
557                let qual = seq
558                    .elements
559                    .first()
560                    .and_then(|e| e.first())
561                    .map(|s| s.as_str());
562                if matches!(qual, Some("Z02") | Some("ZE3")) {
563                    let pia_segs = nav.find_segments_in_group("PIA", &["SG4", "SG8"], i);
564                    let has_pia5 = pia_segs.iter().any(|s| {
565                        s.elements
566                            .first()
567                            .and_then(|e| e.first())
568                            .is_some_and(|v| v == "5")
569                    });
570                    if has_pia5 {
571                        let zeitraum_id = seq
572                            .elements
573                            .get(1)
574                            .and_then(|e| e.first())
575                            .cloned()
576                            .unwrap_or_default();
577                        if !zeitraum_id.is_empty() {
578                            keys.push((qual.unwrap_or_default().to_string(), zeitraum_id));
579                        }
580                    }
581                }
582            }
583        }
584        if keys.is_empty() {
585            return ConditionResult::Unknown;
586        }
587        let mut seen = std::collections::HashSet::new();
588        ConditionResult::from(keys.iter().any(|k| !seen.insert(k)))
589    }
590
591    /// [312] Wenn DTM+137 (Nachrichtendatum) im DE2380 &lt; 202603312200?+00
592    fn evaluate_312(&self, ctx: &EvaluationContext) -> ConditionResult {
593        ctx.dtm_lt("137", "202603312200+00")
594    }
595
596    /// [313] Wenn DTM+137 (Nachrichtendatum) im DE2380 ≥ 202603312200?+00
597    fn evaluate_313(&self, ctx: &EvaluationContext) -> ConditionResult {
598        ctx.dtm_ge("137", "202603312200+00")
599    }
600
601    /// [326] Wenn die Marktlokation nicht stillgelegt wird
602    /// EXTERNAL: Requires context from outside the message.
603    fn evaluate_326(&self, ctx: &EvaluationContext) -> ConditionResult {
604        ctx.external.evaluate("market_location_not_decommissioned")
605    }
606
607    /// [328] Wenn die Marktlokation stillgelegt wird
608    /// EXTERNAL: Requires context from outside the message.
609    fn evaluate_328(&self, ctx: &EvaluationContext) -> ConditionResult {
610        ctx.external.evaluate("market_location_decommissioned")
611    }
612
613    /// [346] Wenn SG10 CCI+++E13 (Zählertyp) CAV+MME (mME) in dieser SG8 vorhanden
614    fn evaluate_346(&self, ctx: &EvaluationContext) -> ConditionResult {
615        let nav = match ctx.navigator() {
616            Some(n) => n,
617            None => {
618                // Fallback: message-wide check for both CCI+E13 and CAV+MME
619                let has_cci = ctx.has_qualifier("CCI", 2, "E13");
620                let has_cav = ctx.has_qualifier("CAV", 0, "MME");
621                return match (has_cci, has_cav) {
622                    (ConditionResult::True, ConditionResult::True) => ConditionResult::True,
623                    (ConditionResult::False, _) | (_, ConditionResult::False) => {
624                        ConditionResult::False
625                    }
626                    _ => ConditionResult::Unknown,
627                };
628            }
629        };
630        let sg8_count = nav.group_instance_count(&["SG4", "SG8"]);
631        for i in 0..sg8_count {
632            let sg10_count = nav.child_group_instance_count(&["SG4", "SG8"], i, "SG10");
633            for j in 0..sg10_count {
634                let ccis = nav.find_segments_in_child_group("CCI", &["SG4", "SG8"], i, "SG10", j);
635                let has_cci_e13 = ccis.iter().any(|s| {
636                    s.elements
637                        .get(2)
638                        .and_then(|e| e.first())
639                        .is_some_and(|v| v == "E13")
640                });
641                if has_cci_e13 {
642                    let cavs =
643                        nav.find_segments_in_child_group("CAV", &["SG4", "SG8"], i, "SG10", j);
644                    if cavs.iter().any(|s| {
645                        s.elements
646                            .first()
647                            .and_then(|e| e.first())
648                            .is_some_and(|v| v == "MME")
649                    }) {
650                        return ConditionResult::True;
651                    }
652                }
653            }
654        }
655        ConditionResult::False
656    }
657
658    /// [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/...
659    fn evaluate_413(&self, ctx: &EvaluationContext) -> ConditionResult {
660        // At least one SG8 SEQ+{Z02|ZA1|ZA2} with matching DE1229 qualifier
661        // and matching DE1050 Zeitraum-ID exists whose PIA+5 OBIS is
662        // Wirkarbeit-kumuliert (C ∈ {1,2}, D ∈ {8,9}).
663        ConditionResult::from(ctx.count_sg8_obis_matches(is_obis_wirkarbeit_kumuliert) >= 1)
664    }
665
666    /// [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...
667    fn evaluate_414(&self, ctx: &EvaluationContext) -> ConditionResult {
668        // Negation of [413] — no matching kumuliert SG8 exists.
669        ConditionResult::from(ctx.count_sg8_obis_matches(is_obis_wirkarbeit_kumuliert) == 0)
670    }
671
672    /// [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...
673    fn evaluate_415(&self, ctx: &EvaluationContext) -> ConditionResult {
674        // "eine weitere" = at least one BESIDES the current → count >= 2.
675        // OBIS predicate: Wirkarbeit 1/4 Stunde (C ∈ {1,2}, D = 29).
676        ConditionResult::from(ctx.count_sg8_obis_matches(is_obis_wirkarbeit_quarter_hour) >= 2)
677    }
678
679    /// [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...
680    fn evaluate_416(&self, ctx: &EvaluationContext) -> ConditionResult {
681        // No SG8 matching current's Zeitraum-ID/qualifier has a 1/4 Stunde
682        // Wirkarbeit OBIS in its PIA+5.
683        ConditionResult::from(ctx.count_sg8_obis_matches(is_obis_wirkarbeit_quarter_hour) == 0)
684    }
685
686    /// [461] Wenn in derselben SG10 das CCI (Art und Nutzung der Technischen Ressource) im CAV+ZG8 (Technischen Ressource fällt unter § 14a EnWG) vorhanden
687    fn evaluate_461(&self, ctx: &EvaluationContext) -> ConditionResult {
688        // Wenn in derselben SG10 das CCI (Art und Nutzung der Technischen Ressource)
689        // im CAV+ZG8 (Technische Ressource fällt unter § 14a EnWG) vorhanden
690        // CCI is the SG10 entry segment; condition reduces to: any SG10 has CAV with ZG8
691        ctx.any_group_has_qualifier("CAV", 0, "ZG8", &["SG4", "SG8", "SG10"])
692    }
693
694    /// [534] Hinweis: Aufgrund der Wiederholbarkeit des SG4 STS Status der Antwort ist die Angabe von max. acht Verwendungszeiträumen möglich
695    fn evaluate_534(&self, _ctx: &EvaluationContext) -> ConditionResult {
696        // Hinweis: Aufgrund der Wiederholbarkeit des SG4 STS Status der Antwort ist
697        // die Angabe von max. acht Verwendungszeiträumen möglich
698        // — informational note about cardinality, always applies
699        ConditionResult::True
700    }
701
702    /// [557] Hinweis: Wurden vom Verantwortlichen mehr als acht Verwendungszeiträume gesendet, so werden acht beliebige Verwendungszeiträume beantwortet
703    fn evaluate_557(&self, _ctx: &EvaluationContext) -> ConditionResult {
704        // Hinweis: Wurden vom Verantwortlichen mehr als acht Verwendungszeiträume
705        // gesendet, so werden acht beliebige Verwendungszeiträume beantwortet
706        // — informational processing note, always applies
707        ConditionResult::True
708    }
709
710    /// [560] Hinweis: Dies entspricht dem Geschäftsvorfall A
711    fn evaluate_560(&self, _ctx: &EvaluationContext) -> ConditionResult {
712        // Hinweis: Dies entspricht dem Geschäftsvorfall A — informational note, always applies
713        ConditionResult::True
714    }
715
716    /// [561] Hinweis: Dies entspricht dem Geschäftsvorfall B
717    fn evaluate_561(&self, _ctx: &EvaluationContext) -> ConditionResult {
718        // Hinweis: Dies entspricht dem Geschäftsvorfall B — informational note, always applies
719        ConditionResult::True
720    }
721
722    /// [562] Hinweis: Bei Marktlokationen, welche auf Basis von Werten bilanziert werden, wird der erste Tag des Jahres angegeben.
723    fn evaluate_562(&self, _ctx: &EvaluationContext) -> ConditionResult {
724        // Hinweis: Bei Marktlokationen, welche auf Basis von Werten bilanziert werden, wird der erste Tag des Jahres angegeben — informational note, always applies
725        ConditionResult::True
726    }
727
728    /// [564] Hinweis: Das IDE+Z01 (Liste) definiert den Geschäftsvorfall. Alle aufgelisteten IDE+24 sind Bestandteil des Geschäftsvorfalls
729    fn evaluate_564(&self, _ctx: &EvaluationContext) -> ConditionResult {
730        // Hinweis: Das IDE+Z01 (Liste) definiert den Geschäftsvorfall. Alle aufgelisteten IDE+24 sind Bestandteil des Geschäftsvorfalls — informational note, always applies
731        ConditionResult::True
732    }
733
734    /// [565] Hinweis: Das IDE+24 beschreibt einen Bestandteil einer Liste, wenn ein IDE+Z01 vorhanden ist. Es beschreibt nicht den Geschäftsvorfall
735    fn evaluate_565(&self, _ctx: &EvaluationContext) -> ConditionResult {
736        // 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
737        ConditionResult::True
738    }
739
740    /// [578] Hinweis: Dies gilt für den Zeitraum welcher mit Z53 (keine Daten) definiert ist
741    fn evaluate_578(&self, _ctx: &EvaluationContext) -> ConditionResult {
742        // Hinweis: Dies gilt für den Zeitraum welcher mit Z53 (keine Daten) definiert ist — informational note, always applies
743        ConditionResult::True
744    }
745
746    /// [681] Hinweis: Übergangsversorgung gibt es nur bei Marktlokationen, die unter § 38a EnWG fallen. Grundlage ist eine bilaterale Vereinbarung
747    fn evaluate_681(&self, _ctx: &EvaluationContext) -> ConditionResult {
748        // Hinweis: Übergangsversorgung applies only to Marktlokationen under § 38a EnWG based on bilateral agreement — informational note, always applies
749        ConditionResult::True
750    }
751
752    /// [686] Hinweis: Zu verwenden bei der Abmeldung der Übergangsversorgung.
753    fn evaluate_686(&self, _ctx: &EvaluationContext) -> ConditionResult {
754        // Hinweis: Zu verwenden bei der Abmeldung der Übergangsversorgung — informational note, always applies
755        ConditionResult::True
756    }
757
758    /// [2000] Je SG5 LOC+Z16 (Marktlokation) ist genau einmal die Segmentgruppe anzugeben
759    // 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)
760    fn evaluate_2000(&self, ctx: &EvaluationContext) -> ConditionResult {
761        // Je SG5 LOC+Z16 (Marktlokation) ist genau einmal die Segmentgruppe anzugeben
762        // Per SG4 transaction group, exactly one SG5 instance should contain LOC+Z16
763        let nav = match ctx.navigator() {
764            Some(n) => n,
765            None => {
766                let count = ctx.count_qualified_in_group("LOC", 0, "Z16", &["SG4", "SG5"]);
767                return ConditionResult::from(count == 1);
768            }
769        };
770        let sg4_count = nav.group_instance_count(&["SG4"]);
771        if sg4_count == 0 {
772            return ConditionResult::Unknown;
773        }
774        for i in 0..sg4_count {
775            let sg5_count = nav.child_group_instance_count(&["SG4"], i, "SG5");
776            let mut loc_z16_count = 0usize;
777            for j in 0..sg5_count {
778                let locs = nav.find_segments_in_child_group("LOC", &["SG4"], i, "SG5", j);
779                for loc in &locs {
780                    if loc
781                        .elements
782                        .first()
783                        .and_then(|e| e.first())
784                        .is_some_and(|v| v == "Z16")
785                    {
786                        loc_z16_count += 1;
787                    }
788                }
789            }
790            if loc_z16_count != 1 {
791                return ConditionResult::False;
792            }
793        }
794        ConditionResult::True
795    }
796
797    /// [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 ...
798    // 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)
799    fn evaluate_2016(&self, ctx: &EvaluationContext) -> ConditionResult {
800        // Segmentgruppe ist mind. einmal für jede Zeitraum-ID aus DE1156 der SG6 RFF+Z49/Z53 anzugeben
801        // Collect Zeitraum-IDs from SG6 RFF+Z49 and Z53 (DE1156 = elements[0][2])
802        // Then verify each required Zeitraum-ID is referenced by at least one SG8 SEQ (DE1050 = elements[1][0])
803        let nav = match ctx.navigator() {
804            Some(n) => n,
805            None => return ConditionResult::Unknown,
806        };
807        let sg6_count = nav.group_instance_count(&["SG4", "SG6"]);
808        let mut required_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
809        for i in 0..sg6_count {
810            let rffs = nav.find_segments_in_group("RFF", &["SG4", "SG6"], i);
811            for rff in &rffs {
812                let qual = rff
813                    .elements
814                    .first()
815                    .and_then(|e| e.first())
816                    .map(|s| s.as_str());
817                if matches!(qual, Some("Z49") | Some("Z53")) {
818                    if let Some(zeitraum_id) = rff.elements.first().and_then(|e| e.get(2)) {
819                        if !zeitraum_id.is_empty() {
820                            required_ids.insert(zeitraum_id.clone());
821                        }
822                    }
823                }
824            }
825        }
826        if required_ids.is_empty() {
827            return ConditionResult::Unknown;
828        }
829        // Check that for each required Zeitraum-ID at least one SG8 SEQ references it via DE1050
830        let sg8_count = nav.group_instance_count(&["SG4", "SG8"]);
831        let mut found_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
832        for i in 0..sg8_count {
833            let seqs = nav.find_segments_in_group("SEQ", &["SG4", "SG8"], i);
834            for seq in &seqs {
835                if let Some(ref_id) = seq.elements.get(1).and_then(|e| e.first()) {
836                    if !ref_id.is_empty() {
837                        found_ids.insert(ref_id.clone());
838                    }
839                }
840            }
841        }
842        ConditionResult::from(required_ids.iter().all(|id| found_ids.contains(id)))
843    }
844
845    /// [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
846    // 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)
847    fn evaluate_2121(&self, ctx: &EvaluationContext) -> ConditionResult {
848        // Segmentgruppe ist genau einmal je SG8 SEQ+Z01/Z80/Z81 anzugeben
849        // True when at least one SG8 with SEQ+Z01, Z80, or Z81 exists (cardinality trigger)
850        ctx.any_group_has_any_qualifier("SEQ", 0, &["Z01", "Z80", "Z81"], &["SG4", "SG8"])
851    }
852}