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 > 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 >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 > 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 < 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 < 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}