Skip to main content

automapper_validation/generated/fv2510/
pricat_conditions_fv2510.rs

1// <auto-generated>
2// Generated by automapper-generator generate-conditions
3// AHB: xml-migs-and-ahbs/FV2510/PRICAT_AHB_2_0f_Fehlerkorrektur_20251211.xml
4// Generated: 2026-03-12T10:50:19Z
5// </auto-generated>
6
7#[allow(unused_imports)]
8use crate::eval::format_validators::*;
9use crate::eval::{ConditionEvaluator, ConditionResult, EvaluationContext};
10
11/// Generated condition evaluator for PRICAT FV2510.
12pub struct PricatConditionEvaluatorFV2510 {
13    // External condition IDs that require runtime context.
14    external_conditions: std::collections::HashSet<u32>,
15}
16
17impl Default for PricatConditionEvaluatorFV2510 {
18    fn default() -> Self {
19        let mut external_conditions = std::collections::HashSet::new();
20        external_conditions.insert(1);
21        external_conditions.insert(8);
22        external_conditions.insert(14);
23        external_conditions.insert(19);
24        external_conditions.insert(22);
25        external_conditions.insert(30);
26        external_conditions.insert(35);
27        external_conditions.insert(36);
28        external_conditions.insert(39);
29        external_conditions.insert(40);
30        external_conditions.insert(41);
31        external_conditions.insert(42);
32        external_conditions.insert(45);
33        external_conditions.insert(46);
34        external_conditions.insert(54);
35        external_conditions.insert(60);
36        external_conditions.insert(63);
37        external_conditions.insert(68);
38        external_conditions.insert(69);
39        external_conditions.insert(70);
40        external_conditions.insert(492);
41        Self {
42            external_conditions,
43        }
44    }
45}
46
47impl ConditionEvaluator for PricatConditionEvaluatorFV2510 {
48    fn message_type(&self) -> &str {
49        "PRICAT"
50    }
51
52    fn format_version(&self) -> &str {
53        "FV2510"
54    }
55
56    fn evaluate(&self, condition: u32, ctx: &EvaluationContext) -> ConditionResult {
57        match condition {
58            1 => self.evaluate_1(ctx),
59            2 => self.evaluate_2(ctx),
60            3 => self.evaluate_3(ctx),
61            4 => self.evaluate_4(ctx),
62            5 => self.evaluate_5(ctx),
63            6 => self.evaluate_6(ctx),
64            7 => self.evaluate_7(ctx),
65            8 => self.evaluate_8(ctx),
66            9 => self.evaluate_9(ctx),
67            10 => self.evaluate_10(ctx),
68            12 => self.evaluate_12(ctx),
69            14 => self.evaluate_14(ctx),
70            19 => self.evaluate_19(ctx),
71            22 => self.evaluate_22(ctx),
72            24 => self.evaluate_24(ctx),
73            26 => self.evaluate_26(ctx),
74            27 => self.evaluate_27(ctx),
75            28 => self.evaluate_28(ctx),
76            29 => self.evaluate_29(ctx),
77            30 => self.evaluate_30(ctx),
78            31 => self.evaluate_31(ctx),
79            32 => self.evaluate_32(ctx),
80            33 => self.evaluate_33(ctx),
81            34 => self.evaluate_34(ctx),
82            35 => self.evaluate_35(ctx),
83            36 => self.evaluate_36(ctx),
84            37 => self.evaluate_37(ctx),
85            38 => self.evaluate_38(ctx),
86            39 => self.evaluate_39(ctx),
87            40 => self.evaluate_40(ctx),
88            41 => self.evaluate_41(ctx),
89            42 => self.evaluate_42(ctx),
90            43 => self.evaluate_43(ctx),
91            44 => self.evaluate_44(ctx),
92            45 => self.evaluate_45(ctx),
93            46 => self.evaluate_46(ctx),
94            47 => self.evaluate_47(ctx),
95            48 => self.evaluate_48(ctx),
96            49 => self.evaluate_49(ctx),
97            50 => self.evaluate_50(ctx),
98            51 => self.evaluate_51(ctx),
99            52 => self.evaluate_52(ctx),
100            53 => self.evaluate_53(ctx),
101            54 => self.evaluate_54(ctx),
102            55 => self.evaluate_55(ctx),
103            56 => self.evaluate_56(ctx),
104            57 => self.evaluate_57(ctx),
105            60 => self.evaluate_60(ctx),
106            61 => self.evaluate_61(ctx),
107            62 => self.evaluate_62(ctx),
108            63 => self.evaluate_63(ctx),
109            64 => self.evaluate_64(ctx),
110            65 => self.evaluate_65(ctx),
111            66 => self.evaluate_66(ctx),
112            67 => self.evaluate_67(ctx),
113            68 => self.evaluate_68(ctx),
114            69 => self.evaluate_69(ctx),
115            70 => self.evaluate_70(ctx),
116            71 => self.evaluate_71(ctx),
117            72 => self.evaluate_72(ctx),
118            490 => self.evaluate_490(ctx),
119            491 => self.evaluate_491(ctx),
120            492 => self.evaluate_492(ctx),
121            494 => self.evaluate_494(ctx),
122            495 => self.evaluate_495(ctx),
123            502 => self.evaluate_502(ctx),
124            503 => self.evaluate_503(ctx),
125            504 => self.evaluate_504(ctx),
126            511 => self.evaluate_511(ctx),
127            512 => self.evaluate_512(ctx),
128            513 => self.evaluate_513(ctx),
129            519 => self.evaluate_519(ctx),
130            520 => self.evaluate_520(ctx),
131            521 => self.evaluate_521(ctx),
132            522 => self.evaluate_522(ctx),
133            902 => self.evaluate_902(ctx),
134            908 => self.evaluate_908(ctx),
135            909 => self.evaluate_909(ctx),
136            911 => self.evaluate_911(ctx),
137            912 => self.evaluate_912(ctx),
138            926 => self.evaluate_926(ctx),
139            929 => self.evaluate_929(ctx),
140            931 => self.evaluate_931(ctx),
141            932 => self.evaluate_932(ctx),
142            933 => self.evaluate_933(ctx),
143            937 => self.evaluate_937(ctx),
144            939 => self.evaluate_939(ctx),
145            940 => self.evaluate_940(ctx),
146            941 => self.evaluate_941(ctx),
147            942 => self.evaluate_942(ctx),
148            946 => self.evaluate_946(ctx),
149            948 => self.evaluate_948(ctx),
150            949 => self.evaluate_949(ctx),
151            957 => self.evaluate_957(ctx),
152            959 => self.evaluate_959(ctx),
153            968 => self.evaluate_968(ctx),
154            _ => ConditionResult::Unknown,
155        }
156    }
157
158    fn is_external(&self, condition: u32) -> bool {
159        self.external_conditions.contains(&condition)
160    }
161    fn is_known(&self, condition: u32) -> bool {
162        matches!(
163            condition,
164            1 | 2
165                | 3
166                | 4
167                | 5
168                | 6
169                | 7
170                | 8
171                | 9
172                | 10
173                | 12
174                | 14
175                | 19
176                | 22
177                | 24
178                | 26
179                | 27
180                | 28
181                | 29
182                | 30
183                | 31
184                | 32
185                | 33
186                | 34
187                | 35
188                | 36
189                | 37
190                | 38
191                | 39
192                | 40
193                | 41
194                | 42
195                | 43
196                | 44
197                | 45
198                | 46
199                | 47
200                | 48
201                | 49
202                | 50
203                | 51
204                | 52
205                | 53
206                | 54
207                | 55
208                | 56
209                | 57
210                | 60
211                | 61
212                | 62
213                | 63
214                | 64
215                | 65
216                | 66
217                | 67
218                | 68
219                | 69
220                | 70
221                | 71
222                | 72
223                | 490
224                | 491
225                | 492
226                | 494
227                | 495
228                | 502
229                | 503
230                | 504
231                | 511
232                | 512
233                | 513
234                | 519
235                | 520
236                | 521
237                | 522
238                | 902
239                | 908
240                | 909
241                | 911
242                | 912
243                | 926
244                | 929
245                | 931
246                | 932
247                | 933
248                | 937
249                | 939
250                | 940
251                | 941
252                | 942
253                | 946
254                | 948
255                | 949
256                | 957
257                | 959
258                | 968
259        )
260    }
261}
262
263impl PricatConditionEvaluatorFV2510 {
264    /// [8] Wenn das in DE1001 angegebene Preisblatt vom NB nicht genutzt wird.
265    /// EXTERNAL: Requires context from outside the message.
266    fn evaluate_8(&self, ctx: &EvaluationContext) -> ConditionResult {
267        ctx.external.evaluate("nb_does_not_use_price_list")
268    }
269
270    /// [10] Wenn eine weitere SG36 vorhanden ist, bei der sich der Inhalt von LIN DE7140 von LIN DE7140 dieser SG36 nur in der Ziffer nach dem letzten "-" unterscheidet und die Ziffer dort größer ist als in ...
271    // REVIEW: Requires iterating all SG36 instances, extracting LIN DE7140 (elements[2][0]), splitting on the last '-', and checking whether any other SG36 has the same prefix with a strictly higher numeric suffix. This is the 'Gruppenartikel-ID / Artikel-ID' relationship. Group path SG17→SG36 based on PRICAT tx_group='SG17' from project knowledge. (medium confidence)
272    fn evaluate_10(&self, ctx: &EvaluationContext) -> ConditionResult {
273        let nav = match ctx.navigator() {
274            Some(n) => n,
275            None => return ConditionResult::Unknown,
276        };
277        let sg36_count = nav.group_instance_count(&["SG17", "SG36"]);
278        if sg36_count < 2 {
279            return ConditionResult::False;
280        }
281        // Collect all LIN DE7140 values from SG36 instances (elements[2][0])
282        let mut lin_values: Vec<String> = Vec::new();
283        for i in 0..sg36_count {
284            let lin_segs = nav.find_segments_in_group("LIN", &["SG17", "SG36"], i);
285            let val = lin_segs
286                .first()
287                .and_then(|s| s.elements.get(2))
288                .and_then(|e| e.first())
289                .cloned()
290                .unwrap_or_default();
291            lin_values.push(val);
292        }
293        // Check if any pair shares the prefix before the last '-'
294        // with one having a strictly higher numeric suffix
295        for i in 0..lin_values.len() {
296            let a = &lin_values[i];
297            if a.is_empty() {
298                continue;
299            }
300            let Some(pos_a) = a.rfind('-') else {
301                continue;
302            };
303            let prefix_a = &a[..pos_a];
304            let Ok(num_a) = a[pos_a + 1..].parse::<u64>() else {
305                continue;
306            };
307            for j in 0..lin_values.len() {
308                if i == j {
309                    continue;
310                }
311                let b = &lin_values[j];
312                if b.is_empty() {
313                    continue;
314                }
315                let Some(pos_b) = b.rfind('-') else {
316                    continue;
317                };
318                let prefix_b = &b[..pos_b];
319                let Ok(num_b) = b[pos_b + 1..].parse::<u64>() else {
320                    continue;
321                };
322                if prefix_a == prefix_b && num_b > num_a {
323                    return ConditionResult::True;
324                }
325            }
326        }
327        ConditionResult::False
328    }
329
330    /// [31] wenn BGM DE1001 = Z32 (Preisblatt Messstellenbetrieb)
331    fn evaluate_31(&self, ctx: &EvaluationContext) -> ConditionResult {
332        ctx.has_segment_matching("BGM", &[(0, 0, "Z32")])
333    }
334
335    /// [34] wenn BGM DE1001 = Z77 (Preisblatt Konfigurationen)
336    fn evaluate_34(&self, ctx: &EvaluationContext) -> ConditionResult {
337        ctx.has_segment_matching("BGM", &[(0, 0, "Z77")])
338    }
339
340    /// [35] Wenn das in DE1001 angegebene Preisblatt vom MSB nicht genutzt wird.
341    /// EXTERNAL: Requires context from outside the message.
342    fn evaluate_35(&self, ctx: &EvaluationContext) -> ConditionResult {
343        ctx.external.evaluate("msb_does_not_use_price_list")
344    }
345
346    /// [44] Wenn BGM DE1001 = Z77, dann muss der hier genannte Zeitpunkt ≥ 01.10.2023 00:00 Uhr gesetzlicher deutscher Zeit sein
347    // REVIEW: Checks BGM DE1001 == Z77, then validates DTM+137 (document date) >= 2023-10-01 00:00. 'Der hier genannte Zeitpunkt' refers to the timestamp field where this condition is annotated in the AHB; DTM+137 is assumed as the document date for PRICAT. Condition is inapplicable (Unknown) when BGM code is not Z77. German legal time threshold mapped to 202310010000 using dtm_ge string comparison on format 303. (medium confidence)
348    fn evaluate_44(&self, ctx: &EvaluationContext) -> ConditionResult {
349        let bgm_segs = ctx.find_segments("BGM");
350        let is_z77 = bgm_segs
351            .first()
352            .and_then(|s| s.elements.first())
353            .and_then(|e| e.first())
354            .is_some_and(|v| v == "Z77");
355        if !is_z77 {
356            return ConditionResult::Unknown;
357        }
358        ctx.dtm_ge("137", "202310010000")
359    }
360
361    /// [45] Es sind nur Werte aus der EDI@Energy Codeliste der Artikelnummern und Artikel-ID erlaubt, die in dieser in Kapitel "Abrechnung Messstellenbetrieb für die Sparte Strom" für die jeweilige Marktroll...
362    /// EXTERNAL: Requires context from outside the message.
363    fn evaluate_45(&self, ctx: &EvaluationContext) -> ConditionResult {
364        ctx.external.evaluate("artikel_messstellenbetrieb_strom")
365    }
366
367    /// [46] Es sind nur Werte aus der EDI@Energy Codeliste der Artikelnummern und Artikel-ID erlaubt, die in dieser in Kapitel "Artikel-ID für die Bestellprozesse beim MSB" genannt sind
368    /// EXTERNAL: Requires context from outside the message.
369    fn evaluate_46(&self, ctx: &EvaluationContext) -> ConditionResult {
370        ctx.external.evaluate("artikel_bestellprozesse_msb")
371    }
372
373    /// [55] Wenn in dieser SG36 ein weiteres RNG (d. h. eine weitere Zone) vorhanden ist, in der der Wert des DE6162 (Wertebereichsgrenze, untere) größer ist als der Wert des DE6162 in diesem RNG
374    // REVIEW: Each SG36 represents one price zone (one RNG). Collects DE6162 (lower range boundary) from RNG elements[1][1] (C280 component 1 = lower bound) across all SG36 instances under SG17. Condition is True if any zone has a strictly higher lower bound than another (min < max), meaning there exists a zone with higher DE6162 than the current minimum. Returns False if fewer than 2 zones or all lower bounds are equal. Group path SG17.SG36 follows PRICAT tx_group=SG17 convention. (medium confidence)
375    fn evaluate_55(&self, ctx: &EvaluationContext) -> ConditionResult {
376        let nav = match ctx.navigator() {
377            Some(n) => n,
378            None => return ConditionResult::Unknown,
379        };
380        let sg36_count = nav.group_instance_count(&["SG17", "SG36"]);
381        let mut lower_bounds: Vec<f64> = Vec::new();
382        for i in 0..sg36_count {
383            let rng_segs = nav.find_segments_in_group("RNG", &["SG17", "SG36"], i);
384            for rng in &rng_segs {
385                // C280: [0]=unit, [1]=DE6162 lower bound, [2]=DE6163 upper bound
386                if let Some(val) = rng.elements.get(1).and_then(|e| e.get(1)) {
387                    if let Ok(n) = val.parse::<f64>() {
388                        lower_bounds.push(n);
389                    }
390                }
391            }
392        }
393        if lower_bounds.len() < 2 {
394            return ConditionResult::False;
395        }
396        let min_val = lower_bounds.iter().cloned().fold(f64::INFINITY, f64::min);
397        ConditionResult::from(lower_bounds.iter().any(|&v| v > min_val))
398    }
399
400    /// [56] Wenn BGM DE1001 = Z94, dann muss der hier genannte Zeitpunkt ≥ 01.10.2025 00:00 Uhr gesetzlicher deutscher Zeit sein
401    // REVIEW: Checks BGM DE1001 == Z94, then validates DTM+137 (document date) >= 2025-10-01 00:00. Mirrors condition 44 pattern but for the Z94 document code introduced with FV2510 and the later threshold date. Condition is inapplicable (Unknown) when BGM code is not Z94. German legal time threshold 2025-10-01 00:00 mapped to 202510010000. (medium confidence)
402    fn evaluate_56(&self, ctx: &EvaluationContext) -> ConditionResult {
403        let bgm_segs = ctx.find_segments("BGM");
404        let is_z94 = bgm_segs
405            .first()
406            .and_then(|s| s.elements.first())
407            .and_then(|e| e.first())
408            .is_some_and(|v| v == "Z94");
409        if !is_z94 {
410            return ConditionResult::Unknown;
411        }
412        ctx.dtm_ge("137", "202510010000")
413    }
414
415    /// [57] wenn BGM DE1001 = Z94 (Preisblatt Technik)
416    fn evaluate_57(&self, ctx: &EvaluationContext) -> ConditionResult {
417        ctx.has_qualifier("BGM", 0, "Z94")
418    }
419
420    /// [60] Wenn in dieser SG36 der Teil vor dem "-" aus LIN DE7140 aus der Tabelle des Kapitels "Produkte zur Bestellung einer Änderung an einer Lokation in der Sparte Strom" der EDI@Energy Codeliste der Kon...
421    /// EXTERNAL: Requires context from outside the message.
422    fn evaluate_60(&self, ctx: &EvaluationContext) -> ConditionResult {
423        ctx.external
424            .evaluate("lin_product_code_is_lokationsaenderung_strom")
425    }
426
427    /// [61] Wenn in dieser SG36 der Wert der Zahl nach dem "-" aus LIN DE7140 &gt; 01
428    // REVIEW: LIN DE7140 is elements[2][0]. The condition checks if the numeric part after '-' is greater than 01 (i.e., > 1). Uses group-scoped navigator for SG36 instances, falls back to message-wide on no navigator. Parses after the dash and compares as u32. (medium confidence)
429    fn evaluate_61(&self, ctx: &EvaluationContext) -> ConditionResult {
430        let nav = match ctx.navigator() {
431            Some(n) => n,
432            None => {
433                let lin_segs = ctx.find_segments("LIN");
434                for lin in &lin_segs {
435                    if let Some(val) = lin.elements.get(2).and_then(|e| e.first()) {
436                        if let Some(after_dash) = val.splitn(2, '-').nth(1) {
437                            if let Ok(num) = after_dash.parse::<u32>() {
438                                if num > 1 {
439                                    return ConditionResult::True;
440                                }
441                            }
442                        }
443                    }
444                }
445                return ConditionResult::False;
446            }
447        };
448        let sg36_count = nav.group_instance_count(&["SG36"]);
449        for i in 0..sg36_count {
450            let lin_segs = nav.find_segments_in_group("LIN", &["SG36"], i);
451            for lin in &lin_segs {
452                if let Some(val) = lin.elements.get(2).and_then(|e| e.first()) {
453                    if let Some(after_dash) = val.splitn(2, '-').nth(1) {
454                        if let Ok(num) = after_dash.parse::<u32>() {
455                            if num > 1 {
456                                return ConditionResult::True;
457                            }
458                        }
459                    }
460                }
461            }
462        }
463        ConditionResult::False
464    }
465
466    /// [62] Wenn IMD+F vorhanden
467    fn evaluate_62(&self, ctx: &EvaluationContext) -> ConditionResult {
468        ctx.has_qualifier("IMD", 0, "F")
469    }
470
471    /// [63] Wenn vorheriges DE7008 zur Beschreibung der Leistung dieser Artikel-ID nicht ausreicht
472    /// EXTERNAL: Requires context from outside the message.
473    fn evaluate_63(&self, ctx: &EvaluationContext) -> ConditionResult {
474        ctx.external
475            .evaluate("previous_imd_description_insufficient")
476    }
477
478    /// [65] wenn in dieser SG36 der Teil des Codes in LIN DE7140 nach dem "-" von "01" abweicht
479    // REVIEW: Checks if the part of LIN DE7140 (elements[2][0]) after '-' differs from '01'. Iterates SG36 instances via navigator; falls back to message-wide LIN scan. Returns True if any SG36 contains a LIN where the suffix is not '01'. (medium confidence)
480    fn evaluate_65(&self, ctx: &EvaluationContext) -> ConditionResult {
481        let nav = match ctx.navigator() {
482            Some(n) => n,
483            None => {
484                let lins = ctx.find_segments("LIN");
485                let found = lins.iter().any(|s| {
486                    s.elements
487                        .get(2)
488                        .and_then(|e| e.first())
489                        .and_then(|code| code.split_once('-'))
490                        .map(|(_, suffix)| suffix != "01")
491                        .unwrap_or(false)
492                });
493                return ConditionResult::from(found);
494            }
495        };
496        let sg36_count = nav.group_instance_count(&["SG17", "SG36"]);
497        for i in 0..sg36_count {
498            let lins = nav.find_segments_in_group("LIN", &["SG17", "SG36"], i);
499            for lin in &lins {
500                if let Some(code) = lin.elements.get(2).and_then(|e| e.first()) {
501                    if code
502                        .split_once('-')
503                        .map(|(_, suffix)| suffix != "01")
504                        .unwrap_or(false)
505                    {
506                        return ConditionResult::True;
507                    }
508                }
509            }
510        }
511        ConditionResult::False
512    }
513
514    /// [66] wenn im DE7140 des LIN dieser SG36 der Teil des Codes nach dem "-" den Wert 02 hat, muss dieses DE = 0 sein und in allen anderen RNG zu Artikel-ID, bei denen der Teil des Codes vor dem "-" mit dem ...
515    fn evaluate_66(&self, _ctx: &EvaluationContext) -> ConditionResult {
516        // TODO: Condition [66] requires manual implementation
517        // Reason: Extremely complex cross-group RNG condition: requires finding all RNG segments whose LIN prefix matches, comparing their DE6152 values, and enforcing that when suffix=='02' the DE must be 0 and all matching RNG values must be identical. This involves multi-level cross-group value correlation beyond what the available helpers can express cleanly, and the exact element positions for RNG segments are not provided in the segment reference.
518        ConditionResult::Unknown
519    }
520
521    /// [67] wenn in dieser SG17 ein weiteres LIN vorhanden ist, in dem der Teil des Codes vor dem "-" des DE7140 mit dem des DE7140 des LIN dieser SG36 identisch ist und in dem der Wert nach dem "-" um eins gr...
522    // REVIEW: Iterates all SG17 instances and collects (prefix, suffix) pairs from LIN DE7140 across all their child SG36 instances. For each code, checks whether another LIN in the same SG17 has the same prefix and a numerically incremented suffix (preserving leading-zero width). Returns True if such a consecutive pair exists. (medium confidence)
523    fn evaluate_67(&self, ctx: &EvaluationContext) -> ConditionResult {
524        let nav = match ctx.navigator() {
525            Some(n) => n,
526            None => return ConditionResult::Unknown,
527        };
528        let sg17_count = nav.group_instance_count(&["SG17"]);
529        for sg17_i in 0..sg17_count {
530            let sg36_count = nav.child_group_instance_count(&["SG17"], sg17_i, "SG36");
531            let mut code_parts: Vec<(String, String)> = Vec::new();
532            for sg36_i in 0..sg36_count {
533                let lins =
534                    nav.find_segments_in_child_group("LIN", &["SG17"], sg17_i, "SG36", sg36_i);
535                for lin in &lins {
536                    if let Some(code) = lin.elements.get(2).and_then(|e| e.first()) {
537                        if let Some((prefix, suffix)) = code.split_once('-') {
538                            code_parts.push((prefix.to_string(), suffix.to_string()));
539                        }
540                    }
541                }
542            }
543            for (prefix, suffix) in &code_parts {
544                if let Ok(n) = suffix.parse::<u32>() {
545                    let next = n + 1;
546                    let width = suffix.len();
547                    let next_str = format!("{:0width$}", next, width = width);
548                    if code_parts
549                        .iter()
550                        .any(|(p, s)| p == prefix && s == &next_str)
551                    {
552                        return ConditionResult::True;
553                    }
554                }
555            }
556        }
557        ConditionResult::False
558    }
559
560    /// [68] Der Teil des Codes vor dem "-" muss ein Messprodukt-Code sein
561    /// EXTERNAL: Requires context from outside the message.
562    fn evaluate_68(&self, ctx: &EvaluationContext) -> ConditionResult {
563        ctx.external.evaluate("is_messprodukt_code")
564    }
565
566    /// [69] Der Teil des Codes vor dem "-" muss ein Konfigurationsprodukt-Code sein
567    /// EXTERNAL: Requires context from outside the message.
568    fn evaluate_69(&self, ctx: &EvaluationContext) -> ConditionResult {
569        ctx.external.evaluate("is_konfigurationsprodukt_code")
570    }
571
572    /// [70] Der Teil des Codes vor dem "-" muss ein Produkt-Code sein
573    /// EXTERNAL: Requires context from outside the message.
574    fn evaluate_70(&self, ctx: &EvaluationContext) -> ConditionResult {
575        ctx.external.evaluate("lin_prefix_is_valid_product_code")
576    }
577
578    /// [71] Es muss der Code 9991000003030-01 sein
579    fn evaluate_71(&self, ctx: &EvaluationContext) -> ConditionResult {
580        let segs = ctx.find_segments("LIN");
581        if segs.is_empty() {
582            return ConditionResult::Unknown;
583        }
584        let matches = segs.iter().any(|s| {
585            s.elements
586                .get(2)
587                .and_then(|e| e.first())
588                .is_some_and(|v| v == "9991000003030-01")
589        });
590        ConditionResult::from(matches)
591    }
592
593    /// [72] Wenn in dieser SG36 die letzte Ziffer von LIN DE7140 &gt;1 ist, dann muss es eine SG36 geben dessen Inhalt von LIN DE7140 sich von dem in dieser SG36 nur in der letzten Ziffer unterscheidet und in ...
594    // REVIEW: Cross-SG36 interval continuity check: for each SG36 whose LIN DE7140 ends with digit > 1, there must be another SG36 with DE7140 differing only in the last digit, and that SG36's RNG DE6152 (upper bound, elements[1][2]) must equal this SG36's RNG DE6162 (lower bound, elements[1][1]). RNG is in child group SG40 under SG36. Implemented via navigator with parent-child navigation. Medium confidence due to complexity of cross-group interval continuity logic. (medium confidence)
595    fn evaluate_72(&self, ctx: &EvaluationContext) -> ConditionResult {
596        let nav = match ctx.navigator() {
597            Some(n) => n,
598            None => return ConditionResult::Unknown,
599        };
600        let sg36_count = nav.group_instance_count(&["SG36"]);
601        if sg36_count == 0 {
602            return ConditionResult::Unknown;
603        }
604        for i in 0..sg36_count {
605            let lin_segs = nav.find_segments_in_group("LIN", &["SG36"], i);
606            let lin_val = match lin_segs
607                .first()
608                .and_then(|s| s.elements.get(2))
609                .and_then(|e| e.first())
610                .filter(|v| !v.is_empty())
611            {
612                Some(v) => v.clone(),
613                None => continue,
614            };
615            let last_digit = match lin_val.chars().last().and_then(|c| c.to_digit(10)) {
616                Some(d) => d,
617                None => continue,
618            };
619            if last_digit <= 1 {
620                continue;
621            }
622            // Get this SG36's RNG DE6162 (lower bound) from child SG40
623            let sg40_count = nav.child_group_instance_count(&["SG36"], i, "SG40");
624            let mut this_lower: Option<String> = None;
625            for j in 0..sg40_count {
626                let rngs = nav.find_segments_in_child_group("RNG", &["SG36"], i, "SG40", j);
627                if let Some(rng) = rngs.first() {
628                    if let Some(lower) = rng.elements.get(1).and_then(|e| e.get(1)) {
629                        if !lower.is_empty() {
630                            this_lower = Some(lower.clone());
631                            break;
632                        }
633                    }
634                }
635            }
636            let this_lower = match this_lower {
637                Some(v) => v,
638                None => continue,
639            };
640            let lin_prefix = &lin_val[..lin_val.len() - 1];
641            let mut found = false;
642            for k in 0..sg36_count {
643                if k == i {
644                    continue;
645                }
646                let lin_segs_k = nav.find_segments_in_group("LIN", &["SG36"], k);
647                let lin_val_k = match lin_segs_k
648                    .first()
649                    .and_then(|s| s.elements.get(2))
650                    .and_then(|e| e.first())
651                    .filter(|v| !v.is_empty())
652                {
653                    Some(v) => v.clone(),
654                    None => continue,
655                };
656                if lin_val_k.len() != lin_val.len() {
657                    continue;
658                }
659                if &lin_val_k[..lin_val_k.len() - 1] != lin_prefix {
660                    continue;
661                }
662                // Check RNG DE6152 (upper bound) in that SG36's SG40
663                let sg40_count_k = nav.child_group_instance_count(&["SG36"], k, "SG40");
664                for j in 0..sg40_count_k {
665                    let rngs_k = nav.find_segments_in_child_group("RNG", &["SG36"], k, "SG40", j);
666                    if let Some(rng_k) = rngs_k.first() {
667                        if let Some(upper_k) = rng_k.elements.get(1).and_then(|e| e.get(2)) {
668                            if upper_k == &this_lower {
669                                found = true;
670                                break;
671                            }
672                        }
673                    }
674                }
675                if found {
676                    break;
677                }
678            }
679            if !found {
680                return ConditionResult::False;
681            }
682        }
683        ConditionResult::True
684    }
685
686    /// [511] Hinweis: 1. Der genannte Wert gehört nicht zum Intervall.  2. Die untere Wertegrenze zu der Artikel-ID, deren Zahl an der letzten Stelle den Wert n hat, muss kleiner sein, als die untere Wertegren...
687    fn evaluate_511(&self, _ctx: &EvaluationContext) -> ConditionResult {
688        // Hinweis: 1. Der genannte Wert gehört nicht zum Intervall.
689        // 2. Die untere Wertegrenze zu der Artikel-ID, deren Zahl an der letzten Stelle den Wert n hat,
690        // muss kleiner sein, als die untere Wertegrenze zu der Artikel-ID, deren Zahl an der letzten
691        // Stelle den Wert n+1 hat. — informational annotation, always applies
692        ConditionResult::True
693    }
694
695    /// [522] Hinweis: Hier ist die Leistung zu beschreiben, die mit dieser Artikel-ID in Rechnung gestellt wird, wobei darauf zu achten ist, dass zu erkennen ist, wie sich diese von den Leistungen unterscheiden...
696    fn evaluate_522(&self, _ctx: &EvaluationContext) -> ConditionResult {
697        // Hinweis: Hier ist die Leistung zu beschreiben, die mit dieser Artikel-ID in Rechnung gestellt
698        // wird, wobei darauf zu achten ist, dass zu erkennen ist, wie sich diese von den Leistungen
699        // unterscheiden, bei denen die ersten 13 Stellen der Artikel-ID mit den ersten 13 Stellen
700        // dieser Artikel-ID identisch sind. — informational annotation, always applies
701        ConditionResult::True
702    }
703
704    /// [1] Wenn Vorgängerversion vorhanden
705    /// EXTERNAL: Requires context from outside the message.
706    // REVIEW: Whether a Vorgängerversion (predecessor version) exists is a business context question — it depends on whether a prior PRICAT was sent for the same catalog/articles and is tracked outside the EDIFACT message itself. Cannot be determined from the current message content alone. (medium confidence)
707    fn evaluate_1(&self, ctx: &EvaluationContext) -> ConditionResult {
708        ctx.external.evaluate("previous_version_exists")
709    }
710
711    /// [2] Wenn in dieser SG36 LIN in DE7140 9990001000813 vorhanden
712    fn evaluate_2(&self, ctx: &EvaluationContext) -> ConditionResult {
713        let values = ctx.collect_group_values("LIN", 2, 0, &["SG36"]);
714        ConditionResult::from(values.iter().any(|(_, v)| v == "9990001000813"))
715    }
716
717    /// [3] Wenn IMD+X vorhanden
718    fn evaluate_3(&self, ctx: &EvaluationContext) -> ConditionResult {
719        ctx.has_qualifier("IMD", 0, "X")
720    }
721
722    /// [4] Wenn SG36 IMD+C in diesem IMD vorhanden
723    fn evaluate_4(&self, ctx: &EvaluationContext) -> ConditionResult {
724        ctx.any_group_has_qualifier("IMD", 0, "C", &["SG36"])
725    }
726
727    /// [5] Wenn SG36 IMD+X in diesem IMD vorhanden
728    fn evaluate_5(&self, ctx: &EvaluationContext) -> ConditionResult {
729        ctx.any_group_has_qualifier("IMD", 0, "X", &["SG36"])
730    }
731
732    /// [6] Wenn in dieser SG36 LIN in DE7140 9990001000798 vorhanden
733    fn evaluate_6(&self, ctx: &EvaluationContext) -> ConditionResult {
734        let values = ctx.collect_group_values("LIN", 2, 0, &["SG36"]);
735        ConditionResult::from(values.iter().any(|(_, v)| v == "9990001000798"))
736    }
737
738    /// [7] Wenn in dieser SG36 LIN in DE7140 9990001000798 nicht vorhanden
739    fn evaluate_7(&self, ctx: &EvaluationContext) -> ConditionResult {
740        let values = ctx.collect_group_values("LIN", 2, 0, &["SG36"]);
741        ConditionResult::from(!values.iter().any(|(_, v)| v == "9990001000798"))
742    }
743
744    /// [9] Wenn BGM DE1373 =11 nicht vorhanden
745    fn evaluate_9(&self, ctx: &EvaluationContext) -> ConditionResult {
746        match ctx.find_segment("BGM") {
747            None => ConditionResult::False, // segment absent → condition not applicable
748            Some(seg) => {
749                let val = seg
750                    .elements
751                    .get(4)
752                    .and_then(|e| e.first())
753                    .map(|s| s.as_str())
754                    .unwrap_or("");
755                ConditionResult::from(val != "11")
756            }
757        }
758    }
759
760    /// [12] je UNB ist nur eine Nachricht mit BGM+Z04 in der Übertragungsdatei erlaubt (nur eine Nachricht je Übertragungsdatei)
761    // REVIEW: The notation-resolved hint says elements[0]=Z04. BGM C002.DE1001 is at elements[0][0]. has_qualifier checks elements[0][0]=='Z04'. The 'only one per UNB' structural constraint is an AHB rule consuming this condition — within a single message, we can only check if this IS a Z04 message type. (medium confidence)
762    fn evaluate_12(&self, ctx: &EvaluationContext) -> ConditionResult {
763        ctx.has_qualifier("BGM", 0, "Z04")
764    }
765
766    /// [14] je UNB ist maximal je Code aus DE1001 eine Nachricht in der Übertragungsdatei erlaubt
767    /// EXTERNAL: Requires context from outside the message.
768    // REVIEW: This is a transmission-level cardinality constraint: per UNB envelope, at most one message per DE1001 message type code is allowed. Enforcing this requires visibility into all messages within the same UNB envelope, which is outside the scope of evaluating a single message's EvaluationContext. Must be handled externally at the interchange/envelope level. (medium confidence)
769    fn evaluate_14(&self, ctx: &EvaluationContext) -> ConditionResult {
770        ctx.external.evaluate("max_one_message_per_type_in_unb")
771    }
772
773    /// [19] Nur MP-ID aus Sparte Strom
774    /// EXTERNAL: Requires context from outside the message.
775    // REVIEW: Nur MP-ID aus Sparte Strom — checking whether a market participant ID belongs to the electricity (Strom) sector requires a lookup against an external market participant registry (Marktstammdatenregister or similar). The sector classification cannot be derived from the EDIFACT message content alone. (medium confidence)
776    fn evaluate_19(&self, ctx: &EvaluationContext) -> ConditionResult {
777        ctx.external.evaluate("mp_id_is_strom_sector")
778    }
779
780    /// [22] Wenn die Artikel-ID aus dieser SG36 LIN DE7140 in der EDI@Energy Codeliste der Artikelnummern und Artikel-ID in der Spalte "PRICAT Preisangabe" ein X hat
781    /// EXTERNAL: Requires context from outside the message.
782    // REVIEW: This condition requires a lookup of the Artikel-ID from LIN DE7140 (elements[2][0]) against the EDI@Energy code list of article numbers, specifically checking whether the 'PRICAT Preisangabe' column has an X for that entry. Code list membership checks against external EDI@Energy publications cannot be determined from EDIFACT message content alone — they require an external code list resolver. (medium confidence)
783    fn evaluate_22(&self, ctx: &EvaluationContext) -> ConditionResult {
784        ctx.external.evaluate("article_id_has_pricat_price_flag")
785    }
786
787    /// [24] Wenn in dieser SG36 Wert von LIN DE7140 im Format n1-n2-n1-n8-n2-n1
788    fn evaluate_24(&self, ctx: &EvaluationContext) -> ConditionResult {
789        let values = ctx.collect_group_values("LIN", 2, 0, &["SG36"]);
790        ConditionResult::from(values.iter().any(|(_, val)| {
791            let parts: Vec<&str> = val.split('-').collect();
792            if parts.len() != 6 {
793                return false;
794            }
795            let expected_lens = [1usize, 2, 1, 8, 2, 1];
796            parts
797                .iter()
798                .zip(expected_lens.iter())
799                .all(|(p, &len)| p.len() == len && p.chars().all(|ch| ch.is_ascii_digit()))
800        }))
801    }
802
803    /// [26] Wenn BGM DE1001 = Z70 vorhanden
804    fn evaluate_26(&self, ctx: &EvaluationContext) -> ConditionResult {
805        ctx.has_qualifier("BGM", 0, "Z70")
806    }
807
808    /// [27] Wenn BGM DE1001 = Z70 nicht vorhanden
809    fn evaluate_27(&self, ctx: &EvaluationContext) -> ConditionResult {
810        ctx.lacks_qualifier("BGM", 0, "Z70")
811    }
812
813    /// [28] Wenn die zugehörige Artikel-ID in der letzten Stelle eine 1 ist
814    // REVIEW: Checks the last character of the article ID (Artikel-ID) in the PIA segment. The PIA segment's additional product identification is at elements[1][0]. Last digit '1' → True, any other digit → False. Medium confidence because the exact PIA element path for Artikel-ID in PRICAT is assumed from convention; no segment structure reference provided. (medium confidence)
815    fn evaluate_28(&self, ctx: &EvaluationContext) -> ConditionResult {
816        // Wenn die zugehörige Artikel-ID in der letzten Stelle eine 1 ist
817        // Artikel-ID is in PIA segment, elements[1][0]
818        let segs = ctx.find_segments("PIA");
819        match segs
820            .first()
821            .and_then(|s| s.elements.get(1))
822            .and_then(|e| e.first())
823        {
824            Some(val) => match val.chars().last() {
825                Some('1') => ConditionResult::True,
826                Some(c) if c.is_ascii_digit() => ConditionResult::False,
827                Some(_) => ConditionResult::False,
828                None => ConditionResult::False, // segment absent → condition not applicable
829            },
830            None => ConditionResult::False, // segment absent → condition not applicable
831        }
832    }
833
834    /// [29] Wenn die zugehörige Artikel-ID in der letzten Stelle &gt; 1 ist
835    // REVIEW: Checks the last character of the article ID in PIA elements[1][0]. Last digit > '1' (i.e., '2'–'9') → True, last digit <= '1' → False. Same assumptions as condition 28 regarding PIA element path. (medium confidence)
836    fn evaluate_29(&self, ctx: &EvaluationContext) -> ConditionResult {
837        // Wenn die zugehörige Artikel-ID in der letzten Stelle > 1 ist
838        // Artikel-ID is in PIA segment, elements[1][0]
839        let segs = ctx.find_segments("PIA");
840        match segs
841            .first()
842            .and_then(|s| s.elements.get(1))
843            .and_then(|e| e.first())
844        {
845            Some(val) => match val.chars().last() {
846                Some(c) if c.is_ascii_digit() && c > '1' => ConditionResult::True,
847                Some(c) if c.is_ascii_digit() => ConditionResult::False,
848                Some(_) => ConditionResult::False,
849                None => ConditionResult::False, // segment absent → condition not applicable
850            },
851            None => ConditionResult::False, // segment absent → condition not applicable
852        }
853    }
854
855    /// [30] wenn MP-ID in SG2 NAD+MR in der Rolle LF
856    /// EXTERNAL: Requires context from outside the message.
857    fn evaluate_30(&self, ctx: &EvaluationContext) -> ConditionResult {
858        ctx.external.evaluate("recipient_is_lf")
859    }
860
861    /// [32] wenn der Zeitpunkt im DTM+157 DE2380 &lt; 01.01.2024 00:00 Uhr gesetzlicher deutscher Zeit
862    // REVIEW: DTM+157 (Gültigkeitsbeginn) has DE2380 at elements[0][1] in format 303 (YYYYMMDDHHMM). String comparison is valid for this zero-padded numeric format. Assumes German local time encoding as per EDI@Energy convention. Returns Unknown if DTM+157 is absent. (medium confidence)
863    fn evaluate_32(&self, ctx: &EvaluationContext) -> ConditionResult {
864        {
865            let dtms = ctx.find_segments_with_qualifier("DTM", 0, "157");
866            match dtms.first() {
867                Some(dtm) => {
868                    match dtm
869                        .elements
870                        .first()
871                        .and_then(|e| e.get(1))
872                        .map(|s| s.as_str())
873                    {
874                        Some(value) if !value.is_empty() => {
875                            // Format 303: YYYYMMDDHHMM — string comparison valid for numeric date strings
876                            // 01.01.2024 00:00 German legal time → "202401010000"
877                            ConditionResult::from(value < "202401010000")
878                        }
879                        _ => ConditionResult::Unknown,
880                    }
881                }
882                None => ConditionResult::False, // segment absent → condition not applicable
883            }
884        }
885    }
886
887    /// [33] wenn der Zeitpunkt im DTM+157 DE2380 ≥ 01.01.2024 00:00 Uhr gesetzlicher deutscher Zeit
888    // REVIEW: Mirror of condition 32 with >= comparison. DTM+157 DE2380 at elements[0][1], format 303. Returns Unknown if segment absent. (medium confidence)
889    fn evaluate_33(&self, ctx: &EvaluationContext) -> ConditionResult {
890        {
891            let dtms = ctx.find_segments_with_qualifier("DTM", 0, "157");
892            match dtms.first() {
893                Some(dtm) => {
894                    match dtm
895                        .elements
896                        .first()
897                        .and_then(|e| e.get(1))
898                        .map(|s| s.as_str())
899                    {
900                        Some(value) if !value.is_empty() => {
901                            // Format 303: YYYYMMDDHHMM — string comparison valid for numeric date strings
902                            // 01.01.2024 00:00 German legal time → "202401010000"
903                            ConditionResult::from(value >= "202401010000")
904                        }
905                        _ => ConditionResult::Unknown,
906                    }
907                }
908                None => ConditionResult::False, // segment absent → condition not applicable
909            }
910        }
911    }
912
913    /// [36] Wenn MP-ID in SG2 NAD+MR in der Rolle NB
914    /// EXTERNAL: Requires context from outside the message.
915    fn evaluate_36(&self, ctx: &EvaluationContext) -> ConditionResult {
916        ctx.external.evaluate("recipient_is_nb")
917    }
918
919    /// [37] Wenn im DE3155 in demselben COM der Code EM vorhanden ist
920    // REVIEW: COM segment C076 has DE3148 (number) at elements[0][0] and DE3155 (channel code) at elements[0][1]. The condition checks if DE3155 == "EM" (email). COM segment structure is standard EDIFACT and not ambiguous despite not being in the provided reference. 'Demselben COM' at message level means any COM with EM qualifier. (medium confidence)
921    fn evaluate_37(&self, ctx: &EvaluationContext) -> ConditionResult {
922        {
923            let coms = ctx.find_segments("COM");
924            ConditionResult::from(coms.iter().any(|s| {
925                s.elements
926                    .first()
927                    .and_then(|e| e.get(1))
928                    .is_some_and(|v| v == "EM")
929            }))
930        }
931    }
932
933    /// [38] Wenn im DE3155 in demselben COM der Code TE / FX / AJ / AL vorhanden ist
934    // REVIEW: Same COM segment structure as condition 37. Checks DE3155 for any of TE (telephone), FX (fax), AJ, AL channel codes. (medium confidence)
935    fn evaluate_38(&self, ctx: &EvaluationContext) -> ConditionResult {
936        {
937            let coms = ctx.find_segments("COM");
938            ConditionResult::from(coms.iter().any(|s| {
939                s.elements
940                    .first()
941                    .and_then(|e| e.get(1))
942                    .is_some_and(|v| matches!(v.as_str(), "TE" | "FX" | "AJ" | "AL"))
943            }))
944        }
945    }
946
947    /// [39] Es sind nur Werte aus der EDI@Energy Codeliste der Artikelnummern und Artikel-ID erlaubt, die in der Spalte MaBiS ein X haben
948    /// EXTERNAL: Requires context from outside the message.
949    fn evaluate_39(&self, ctx: &EvaluationContext) -> ConditionResult {
950        ctx.external.evaluate("article_id_in_mabis_codelist")
951    }
952
953    /// [40] Es sind nur Werte aus der EDI@Energy Codeliste der Artikelnummern und Artikel-ID erlaubt, die in der Spalte H ein X haben
954    /// EXTERNAL: Requires context from outside the message.
955    fn evaluate_40(&self, ctx: &EvaluationContext) -> ConditionResult {
956        ctx.external.evaluate("article_id_in_h_codelist")
957    }
958
959    /// [41] Es sind nur Werte aus der EDI@Energy Codeliste der Artikelnummern und Artikel-ID erlaubt, die in der Spalte "PRICAT Codeverwendung" ein X haben
960    /// EXTERNAL: Requires context from outside the message.
961    fn evaluate_41(&self, ctx: &EvaluationContext) -> ConditionResult {
962        ctx.external.evaluate("article_id_in_pricat_codelist")
963    }
964
965    /// [42] Es sind nur Werte erlaubt, die die Bildungsvorschrift der EDI@Energy Codeliste der Artikelnummern und Artikel-ID erfüllen, und die in der Spalte "PRICAT Codeverwendung" ein X haben
966    /// EXTERNAL: Requires context from outside the message.
967    fn evaluate_42(&self, ctx: &EvaluationContext) -> ConditionResult {
968        ctx.external
969            .evaluate("article_id_valid_pricat_formation_rule")
970    }
971
972    /// [43] Wenn BGM DE1001 = Z32 und LIN DE7140 im Format n1-n2-n1-n3, dann muss der hier genannte Zeitpunkt ≥ 01.01.2024 00:00 Uhr gesetzlicher deutscher Zeit sein
973    // REVIEW: Checks BGM DE1001==Z32, LIN DE7140 contains hyphens (n1-n2-n1-n3 format like '1-01-6-005'), and DTM+157 (Gültigkeitsbeginn) >= 20240101. Medium confidence because 'der hier genannte Zeitpunkt' is assumed to be DTM+157. (medium confidence)
974    fn evaluate_43(&self, ctx: &EvaluationContext) -> ConditionResult {
975        let bgm = match ctx.find_segment("BGM") {
976            Some(s) => s,
977            None => return ConditionResult::Unknown,
978        };
979        if bgm
980            .elements
981            .first()
982            .and_then(|e| e.first())
983            .map(|s| s.as_str())
984            != Some("Z32")
985        {
986            return ConditionResult::False;
987        }
988        let lin_segments = ctx.find_segments("LIN");
989        let has_hyphen_format = lin_segments.iter().any(|s| {
990            s.elements
991                .get(2)
992                .and_then(|e| e.first())
993                .map_or(false, |v| v.contains('-'))
994        });
995        if !has_hyphen_format {
996            return ConditionResult::False;
997        }
998        let dtm_segs = ctx.find_segments_with_qualifier("DTM", 0, "157");
999        match dtm_segs.first() {
1000            Some(dtm) => {
1001                let value = dtm
1002                    .elements
1003                    .first()
1004                    .and_then(|e| e.get(1))
1005                    .map(|s| s.as_str())
1006                    .unwrap_or("");
1007                if value.len() >= 8 {
1008                    ConditionResult::from(&value[..8] >= "20240101")
1009                } else {
1010                    ConditionResult::Unknown
1011                }
1012            }
1013            None => ConditionResult::False, // segment absent → condition not applicable
1014        }
1015    }
1016
1017    /// [47] Wenn BGM DE1001 = Z32 und LIN DE7140 im Format n13, dann muss der hier genannte Zeitpunkt &lt; 01.01.2024 00:00 Uhr gesetzlicher deutscher Zeit sein
1018    // REVIEW: Checks BGM DE1001==Z32, LIN DE7140 is exactly 13 digits with no hyphens (legacy EAN/GLN format), and DTM+157 < 20240101. Complement of condition 43. (medium confidence)
1019    fn evaluate_47(&self, ctx: &EvaluationContext) -> ConditionResult {
1020        let bgm = match ctx.find_segment("BGM") {
1021            Some(s) => s,
1022            None => return ConditionResult::Unknown,
1023        };
1024        if bgm
1025            .elements
1026            .first()
1027            .and_then(|e| e.first())
1028            .map(|s| s.as_str())
1029            != Some("Z32")
1030        {
1031            return ConditionResult::False;
1032        }
1033        let lin_segments = ctx.find_segments("LIN");
1034        let has_n13_format = lin_segments.iter().any(|s| {
1035            s.elements
1036                .get(2)
1037                .and_then(|e| e.first())
1038                .map_or(false, |v| {
1039                    !v.contains('-') && v.len() == 13 && v.chars().all(|c| c.is_ascii_digit())
1040                })
1041        });
1042        if !has_n13_format {
1043            return ConditionResult::False;
1044        }
1045        let dtm_segs = ctx.find_segments_with_qualifier("DTM", 0, "157");
1046        match dtm_segs.first() {
1047            Some(dtm) => {
1048                let value = dtm
1049                    .elements
1050                    .first()
1051                    .and_then(|e| e.get(1))
1052                    .map(|s| s.as_str())
1053                    .unwrap_or("");
1054                if value.len() >= 8 {
1055                    ConditionResult::from(&value[..8] < "20240101")
1056                } else {
1057                    ConditionResult::Unknown
1058                }
1059            }
1060            None => ConditionResult::False, // segment absent → condition not applicable
1061        }
1062    }
1063
1064    /// [48] Wenn in dieser SG36 LIN in DE7140 einer der Codes 1-01-6-005 / 1-01-9-001 / 1-01-9-002 / 1-02-0-015 / 1-03-8-001 / 1-03-8-002 / 1-03-8-003 / 1-03-8-004 / 1-03-9-001 / 1-03-9-002 / 1-03-9-003 / 1-03...
1065    fn evaluate_48(&self, ctx: &EvaluationContext) -> ConditionResult {
1066        const CODES: &[&str] = &[
1067            "1-01-6-005",
1068            "1-01-9-001",
1069            "1-01-9-002",
1070            "1-02-0-015",
1071            "1-03-8-001",
1072            "1-03-8-002",
1073            "1-03-8-003",
1074            "1-03-8-004",
1075            "1-03-9-001",
1076            "1-03-9-002",
1077            "1-03-9-003",
1078            "1-03-9-004",
1079            "1-07-4-001",
1080        ];
1081        let lin_segments = ctx.find_segments("LIN");
1082        ConditionResult::from(lin_segments.iter().any(|s| {
1083            s.elements
1084                .get(2)
1085                .and_then(|e| e.first())
1086                .map_or(false, |v| CODES.contains(&v.as_str()))
1087        }))
1088    }
1089
1090    /// [49] Wenn in dieser SG36 LIN in DE7140 keiner der Codes 1-01-6-005 / 1-01-9-001 / 1-01-9-002 / 1-02-0-015 / 1-03-8-001 / 1-03-8-002 / 1-03-8-003 / 1-03-8-004 / 1-03-9-001 / 1-03-9-002 / 1-03-9-003 / 1-0...
1091    fn evaluate_49(&self, ctx: &EvaluationContext) -> ConditionResult {
1092        const CODES: &[&str] = &[
1093            "1-01-6-005",
1094            "1-01-9-001",
1095            "1-01-9-002",
1096            "1-02-0-015",
1097            "1-03-8-001",
1098            "1-03-8-002",
1099            "1-03-8-003",
1100            "1-03-8-004",
1101            "1-03-9-001",
1102            "1-03-9-002",
1103            "1-03-9-003",
1104            "1-03-9-004",
1105            "1-07-4-001",
1106        ];
1107        let lin_segments = ctx.find_segments("LIN");
1108        ConditionResult::from(!lin_segments.iter().any(|s| {
1109            s.elements
1110                .get(2)
1111                .and_then(|e| e.first())
1112                .map_or(false, |v| CODES.contains(&v.as_str()))
1113        }))
1114    }
1115
1116    /// [50] Wenn MP-ID aus RFF+Z56 mit MP-ID aus NAD+MS identisch ist
1117    fn evaluate_50(&self, ctx: &EvaluationContext) -> ConditionResult {
1118        let rff_segs = ctx.find_segments_with_qualifier("RFF", 0, "Z56");
1119        let nad_segs = ctx.find_segments_with_qualifier("NAD", 0, "MS");
1120        let rff_mp_id = rff_segs
1121            .first()
1122            .and_then(|s| s.elements.first())
1123            .and_then(|e| e.get(1))
1124            .map(|s| s.as_str());
1125        let nad_mp_id = nad_segs
1126            .first()
1127            .and_then(|s| s.elements.get(1))
1128            .and_then(|e| e.first())
1129            .map(|s| s.as_str());
1130        match (rff_mp_id, nad_mp_id) {
1131            (Some(rff), Some(nad)) if !rff.is_empty() && !nad.is_empty() => {
1132                ConditionResult::from(rff == nad)
1133            }
1134            _ => ConditionResult::Unknown,
1135        }
1136    }
1137
1138    /// [51] Wenn BGM+Z54 vorhanden
1139    fn evaluate_51(&self, ctx: &EvaluationContext) -> ConditionResult {
1140        ctx.has_qualifier("BGM", 0, "Z54")
1141    }
1142
1143    /// [52] Wenn BGM+Z54 nicht vorhanden
1144    fn evaluate_52(&self, ctx: &EvaluationContext) -> ConditionResult {
1145        ctx.lacks_qualifier("BGM", 0, "Z54")
1146    }
1147
1148    /// [53] Diese SG40 darf genau einmal in der SG36 angegeben werden
1149    // REVIEW: Validates that SG40 appears exactly once within each SG36 instance. Uses the group navigator to count child SG40 instances per SG36 parent. PRICAT tx_group is SG17; SG36 is a nested group within SG17. Falls back to Unknown when no navigator is available. Medium confidence because the exact parent path ["SG17", "SG36"] is inferred from PRICAT structure conventions without an explicit segment reference. (medium confidence)
1150    fn evaluate_53(&self, ctx: &EvaluationContext) -> ConditionResult {
1151        // Diese SG40 darf genau einmal in der SG36 angegeben werden
1152        // For each SG36 instance, verify SG40 appears exactly once as a child group
1153        let nav = match ctx.navigator() {
1154            Some(n) => n,
1155            None => return ConditionResult::Unknown,
1156        };
1157        let sg36_count = nav.group_instance_count(&["SG17", "SG36"]);
1158        if sg36_count == 0 {
1159            return ConditionResult::Unknown;
1160        }
1161        for i in 0..sg36_count {
1162            let sg40_count = nav.child_group_instance_count(&["SG17", "SG36"], i, "SG40");
1163            if sg40_count != 1 {
1164                return ConditionResult::False;
1165            }
1166        }
1167        ConditionResult::True
1168    }
1169
1170    /// [54] Falls der Preis des Artikels gezont ist
1171    /// EXTERNAL: Requires context from outside the message.
1172    // REVIEW: 'Falls der Preis des Artikels gezont ist' describes whether the article has zone-based pricing — a product/business configuration attribute. While zone pricing might leave structural traces in the message (multiple zone segments), whether a price is fundamentally 'gezont' is a business model characteristic of the article that is external to the EDIFACT message content itself. (medium confidence)
1173    fn evaluate_54(&self, ctx: &EvaluationContext) -> ConditionResult {
1174        ctx.external.evaluate("article_price_is_zoned")
1175    }
1176
1177    /// [64] Wenn der Zeitpunkt im DTM+157 DE2380 ≥ 01.01.2026, 00:00 Uhr gesetzlicher deutscher Zeit
1178    fn evaluate_64(&self, ctx: &EvaluationContext) -> ConditionResult {
1179        let dtm_segs = ctx.find_segments_with_qualifier("DTM", 0, "157");
1180        match dtm_segs.first() {
1181            Some(dtm) => {
1182                let value = dtm
1183                    .elements
1184                    .first()
1185                    .and_then(|e| e.get(1))
1186                    .map(|s| s.as_str())
1187                    .unwrap_or("");
1188                if value.len() >= 8 {
1189                    ConditionResult::from(&value[..8] >= "20260101")
1190                } else {
1191                    ConditionResult::Unknown
1192                }
1193            }
1194            None => ConditionResult::False, // segment absent → condition not applicable
1195        }
1196    }
1197
1198    /// [490] wenn Wert in diesem DE, an der Stelle CCYYMMDD ein Datum aus dem angegeben Zeitraum der Tabelle Kapitel 3.5 „Prozesszeitpunkt bei MESZ mit UTC“ ist
1199    fn evaluate_490(&self, ctx: &EvaluationContext) -> ConditionResult {
1200        let dtm_segs = ctx.find_segments("DTM");
1201        match dtm_segs
1202            .first()
1203            .and_then(|s| s.elements.first())
1204            .and_then(|e| e.get(1))
1205        {
1206            Some(val) => is_mesz_utc(val),
1207            None => ConditionResult::False, // segment absent → condition not applicable
1208        }
1209    }
1210
1211    /// [491] wenn Wert in diesem DE, an der Stelle CCYYMMDD ein Datum aus dem angegeben Zeitraum der Tabelle Kapitel 3.6 „Prozesszeitpunkt bei MEZ mit UTC“ ist
1212    fn evaluate_491(&self, ctx: &EvaluationContext) -> ConditionResult {
1213        let dtm_segs = ctx.find_segments("DTM");
1214        match dtm_segs
1215            .first()
1216            .and_then(|s| s.elements.first())
1217            .and_then(|e| e.get(1))
1218        {
1219            Some(val) => is_mez_utc(val),
1220            None => ConditionResult::False, // segment absent → condition not applicable
1221        }
1222    }
1223
1224    /// [492] wenn MP-ID in NAD+MR aus Sparte Strom
1225    /// EXTERNAL: Requires context from outside the message.
1226    // REVIEW: Checks if the NAD+MR market participant ID belongs to the electricity (Strom) sector. Sector membership cannot be determined from the EDIFACT message alone — requires external business context. (medium confidence)
1227    fn evaluate_492(&self, ctx: &EvaluationContext) -> ConditionResult {
1228        ctx.external.evaluate("recipient_is_strom")
1229    }
1230
1231    /// [494] Das hier genannte Datum muss der Zeitpunkt sein, zu dem das Dokument erstellt wurde, oder ein Zeitpunkt, der davor liegt
1232    fn evaluate_494(&self, _ctx: &EvaluationContext) -> ConditionResult {
1233        // Hinweis: Das hier genannte Datum muss der Zeitpunkt sein, zu dem das Dokument
1234        // erstellt wurde, oder ein Zeitpunkt, der davor liegt. Informational annotation
1235        // about the semantic meaning of the date field — always applies unconditionally.
1236        ConditionResult::True
1237    }
1238
1239    /// [495] Der Zeitpunkt muss ≤ dem Wert im DE2380 des DTM+137 sein
1240    // REVIEW: The condition checks that the Betrachtungszeitintervall (DTM+492) value is <= the Nachrichtendatum (DTM+137) value. Both DTM qualifiers appear in the segment reference for PRICAT. We extract DTM+137's DE2380 value as the threshold and use dtm_le to compare. Medium confidence because 'Der Zeitpunkt' could refer to DTM+157 (Gültigkeitsbeginn) instead of DTM+492 — the exact field being validated is not explicit in the description. (medium confidence)
1241    fn evaluate_495(&self, ctx: &EvaluationContext) -> ConditionResult {
1242        // Der Zeitpunkt (DTM+492 Betrachtungszeitintervall) muss <= DTM+137 (Nachrichtendatum) sein
1243        let dtm_137_segs = ctx.find_segments_with_qualifier("DTM", 0, "137");
1244        let dtm_137 = match dtm_137_segs.first() {
1245            Some(s) => s,
1246            None => return ConditionResult::Unknown,
1247        };
1248        let threshold = match dtm_137.elements.first().and_then(|e| e.get(1)) {
1249            Some(v) => v.clone(),
1250            None => return ConditionResult::Unknown,
1251        };
1252        ctx.dtm_le("492", &threshold)
1253    }
1254
1255    /// [502] Hinweis: Preis in Euro je MWh
1256    fn evaluate_502(&self, _ctx: &EvaluationContext) -> ConditionResult {
1257        ConditionResult::True
1258    }
1259
1260    /// [503] Hinweis: Hier ist immer der Wert 1000 einzutragen, da in DE5118 der Preis in €/MWh angegeben wird.
1261    fn evaluate_503(&self, _ctx: &EvaluationContext) -> ConditionResult {
1262        ConditionResult::True
1263    }
1264
1265    /// [504] Hinweis: Dokumentennummer der PRICAT
1266    fn evaluate_504(&self, _ctx: &EvaluationContext) -> ConditionResult {
1267        ConditionResult::True
1268    }
1269
1270    /// [512] Hinweis: Der genannte Wert gehört zum Intervall
1271    fn evaluate_512(&self, _ctx: &EvaluationContext) -> ConditionResult {
1272        ConditionResult::True
1273    }
1274
1275    /// [513] Hinweis: Die zum Preis gehörende Einheit ist in der Codeliste definiert
1276    fn evaluate_513(&self, _ctx: &EvaluationContext) -> ConditionResult {
1277        ConditionResult::True
1278    }
1279
1280    /// [519] Hinweis: Es darf nur eine Information im DE3148 übermittelt werden
1281    fn evaluate_519(&self, _ctx: &EvaluationContext) -> ConditionResult {
1282        ConditionResult::True
1283    }
1284
1285    /// [520] Hinweis: Falls der Preis des Artikels gezont ist, ist diese SG40 so oft zu wiederholen, bis alle Preise zu diesem Artikel genannt sind
1286    fn evaluate_520(&self, _ctx: &EvaluationContext) -> ConditionResult {
1287        ConditionResult::True
1288    }
1289
1290    /// [521] Hinweis: Je Artikel-ID muss in einem RNG der Wert dieses DE = 0 sein und in allen anderen RNG zu dieser Artikel-ID muss der Wert dieses DE mit dem Wert des DE6152 eines anderen RNG zu dieser Artike...
1291    fn evaluate_521(&self, _ctx: &EvaluationContext) -> ConditionResult {
1292        // Hinweis: informational annotation about RNG DE value relationships — one RNG must have value 0,
1293        // all others must equal DE6152 of another RNG for the same Artikel-ID
1294        ConditionResult::True
1295    }
1296
1297    /// [902] Format: Möglicher Wert: ≥ 0
1298    fn evaluate_902(&self, _ctx: &EvaluationContext) -> ConditionResult {
1299        ConditionResult::True
1300    }
1301
1302    /// [908] Format: Mögliche Werte: 1 bis n
1303    fn evaluate_908(&self, _ctx: &EvaluationContext) -> ConditionResult {
1304        ConditionResult::True
1305    }
1306
1307    /// [909] Format: Mögliche Werte: 0 bis n
1308    // REVIEW: Format condition specifying possible values 0 to n (non-negative). Applied to LIN DE1082 (Positionsnummer) as the most likely numeric element in PRICAT that can take values starting from 0. Medium confidence because the AHB context specifying which exact element this condition applies to is not included in the description. (medium confidence)
1309    fn evaluate_909(&self, ctx: &EvaluationContext) -> ConditionResult {
1310        // Format: Mögliche Werte 0 bis n — numeric value must be >= 0
1311        // Applied to LIN DE1082 (Positionsnummer) which can start from 0
1312        let lin_segs = ctx.find_segments("LIN");
1313        if lin_segs.is_empty() {
1314            return ConditionResult::Unknown;
1315        }
1316        for seg in &lin_segs {
1317            let val = match seg.elements.first().and_then(|e| e.first()) {
1318                Some(v) => v,
1319                None => return ConditionResult::Unknown,
1320            };
1321            match validate_numeric(val, ">=", 0.0) {
1322                ConditionResult::False => return ConditionResult::False,
1323                ConditionResult::Unknown => return ConditionResult::Unknown,
1324                ConditionResult::True => {}
1325            }
1326        }
1327        ConditionResult::True
1328    }
1329
1330    /// [911] Format: Mögliche Werte: 1 bis n, je Nachricht oder Segmentgruppe bei 1 beginnend und fortlaufend aufsteigend
1331    // REVIEW: Format condition specifying values 1 to n, starting at 1 and continuously ascending per message or segment group. This describes sequential position numbering, which in PRICAT is DE1082 in the LIN segment. The implementation collects all LIN segments and verifies their position numbers are 1, 2, 3, ... in order. Medium confidence because the specific element and scope (per-message vs per-SG17 group) are not explicit in the condition description alone. (medium confidence)
1332    fn evaluate_911(&self, ctx: &EvaluationContext) -> ConditionResult {
1333        // Format: Mögliche Werte 1 bis n, sequential per message/segment group starting at 1
1334        // Validates LIN DE1082 (Positionsnummer) is sequential starting from 1
1335        let lin_segs = ctx.find_segments("LIN");
1336        if lin_segs.is_empty() {
1337            return ConditionResult::Unknown;
1338        }
1339        let mut expected: u64 = 1;
1340        for seg in &lin_segs {
1341            let pos_str = match seg.elements.first().and_then(|e| e.first()) {
1342                Some(v) => v,
1343                None => return ConditionResult::Unknown,
1344            };
1345            let pos: u64 = match pos_str.parse() {
1346                Ok(n) => n,
1347                Err(_) => return ConditionResult::False,
1348            };
1349            if pos != expected {
1350                return ConditionResult::False;
1351            }
1352            expected += 1;
1353        }
1354        ConditionResult::True
1355    }
1356
1357    /// [912] Format: max. 6 Nachkommastellen
1358    // REVIEW: Max 6 decimal places format check. In PRICAT context, price values (PRI segment, element 0, component 1) are the primary numeric values with decimal places. Medium confidence because the exact segment this applies to depends on context — it could also apply to QTY or other numeric fields. (medium confidence)
1359    fn evaluate_912(&self, ctx: &EvaluationContext) -> ConditionResult {
1360        // Format: max. 6 Nachkommastellen — applies to PRI price value in PRICAT
1361        let segs = ctx.find_segments("PRI");
1362        match segs
1363            .first()
1364            .and_then(|s| s.elements.first())
1365            .and_then(|e| e.get(1))
1366        {
1367            Some(val) => validate_max_decimal_places(val, 6),
1368            None => ConditionResult::False, // segment absent → condition not applicable
1369        }
1370    }
1371
1372    /// [926] Format: Möglicher Wert: 0
1373    // REVIEW: Value must be exactly 0. Applies to a numeric data element — QTY is most common in PRICAT for this kind of constraint (e.g., minimum order quantity of 0). Medium confidence because the specific segment depends on where this condition is referenced in the AHB. (medium confidence)
1374    fn evaluate_926(&self, ctx: &EvaluationContext) -> ConditionResult {
1375        // Format: Möglicher Wert: 0 — value must equal 0
1376        let segs = ctx.find_segments("QTY");
1377        match segs
1378            .first()
1379            .and_then(|s| s.elements.first())
1380            .and_then(|e| e.get(1))
1381        {
1382            Some(val) => validate_numeric(val, "==", 0.0),
1383            None => ConditionResult::False, // segment absent → condition not applicable
1384        }
1385    }
1386
1387    /// [929] Format: Möglicher Wert: 1000
1388    // REVIEW: Value must be exactly 1000. In PRICAT, 1000 is a common reference quantity (e.g., price per 1000 units in QTY). Medium confidence because the exact target segment depends on where this condition is used in the AHB. (medium confidence)
1389    fn evaluate_929(&self, ctx: &EvaluationContext) -> ConditionResult {
1390        // Format: Möglicher Wert: 1000 — value must equal 1000
1391        let segs = ctx.find_segments("QTY");
1392        match segs
1393            .first()
1394            .and_then(|s| s.elements.first())
1395            .and_then(|e| e.get(1))
1396        {
1397            Some(val) => validate_numeric(val, "==", 1000.0),
1398            None => ConditionResult::False, // segment absent → condition not applicable
1399        }
1400    }
1401
1402    /// [931] Format: ZZZ = +00
1403    // REVIEW: DTM timezone check: EDIFACT DTM format 303 (CCYYMMDDHHMMzzz) or 719 encodes timezone offset. The condition requires ZZZ=+00 (UTC offset). elements[0][1] holds the datetime value string; checking it ends with '+00' covers standard EDIFACT timezone encoding. Medium confidence because the exact DTM qualifier and format code context is unknown without the segment structure reference. (medium confidence)
1404    fn evaluate_931(&self, ctx: &EvaluationContext) -> ConditionResult {
1405        // ZZZ = +00: timezone offset in DTM value must be +00
1406        // DTM format 303: YYYYMMDDHHMMzzz, timezone encoded as last part of elements[0][1]
1407        let dtm_segments = ctx.find_segments("DTM");
1408        for seg in &dtm_segments {
1409            if let Some(value) = seg.elements.first().and_then(|e| e.get(1)) {
1410                if !value.is_empty() {
1411                    // Timezone +00 appears at end of value string
1412                    if value.ends_with("+00") {
1413                        return ConditionResult::True;
1414                    } else {
1415                        return ConditionResult::False;
1416                    }
1417                }
1418            }
1419        }
1420        ConditionResult::Unknown
1421    }
1422
1423    /// [932] Format: HHMM = 2200
1424    // REVIEW: DTM time check: extracts HHMM portion from DTM value. Format 203 (CCYYMMDDHHmm) places hours at positions 8-11. Format 401 (HHmm) starts at position 0. Condition requires exactly 2200. Medium confidence because the specific DTM qualifier context in PRICAT is unknown without the segment structure reference. (medium confidence)
1425    fn evaluate_932(&self, ctx: &EvaluationContext) -> ConditionResult {
1426        // HHMM = 2200: time value in DTM must be 2200
1427        // In DTM format 203 (YYYYMMDDHHMM) or 401 (HHMM), the time portion is HHMM
1428        let dtm_segments = ctx.find_segments("DTM");
1429        for seg in &dtm_segments {
1430            if let Some(value) = seg.elements.first().and_then(|e| e.get(1)) {
1431                let format_code = seg
1432                    .elements
1433                    .first()
1434                    .and_then(|e| e.get(2))
1435                    .map(|s| s.as_str())
1436                    .unwrap_or("");
1437                let time_part = match format_code {
1438                    "203" if value.len() >= 12 => &value[8..12],
1439                    "401" if value.len() >= 4 => &value[0..4],
1440                    _ if value.len() >= 12 => &value[8..12],
1441                    _ => continue,
1442                };
1443                if time_part == "2200" {
1444                    return ConditionResult::True;
1445                } else {
1446                    return ConditionResult::False;
1447                }
1448            }
1449        }
1450        ConditionResult::Unknown
1451    }
1452
1453    /// [933] Format: HHMM = 2300
1454    // REVIEW: Same structure as condition 932 but checks for 2300 instead of 2200. Extracts HHMM portion from DTM value using format code to determine position. (medium confidence)
1455    fn evaluate_933(&self, ctx: &EvaluationContext) -> ConditionResult {
1456        // HHMM = 2300: time value in DTM must be 2300
1457        let dtm_segments = ctx.find_segments("DTM");
1458        for seg in &dtm_segments {
1459            if let Some(value) = seg.elements.first().and_then(|e| e.get(1)) {
1460                let format_code = seg
1461                    .elements
1462                    .first()
1463                    .and_then(|e| e.get(2))
1464                    .map(|s| s.as_str())
1465                    .unwrap_or("");
1466                let time_part = match format_code {
1467                    "203" if value.len() >= 12 => &value[8..12],
1468                    "401" if value.len() >= 4 => &value[0..4],
1469                    _ if value.len() >= 12 => &value[8..12],
1470                    _ => continue,
1471                };
1472                if time_part == "2300" {
1473                    return ConditionResult::True;
1474                } else {
1475                    return ConditionResult::False;
1476                }
1477            }
1478        }
1479        ConditionResult::Unknown
1480    }
1481
1482    /// [937] Format: keine Nachkommastelle
1483    // REVIEW: No decimal places allowed — integer values only. Implemented as validate_max_decimal_places with 0. In PRICAT, this typically applies to QTY (quantity must be whole number). Medium confidence because the exact target segment depends on AHB context. (medium confidence)
1484    fn evaluate_937(&self, ctx: &EvaluationContext) -> ConditionResult {
1485        // Format: keine Nachkommastelle — no decimal places allowed (max 0)
1486        let segs = ctx.find_segments("QTY");
1487        match segs
1488            .first()
1489            .and_then(|s| s.elements.first())
1490            .and_then(|e| e.get(1))
1491        {
1492            Some(val) => validate_max_decimal_places(val, 0),
1493            None => ConditionResult::False, // segment absent → condition not applicable
1494        }
1495    }
1496
1497    /// [939] Format: Die Zeichenkette muss die Zeichen @ und . enthalten
1498    // REVIEW: Email address format validation: the string must contain both '@' and '.'. This pattern matches COM segment (communication address) with channel code EM (Electronic Mail). elements[0][0] holds the address, elements[0][1] holds the channel qualifier. Checks channel=EM first, then falls back to any non-empty COM value. Medium confidence because PRICAT segment structure reference not provided. (medium confidence)
1499    fn evaluate_939(&self, ctx: &EvaluationContext) -> ConditionResult {
1500        // Email format: string must contain both '@' and '.'
1501        // Typically applies to COM segment (communication channel EM = email)
1502        let com_segments = ctx.find_segments("COM");
1503        for seg in &com_segments {
1504            // COM: elements[0][0] = communication address, elements[0][1] = channel code
1505            let channel = seg
1506                .elements
1507                .first()
1508                .and_then(|e| e.get(1))
1509                .map(|s| s.as_str())
1510                .unwrap_or("");
1511            if channel == "EM" {
1512                if let Some(address) = seg.elements.first().and_then(|e| e.first()) {
1513                    let contains_at = address.contains('@');
1514                    let contains_dot = address.contains('.');
1515                    return ConditionResult::from(contains_at && contains_dot);
1516                }
1517            }
1518        }
1519        // Fallback: check any COM value for @ and .
1520        for seg in &com_segments {
1521            if let Some(address) = seg.elements.first().and_then(|e| e.first()) {
1522                if !address.is_empty() {
1523                    return ConditionResult::from(address.contains('@') && address.contains('.'));
1524                }
1525            }
1526        }
1527        ConditionResult::Unknown
1528    }
1529
1530    /// [940] Format: Die Zeichenkette muss mit dem Zeichen + beginnen und danach dürfen nur noch Ziffern folgen
1531    // REVIEW: Phone number format: must start with '+' then only digits (international E.164 format). Applies to COM segment with telephone/fax channel codes (TE, FX, AJ). elements[0][0] = number, elements[0][1] = channel code. Medium confidence because PRICAT segment structure reference not provided. (medium confidence)
1532    fn evaluate_940(&self, ctx: &EvaluationContext) -> ConditionResult {
1533        // Phone number format: must start with '+' followed only by digits
1534        // Typically applies to COM segment (TEL/FAX channel)
1535        let com_segments = ctx.find_segments("COM");
1536        for seg in &com_segments {
1537            let channel = seg
1538                .elements
1539                .first()
1540                .and_then(|e| e.get(1))
1541                .map(|s| s.as_str())
1542                .unwrap_or("");
1543            if matches!(channel, "TE" | "FX" | "AJ") {
1544                if let Some(number) = seg.elements.first().and_then(|e| e.first()) {
1545                    if !number.is_empty() {
1546                        let valid = number.starts_with('+')
1547                            && number.len() > 1
1548                            && number[1..].chars().all(|c| c.is_ascii_digit());
1549                        return ConditionResult::from(valid);
1550                    }
1551                }
1552            }
1553        }
1554        // Fallback: check any COM value for +digit format
1555        for seg in &com_segments {
1556            if let Some(number) = seg.elements.first().and_then(|e| e.first()) {
1557                if !number.is_empty() {
1558                    let valid = number.starts_with('+')
1559                        && number.len() > 1
1560                        && number[1..].chars().all(|c| c.is_ascii_digit());
1561                    return ConditionResult::from(valid);
1562                }
1563            }
1564        }
1565        ConditionResult::Unknown
1566    }
1567
1568    /// [941] Format: Artikelnummer
1569    // REVIEW: Article number format validation on PIA segment (Additional Product ID), element 1, component 0. The pattern n1-n2-n1-n3 is a common PRICAT Artikelnummer format in the German energy market. Medium confidence because the exact digit-segment pattern may differ — should be verified against the specific AHB table for this condition reference. (medium confidence)
1570    fn evaluate_941(&self, ctx: &EvaluationContext) -> ConditionResult {
1571        // Format: Artikelnummer — validate article number format in PIA segment
1572        let segs = ctx.find_segments("PIA");
1573        match segs
1574            .first()
1575            .and_then(|s| s.elements.get(1))
1576            .and_then(|e| e.first())
1577        {
1578            Some(val) => validate_artikel_pattern(val, &[1, 2, 1, 3]),
1579            None => ConditionResult::False, // segment absent → condition not applicable
1580        }
1581    }
1582
1583    /// [942] Format: n1-n2-n1-n3
1584    fn evaluate_942(&self, ctx: &EvaluationContext) -> ConditionResult {
1585        let segs = ctx.find_segments("PIA");
1586        match segs
1587            .first()
1588            .and_then(|s| s.elements.get(1))
1589            .and_then(|e| e.first())
1590        {
1591            Some(val) => validate_artikel_pattern(val, &[1, 2, 1, 3]),
1592            None => ConditionResult::False, // segment absent → condition not applicable
1593        }
1594    }
1595
1596    /// [946] Format: max. 11 Nachkommastellen
1597    // REVIEW: Format: max. 11 Nachkommastellen — validates that a numeric value has at most 11 decimal places. In PRICAT, price values appear in PRI segment element 0 component 1. Using PRI as the most likely price segment in PRICAT context. (medium confidence)
1598    fn evaluate_946(&self, ctx: &EvaluationContext) -> ConditionResult {
1599        let segs = ctx.find_segments("PRI");
1600        match segs
1601            .first()
1602            .and_then(|s| s.elements.first())
1603            .and_then(|e| e.get(1))
1604        {
1605            Some(val) => validate_max_decimal_places(val, 11),
1606            None => ConditionResult::False, // segment absent → condition not applicable
1607        }
1608    }
1609
1610    /// [948] Format: n1-n2-n1-n8-n2
1611    fn evaluate_948(&self, ctx: &EvaluationContext) -> ConditionResult {
1612        let segs = ctx.find_segments("PIA");
1613        match segs
1614            .first()
1615            .and_then(|s| s.elements.get(1))
1616            .and_then(|e| e.first())
1617        {
1618            Some(val) => validate_artikel_pattern(val, &[1, 2, 1, 8, 2]),
1619            None => ConditionResult::False, // segment absent → condition not applicable
1620        }
1621    }
1622
1623    /// [949] Format: n1-n2-n1-n8-n2-n1
1624    fn evaluate_949(&self, ctx: &EvaluationContext) -> ConditionResult {
1625        let segs = ctx.find_segments("PIA");
1626        match segs
1627            .first()
1628            .and_then(|s| s.elements.get(1))
1629            .and_then(|e| e.first())
1630        {
1631            Some(val) => validate_artikel_pattern(val, &[1, 2, 1, 8, 2, 1]),
1632            None => ConditionResult::False, // segment absent → condition not applicable
1633        }
1634    }
1635
1636    /// [957] Format: n1-n2-n1-n8
1637    fn evaluate_957(&self, ctx: &EvaluationContext) -> ConditionResult {
1638        let segs = ctx.find_segments("PIA");
1639        match segs
1640            .first()
1641            .and_then(|s| s.elements.get(1))
1642            .and_then(|e| e.first())
1643        {
1644            Some(val) => validate_artikel_pattern(val, &[1, 2, 1, 8]),
1645            None => ConditionResult::False, // segment absent → condition not applicable
1646        }
1647    }
1648
1649    /// [959] Format: n13-n2
1650    // REVIEW: Format: n13-n2 is an Artikelnummer pattern — 13 digits, dash, 2 digits. In PRICAT, article identifiers appear in PIA element 1 component 0. Using validate_artikel_pattern with &[13, 2]. Medium confidence because the exact target segment/element depends on AHB context not provided here. (medium confidence)
1651    fn evaluate_959(&self, ctx: &EvaluationContext) -> ConditionResult {
1652        let segs = ctx.find_segments("PIA");
1653        match segs
1654            .first()
1655            .and_then(|s| s.elements.get(1))
1656            .and_then(|e| e.first())
1657        {
1658            Some(val) => validate_artikel_pattern(val, &[13, 2]),
1659            None => ConditionResult::False, // segment absent → condition not applicable
1660        }
1661    }
1662
1663    /// [968] Format: Möglicher Wert: ≤ 0
1664    // REVIEW: Format: Möglicher Wert: ≤ 0 means the numeric value must be less than or equal to zero. In PRICAT, PRI segment element 0 component 1 holds the price amount. Using validate_numeric with "<=", 0.0. Medium confidence because the exact target segment depends on AHB context — could also be MOA or another numeric field. (medium confidence)
1665    fn evaluate_968(&self, ctx: &EvaluationContext) -> ConditionResult {
1666        let segs = ctx.find_segments("PRI");
1667        match segs
1668            .first()
1669            .and_then(|s| s.elements.first())
1670            .and_then(|e| e.get(1))
1671        {
1672            Some(val) => validate_numeric(val, "<=", 0.0),
1673            None => ConditionResult::False, // segment absent → condition not applicable
1674        }
1675    }
1676}