automapper_validation/generated/fv2504/pricat_conditions_fv2504.rs
1// <auto-generated>
2// Generated by automapper-generator generate-conditions
3// AHB: xml-migs-and-ahbs/FV2504/PRICAT_AHB_2_0e_Fehlerkorrektur_20250623.xml
4// Generated: 2026-03-12T10:05:58Z
5// </auto-generated>
6
7// LLM-produced bodies aren't idiomatic Rust — silence stylistic lints at
8// file scope so `cargo clippy -D warnings` stays green. Correctness,
9// suspicious, and perf categories stay on so real bugs still surface.
10#![allow(clippy::style, clippy::complexity)]
11
12#[allow(unused_imports)]
13use crate::eval::format_validators::*;
14use crate::eval::{ConditionEvaluator, ConditionResult, EvaluationContext};
15
16/// Generated condition evaluator for PRICAT FV2504.
17pub struct PricatConditionEvaluatorFV2504 {
18 // External condition IDs that require runtime context.
19 external_conditions: std::collections::HashSet<u32>,
20}
21
22impl Default for PricatConditionEvaluatorFV2504 {
23 fn default() -> Self {
24 let mut external_conditions = std::collections::HashSet::new();
25 external_conditions.insert(1);
26 external_conditions.insert(14);
27 external_conditions.insert(19);
28 external_conditions.insert(22);
29 external_conditions.insert(30);
30 external_conditions.insert(36);
31 external_conditions.insert(39);
32 external_conditions.insert(40);
33 external_conditions.insert(41);
34 external_conditions.insert(42);
35 external_conditions.insert(45);
36 external_conditions.insert(46);
37 external_conditions.insert(54);
38 external_conditions.insert(492);
39 Self {
40 external_conditions,
41 }
42 }
43}
44
45impl ConditionEvaluator for PricatConditionEvaluatorFV2504 {
46 fn message_type(&self) -> &str {
47 "PRICAT"
48 }
49
50 fn format_version(&self) -> &str {
51 "FV2504"
52 }
53
54 fn evaluate(&self, condition: u32, ctx: &EvaluationContext) -> ConditionResult {
55 match condition {
56 1 => self.evaluate_1(ctx),
57 2 => self.evaluate_2(ctx),
58 3 => self.evaluate_3(ctx),
59 4 => self.evaluate_4(ctx),
60 5 => self.evaluate_5(ctx),
61 6 => self.evaluate_6(ctx),
62 7 => self.evaluate_7(ctx),
63 9 => self.evaluate_9(ctx),
64 10 => self.evaluate_10(ctx),
65 12 => self.evaluate_12(ctx),
66 14 => self.evaluate_14(ctx),
67 19 => self.evaluate_19(ctx),
68 22 => self.evaluate_22(ctx),
69 24 => self.evaluate_24(ctx),
70 26 => self.evaluate_26(ctx),
71 27 => self.evaluate_27(ctx),
72 28 => self.evaluate_28(ctx),
73 29 => self.evaluate_29(ctx),
74 30 => self.evaluate_30(ctx),
75 31 => self.evaluate_31(ctx),
76 32 => self.evaluate_32(ctx),
77 33 => self.evaluate_33(ctx),
78 34 => self.evaluate_34(ctx),
79 36 => self.evaluate_36(ctx),
80 37 => self.evaluate_37(ctx),
81 38 => self.evaluate_38(ctx),
82 39 => self.evaluate_39(ctx),
83 40 => self.evaluate_40(ctx),
84 41 => self.evaluate_41(ctx),
85 42 => self.evaluate_42(ctx),
86 43 => self.evaluate_43(ctx),
87 44 => self.evaluate_44(ctx),
88 45 => self.evaluate_45(ctx),
89 46 => self.evaluate_46(ctx),
90 47 => self.evaluate_47(ctx),
91 48 => self.evaluate_48(ctx),
92 49 => self.evaluate_49(ctx),
93 50 => self.evaluate_50(ctx),
94 51 => self.evaluate_51(ctx),
95 52 => self.evaluate_52(ctx),
96 53 => self.evaluate_53(ctx),
97 54 => self.evaluate_54(ctx),
98 55 => self.evaluate_55(ctx),
99 64 => self.evaluate_64(ctx),
100 490 => self.evaluate_490(ctx),
101 491 => self.evaluate_491(ctx),
102 492 => self.evaluate_492(ctx),
103 494 => self.evaluate_494(ctx),
104 495 => self.evaluate_495(ctx),
105 502 => self.evaluate_502(ctx),
106 503 => self.evaluate_503(ctx),
107 504 => self.evaluate_504(ctx),
108 511 => self.evaluate_511(ctx),
109 512 => self.evaluate_512(ctx),
110 513 => self.evaluate_513(ctx),
111 519 => self.evaluate_519(ctx),
112 520 => self.evaluate_520(ctx),
113 521 => self.evaluate_521(ctx),
114 902 => self.evaluate_902(ctx),
115 908 => self.evaluate_908(ctx),
116 909 => self.evaluate_909(ctx),
117 911 => self.evaluate_911(ctx),
118 912 => self.evaluate_912(ctx),
119 926 => self.evaluate_926(ctx),
120 929 => self.evaluate_929(ctx),
121 931 => self.evaluate_931(ctx),
122 932 => self.evaluate_932(ctx),
123 933 => self.evaluate_933(ctx),
124 937 => self.evaluate_937(ctx),
125 939 => self.evaluate_939(ctx),
126 940 => self.evaluate_940(ctx),
127 941 => self.evaluate_941(ctx),
128 942 => self.evaluate_942(ctx),
129 946 => self.evaluate_946(ctx),
130 948 => self.evaluate_948(ctx),
131 949 => self.evaluate_949(ctx),
132 957 => self.evaluate_957(ctx),
133 959 => self.evaluate_959(ctx),
134 968 => self.evaluate_968(ctx),
135 _ => ConditionResult::Unknown,
136 }
137 }
138
139 fn is_external(&self, condition: u32) -> bool {
140 self.external_conditions.contains(&condition)
141 }
142 fn is_known(&self, condition: u32) -> bool {
143 matches!(
144 condition,
145 1 | 2
146 | 3
147 | 4
148 | 5
149 | 6
150 | 7
151 | 9
152 | 10
153 | 12
154 | 14
155 | 19
156 | 22
157 | 24
158 | 26
159 | 27
160 | 28
161 | 29
162 | 30
163 | 31
164 | 32
165 | 33
166 | 34
167 | 36
168 | 37
169 | 38
170 | 39
171 | 40
172 | 41
173 | 42
174 | 43
175 | 44
176 | 45
177 | 46
178 | 47
179 | 48
180 | 49
181 | 50
182 | 51
183 | 52
184 | 53
185 | 54
186 | 55
187 | 64
188 | 490
189 | 491
190 | 492
191 | 494
192 | 495
193 | 502
194 | 503
195 | 504
196 | 511
197 | 512
198 | 513
199 | 519
200 | 520
201 | 521
202 | 902
203 | 908
204 | 909
205 | 911
206 | 912
207 | 926
208 | 929
209 | 931
210 | 932
211 | 933
212 | 937
213 | 939
214 | 940
215 | 941
216 | 942
217 | 946
218 | 948
219 | 949
220 | 957
221 | 959
222 | 968
223 )
224 }
225}
226
227impl PricatConditionEvaluatorFV2504 {
228 /// [10] Wenn eine weitere Zone für diese Gruppenartikel-ID vorhanden
229 fn evaluate_10(&self, ctx: &EvaluationContext) -> ConditionResult {
230 // In PRICAT each SG36 is a "Zone" (price range), identified by its
231 // LIN DE7140 (Artikel-ID). The Gruppenartikel-ID is the prefix before
232 // the last '-' (e.g. LIN "1-01-6-005" → group "1-01-6"). The
233 // condition is True when two or more SG36s share the same
234 // Gruppenartikel-ID prefix — i.e. multiple zones exist within the
235 // same article group.
236 let nav = match ctx.navigator() {
237 Some(n) => n,
238 None => return ConditionResult::Unknown,
239 };
240 let sg36_count = nav.group_instance_count(&["SG17", "SG36"]);
241 if sg36_count < 2 {
242 return ConditionResult::False;
243 }
244 let mut prefixes: Vec<String> = Vec::with_capacity(sg36_count);
245 for i in 0..sg36_count {
246 let lin_segs = nav.find_segments_in_group("LIN", &["SG17", "SG36"], i);
247 if let Some(v) = lin_segs
248 .first()
249 .and_then(|s| s.elements.get(2))
250 .and_then(|e| e.first())
251 {
252 if let Some(pos) = v.rfind('-') {
253 prefixes.push(v[..pos].to_string());
254 }
255 }
256 }
257 for i in 0..prefixes.len() {
258 for j in (i + 1)..prefixes.len() {
259 if !prefixes[i].is_empty() && prefixes[i] == prefixes[j] {
260 return ConditionResult::True;
261 }
262 }
263 }
264 ConditionResult::False
265 }
266
267 /// [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" genannt sind
268 /// EXTERNAL: Requires context from outside the message.
269 fn evaluate_45(&self, ctx: &EvaluationContext) -> ConditionResult {
270 ctx.external
271 .evaluate("pricat_artikel_abrechnung_messstellenbetrieb_strom")
272 }
273
274 /// [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 den Universalbestellprozess für die Sparte Strom" genannt sind
275 /// EXTERNAL: Requires context from outside the message.
276 fn evaluate_46(&self, ctx: &EvaluationContext) -> ConditionResult {
277 ctx.external
278 .evaluate("pricat_artikel_universalbestellprozess_strom")
279 }
280
281 /// [54] Falls der Preis des Artikels gezont ist
282 /// EXTERNAL: Requires context from outside the message.
283 // 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)
284 fn evaluate_54(&self, ctx: &EvaluationContext) -> ConditionResult {
285 ctx.external.evaluate("article_price_is_zoned")
286 }
287
288 /// [55] Wenn eine weitere Zone für diese Artikel-ID vorhanden
289 fn evaluate_55(&self, ctx: &EvaluationContext) -> ConditionResult {
290 // Artikel-ID is the full LIN DE7140 (no prefix stripping). True when
291 // two or more SG36 "zones" carry the same full Artikel-ID — i.e. a
292 // duplicate/overlapping zone definition exists for one article.
293 let nav = match ctx.navigator() {
294 Some(n) => n,
295 None => return ConditionResult::Unknown,
296 };
297 let sg36_count = nav.group_instance_count(&["SG17", "SG36"]);
298 if sg36_count < 2 {
299 return ConditionResult::False;
300 }
301 let mut ids: Vec<String> = Vec::with_capacity(sg36_count);
302 for i in 0..sg36_count {
303 let lin_segs = nav.find_segments_in_group("LIN", &["SG17", "SG36"], i);
304 if let Some(v) = lin_segs
305 .first()
306 .and_then(|s| s.elements.get(2))
307 .and_then(|e| e.first())
308 {
309 ids.push(v.clone());
310 }
311 }
312 for i in 0..ids.len() {
313 for j in (i + 1)..ids.len() {
314 if !ids[i].is_empty() && ids[i] == ids[j] {
315 return ConditionResult::True;
316 }
317 }
318 }
319 ConditionResult::False
320 }
321
322 /// [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
323 fn evaluate_490(&self, ctx: &EvaluationContext) -> ConditionResult {
324 match ctx.resolved_value {
325 Some(val) => is_mesz_utc(val),
326 None => ConditionResult::False,
327 }
328 }
329
330 /// [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
331 fn evaluate_491(&self, ctx: &EvaluationContext) -> ConditionResult {
332 match ctx.resolved_value {
333 Some(val) => is_mez_utc(val),
334 None => ConditionResult::False,
335 }
336 }
337
338 /// [1] Wenn Vorgängerversion vorhanden
339 /// EXTERNAL: Requires context from outside the message.
340 // 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)
341 fn evaluate_1(&self, ctx: &EvaluationContext) -> ConditionResult {
342 ctx.external.evaluate("previous_version_exists")
343 }
344
345 /// [2] Wenn in dieser SG36 LIN in DE7140 9990001000813 vorhanden
346 fn evaluate_2(&self, ctx: &EvaluationContext) -> ConditionResult {
347 let values = ctx.collect_group_values("LIN", 2, 0, &["SG36"]);
348 ConditionResult::from(values.iter().any(|(_, v)| v == "9990001000813"))
349 }
350
351 /// [3] Wenn IMD+X vorhanden
352 fn evaluate_3(&self, ctx: &EvaluationContext) -> ConditionResult {
353 ctx.has_qualifier("IMD", 0, "X")
354 }
355
356 /// [4] Wenn SG36 IMD+C in diesem IMD vorhanden
357 fn evaluate_4(&self, ctx: &EvaluationContext) -> ConditionResult {
358 ctx.any_group_has_qualifier("IMD", 0, "C", &["SG36"])
359 }
360
361 /// [5] Wenn SG36 IMD+X in diesem IMD vorhanden
362 fn evaluate_5(&self, ctx: &EvaluationContext) -> ConditionResult {
363 ctx.any_group_has_qualifier("IMD", 0, "X", &["SG36"])
364 }
365
366 /// [6] Wenn in dieser SG36 LIN in DE7140 9990001000798 vorhanden
367 fn evaluate_6(&self, ctx: &EvaluationContext) -> ConditionResult {
368 let values = ctx.collect_group_values("LIN", 2, 0, &["SG36"]);
369 ConditionResult::from(values.iter().any(|(_, v)| v == "9990001000798"))
370 }
371
372 /// [7] Wenn in dieser SG36 LIN in DE7140 9990001000798 nicht vorhanden
373 fn evaluate_7(&self, ctx: &EvaluationContext) -> ConditionResult {
374 let values = ctx.collect_group_values("LIN", 2, 0, &["SG36"]);
375 ConditionResult::from(!values.iter().any(|(_, v)| v == "9990001000798"))
376 }
377
378 /// [9] Wenn BGM DE1373 =11 nicht vorhanden
379 fn evaluate_9(&self, ctx: &EvaluationContext) -> ConditionResult {
380 match ctx.find_segment("BGM") {
381 None => ConditionResult::False, // segment absent → condition not applicable
382 Some(seg) => {
383 let val = seg
384 .elements
385 .get(4)
386 .and_then(|e| e.first())
387 .map(|s| s.as_str())
388 .unwrap_or("");
389 ConditionResult::from(val != "11")
390 }
391 }
392 }
393
394 /// [12] je UNB ist nur eine Nachricht mit BGM+Z04 in der Übertragungsdatei erlaubt (nur eine Nachricht je Übertragungsdatei)
395 // 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)
396 fn evaluate_12(&self, ctx: &EvaluationContext) -> ConditionResult {
397 ctx.has_qualifier("BGM", 0, "Z04")
398 }
399
400 /// [14] je UNB ist maximal je Code aus DE1001 eine Nachricht in der Übertragungsdatei erlaubt
401 /// EXTERNAL: Requires context from outside the message.
402 // 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)
403 fn evaluate_14(&self, ctx: &EvaluationContext) -> ConditionResult {
404 ctx.external.evaluate("max_one_message_per_type_in_unb")
405 }
406
407 /// [19] Nur MP-ID aus Sparte Strom
408 /// EXTERNAL: Requires context from outside the message.
409 // 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)
410 fn evaluate_19(&self, ctx: &EvaluationContext) -> ConditionResult {
411 ctx.external.evaluate("mp_id_is_strom_sector")
412 }
413
414 /// [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
415 /// EXTERNAL: Requires context from outside the message.
416 // 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)
417 fn evaluate_22(&self, ctx: &EvaluationContext) -> ConditionResult {
418 ctx.external.evaluate("article_id_has_pricat_price_flag")
419 }
420
421 /// [24] Wenn in dieser SG36 Wert von LIN DE7140 im Format n1-n2-n1-n8-n2-n1
422 fn evaluate_24(&self, ctx: &EvaluationContext) -> ConditionResult {
423 let values = ctx.collect_group_values("LIN", 2, 0, &["SG36"]);
424 ConditionResult::from(values.iter().any(|(_, val)| {
425 let parts: Vec<&str> = val.split('-').collect();
426 if parts.len() != 6 {
427 return false;
428 }
429 let expected_lens = [1usize, 2, 1, 8, 2, 1];
430 parts
431 .iter()
432 .zip(expected_lens.iter())
433 .all(|(p, &len)| p.len() == len && p.chars().all(|ch| ch.is_ascii_digit()))
434 }))
435 }
436
437 /// [26] Wenn BGM DE1001 = Z70 vorhanden
438 fn evaluate_26(&self, ctx: &EvaluationContext) -> ConditionResult {
439 ctx.has_qualifier("BGM", 0, "Z70")
440 }
441
442 /// [27] Wenn BGM DE1001 = Z70 nicht vorhanden
443 fn evaluate_27(&self, ctx: &EvaluationContext) -> ConditionResult {
444 ctx.lacks_qualifier("BGM", 0, "Z70")
445 }
446
447 /// [28] Wenn die zugehörige Artikel-ID in der letzten Stelle eine 1 ist
448 // 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)
449 fn evaluate_28(&self, ctx: &EvaluationContext) -> ConditionResult {
450 // Wenn die zugehörige Artikel-ID in der letzten Stelle eine 1 ist
451 // Artikel-ID is in PIA segment, elements[1][0]
452 let segs = ctx.find_segments("PIA");
453 match segs
454 .first()
455 .and_then(|s| s.elements.get(1))
456 .and_then(|e| e.first())
457 {
458 Some(val) => match val.chars().last() {
459 Some('1') => ConditionResult::True,
460 Some(c) if c.is_ascii_digit() => ConditionResult::False,
461 Some(_) => ConditionResult::False,
462 None => ConditionResult::False, // segment absent → condition not applicable
463 },
464 None => ConditionResult::False, // segment absent → condition not applicable
465 }
466 }
467
468 /// [29] Wenn die zugehörige Artikel-ID in der letzten Stelle > 1 ist
469 // 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)
470 fn evaluate_29(&self, ctx: &EvaluationContext) -> ConditionResult {
471 // Wenn die zugehörige Artikel-ID in der letzten Stelle > 1 ist
472 // Artikel-ID is in PIA segment, elements[1][0]
473 let segs = ctx.find_segments("PIA");
474 match segs
475 .first()
476 .and_then(|s| s.elements.get(1))
477 .and_then(|e| e.first())
478 {
479 Some(val) => match val.chars().last() {
480 Some(c) if c.is_ascii_digit() && c > '1' => ConditionResult::True,
481 Some(c) if c.is_ascii_digit() => ConditionResult::False,
482 Some(_) => ConditionResult::False,
483 None => ConditionResult::False, // segment absent → condition not applicable
484 },
485 None => ConditionResult::False, // segment absent → condition not applicable
486 }
487 }
488
489 /// [30] wenn MP-ID in SG2 NAD+MR in der Rolle LF
490 /// EXTERNAL: Requires context from outside the message.
491 fn evaluate_30(&self, ctx: &EvaluationContext) -> ConditionResult {
492 ctx.external.evaluate("recipient_is_lf")
493 }
494
495 /// [31] wenn BGM DE1001 = Z32
496 fn evaluate_31(&self, ctx: &EvaluationContext) -> ConditionResult {
497 ctx.has_qualifier("BGM", 0, "Z32")
498 }
499
500 /// [32] wenn der Zeitpunkt im DTM+157 DE2380 < 01.01.2024 00:00 Uhr gesetzlicher deutscher Zeit
501 // 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)
502 fn evaluate_32(&self, ctx: &EvaluationContext) -> ConditionResult {
503 {
504 let dtms = ctx.find_segments_with_qualifier("DTM", 0, "157");
505 match dtms.first() {
506 Some(dtm) => {
507 match dtm
508 .elements
509 .first()
510 .and_then(|e| e.get(1))
511 .map(|s| s.as_str())
512 {
513 Some(value) if !value.is_empty() => {
514 // Format 303: YYYYMMDDHHMM — string comparison valid for numeric date strings
515 // 01.01.2024 00:00 German legal time → "202401010000"
516 ConditionResult::from(value < "202401010000")
517 }
518 _ => ConditionResult::Unknown,
519 }
520 }
521 None => ConditionResult::False, // segment absent → condition not applicable
522 }
523 }
524 }
525
526 /// [33] wenn der Zeitpunkt im DTM+157 DE2380 ≥ 01.01.2024 00:00 Uhr gesetzlicher deutscher Zeit
527 // REVIEW: Mirror of condition 32 with >= comparison. DTM+157 DE2380 at elements[0][1], format 303. Returns Unknown if segment absent. (medium confidence)
528 fn evaluate_33(&self, ctx: &EvaluationContext) -> ConditionResult {
529 {
530 let dtms = ctx.find_segments_with_qualifier("DTM", 0, "157");
531 match dtms.first() {
532 Some(dtm) => {
533 match dtm
534 .elements
535 .first()
536 .and_then(|e| e.get(1))
537 .map(|s| s.as_str())
538 {
539 Some(value) if !value.is_empty() => {
540 // Format 303: YYYYMMDDHHMM — string comparison valid for numeric date strings
541 // 01.01.2024 00:00 German legal time → "202401010000"
542 ConditionResult::from(value >= "202401010000")
543 }
544 _ => ConditionResult::Unknown,
545 }
546 }
547 None => ConditionResult::False, // segment absent → condition not applicable
548 }
549 }
550 }
551
552 /// [34] wenn BGM DE1001 = Z77
553 fn evaluate_34(&self, ctx: &EvaluationContext) -> ConditionResult {
554 ctx.has_qualifier("BGM", 0, "Z77")
555 }
556
557 /// [36] Wenn MP-ID in SG2 NAD+MR in der Rolle NB
558 /// EXTERNAL: Requires context from outside the message.
559 fn evaluate_36(&self, ctx: &EvaluationContext) -> ConditionResult {
560 ctx.external.evaluate("recipient_is_nb")
561 }
562
563 /// [37] Wenn im DE3155 in demselben COM der Code EM vorhanden ist
564 // 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)
565 fn evaluate_37(&self, ctx: &EvaluationContext) -> ConditionResult {
566 {
567 let coms = ctx.find_segments("COM");
568 ConditionResult::from(coms.iter().any(|s| {
569 s.elements
570 .first()
571 .and_then(|e| e.get(1))
572 .is_some_and(|v| v == "EM")
573 }))
574 }
575 }
576
577 /// [38] Wenn im DE3155 in demselben COM der Code TE / FX / AJ / AL vorhanden ist
578 // REVIEW: Same COM segment structure as condition 37. Checks DE3155 for any of TE (telephone), FX (fax), AJ, AL channel codes. (medium confidence)
579 fn evaluate_38(&self, ctx: &EvaluationContext) -> ConditionResult {
580 {
581 let coms = ctx.find_segments("COM");
582 ConditionResult::from(coms.iter().any(|s| {
583 s.elements
584 .first()
585 .and_then(|e| e.get(1))
586 .is_some_and(|v| matches!(v.as_str(), "TE" | "FX" | "AJ" | "AL"))
587 }))
588 }
589 }
590
591 /// [39] Es sind nur Werte aus der EDI@Energy Codeliste der Artikelnummern und Artikel-ID erlaubt, die in der Spalte MaBiS ein X haben
592 /// EXTERNAL: Requires context from outside the message.
593 fn evaluate_39(&self, ctx: &EvaluationContext) -> ConditionResult {
594 ctx.external.evaluate("article_id_in_mabis_codelist")
595 }
596
597 /// [40] Es sind nur Werte aus der EDI@Energy Codeliste der Artikelnummern und Artikel-ID erlaubt, die in der Spalte H ein X haben
598 /// EXTERNAL: Requires context from outside the message.
599 fn evaluate_40(&self, ctx: &EvaluationContext) -> ConditionResult {
600 ctx.external.evaluate("article_id_in_h_codelist")
601 }
602
603 /// [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
604 /// EXTERNAL: Requires context from outside the message.
605 fn evaluate_41(&self, ctx: &EvaluationContext) -> ConditionResult {
606 ctx.external.evaluate("article_id_in_pricat_codelist")
607 }
608
609 /// [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
610 /// EXTERNAL: Requires context from outside the message.
611 fn evaluate_42(&self, ctx: &EvaluationContext) -> ConditionResult {
612 ctx.external
613 .evaluate("article_id_valid_pricat_formation_rule")
614 }
615
616 /// [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
617 // 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)
618 fn evaluate_43(&self, ctx: &EvaluationContext) -> ConditionResult {
619 let bgm = match ctx.find_segment("BGM") {
620 Some(s) => s,
621 None => return ConditionResult::Unknown,
622 };
623 if bgm
624 .elements
625 .first()
626 .and_then(|e| e.first())
627 .map(|s| s.as_str())
628 != Some("Z32")
629 {
630 return ConditionResult::False;
631 }
632 let lin_segments = ctx.find_segments("LIN");
633 let has_hyphen_format = lin_segments.iter().any(|s| {
634 s.elements
635 .get(2)
636 .and_then(|e| e.first())
637 .map_or(false, |v| v.contains('-'))
638 });
639 if !has_hyphen_format {
640 return ConditionResult::False;
641 }
642 let dtm_segs = ctx.find_segments_with_qualifier("DTM", 0, "157");
643 match dtm_segs.first() {
644 Some(dtm) => {
645 let value = dtm
646 .elements
647 .first()
648 .and_then(|e| e.get(1))
649 .map(|s| s.as_str())
650 .unwrap_or("");
651 if value.len() >= 8 {
652 ConditionResult::from(&value[..8] >= "20240101")
653 } else {
654 ConditionResult::Unknown
655 }
656 }
657 None => ConditionResult::False, // segment absent → condition not applicable
658 }
659 }
660
661 /// [44] Wenn BGM DE1001 = Z77, dann muss der hier genannte Zeitpunkt ≥ 01.10.2023 00:00 Uhr gesetzlicher deutscher Zeit sein.
662 // REVIEW: Checks BGM DE1001==Z77 then verifies DTM+157 value >= 20231001 (01.10.2023). Medium confidence because the referenced time point is assumed to be DTM+157 (Gültigkeitsbeginn). (medium confidence)
663 fn evaluate_44(&self, ctx: &EvaluationContext) -> ConditionResult {
664 let bgm = match ctx.find_segment("BGM") {
665 Some(s) => s,
666 None => return ConditionResult::Unknown,
667 };
668 if bgm
669 .elements
670 .first()
671 .and_then(|e| e.first())
672 .map(|s| s.as_str())
673 != Some("Z77")
674 {
675 return ConditionResult::False;
676 }
677 let dtm_segs = ctx.find_segments_with_qualifier("DTM", 0, "157");
678 match dtm_segs.first() {
679 Some(dtm) => {
680 let value = dtm
681 .elements
682 .first()
683 .and_then(|e| e.get(1))
684 .map(|s| s.as_str())
685 .unwrap_or("");
686 if value.len() >= 8 {
687 ConditionResult::from(&value[..8] >= "20231001")
688 } else {
689 ConditionResult::Unknown
690 }
691 }
692 None => ConditionResult::False, // segment absent → condition not applicable
693 }
694 }
695
696 /// [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
697 // 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)
698 fn evaluate_47(&self, ctx: &EvaluationContext) -> ConditionResult {
699 let bgm = match ctx.find_segment("BGM") {
700 Some(s) => s,
701 None => return ConditionResult::Unknown,
702 };
703 if bgm
704 .elements
705 .first()
706 .and_then(|e| e.first())
707 .map(|s| s.as_str())
708 != Some("Z32")
709 {
710 return ConditionResult::False;
711 }
712 let lin_segments = ctx.find_segments("LIN");
713 let has_n13_format = lin_segments.iter().any(|s| {
714 s.elements
715 .get(2)
716 .and_then(|e| e.first())
717 .map_or(false, |v| {
718 !v.contains('-') && v.len() == 13 && v.chars().all(|c| c.is_ascii_digit())
719 })
720 });
721 if !has_n13_format {
722 return ConditionResult::False;
723 }
724 let dtm_segs = ctx.find_segments_with_qualifier("DTM", 0, "157");
725 match dtm_segs.first() {
726 Some(dtm) => {
727 let value = dtm
728 .elements
729 .first()
730 .and_then(|e| e.get(1))
731 .map(|s| s.as_str())
732 .unwrap_or("");
733 if value.len() >= 8 {
734 ConditionResult::from(&value[..8] < "20240101")
735 } else {
736 ConditionResult::Unknown
737 }
738 }
739 None => ConditionResult::False, // segment absent → condition not applicable
740 }
741 }
742
743 /// [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...
744 fn evaluate_48(&self, ctx: &EvaluationContext) -> ConditionResult {
745 const CODES: &[&str] = &[
746 "1-01-6-005",
747 "1-01-9-001",
748 "1-01-9-002",
749 "1-02-0-015",
750 "1-03-8-001",
751 "1-03-8-002",
752 "1-03-8-003",
753 "1-03-8-004",
754 "1-03-9-001",
755 "1-03-9-002",
756 "1-03-9-003",
757 "1-03-9-004",
758 "1-07-4-001",
759 ];
760 let lin_segments = ctx.find_segments("LIN");
761 ConditionResult::from(lin_segments.iter().any(|s| {
762 s.elements
763 .get(2)
764 .and_then(|e| e.first())
765 .map_or(false, |v| CODES.contains(&v.as_str()))
766 }))
767 }
768
769 /// [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...
770 fn evaluate_49(&self, ctx: &EvaluationContext) -> ConditionResult {
771 const CODES: &[&str] = &[
772 "1-01-6-005",
773 "1-01-9-001",
774 "1-01-9-002",
775 "1-02-0-015",
776 "1-03-8-001",
777 "1-03-8-002",
778 "1-03-8-003",
779 "1-03-8-004",
780 "1-03-9-001",
781 "1-03-9-002",
782 "1-03-9-003",
783 "1-03-9-004",
784 "1-07-4-001",
785 ];
786 let lin_segments = ctx.find_segments("LIN");
787 ConditionResult::from(!lin_segments.iter().any(|s| {
788 s.elements
789 .get(2)
790 .and_then(|e| e.first())
791 .map_or(false, |v| CODES.contains(&v.as_str()))
792 }))
793 }
794
795 /// [50] Wenn MP-ID aus RFF+Z56 mit MP-ID aus NAD+MS identisch ist
796 fn evaluate_50(&self, ctx: &EvaluationContext) -> ConditionResult {
797 let rff_segs = ctx.find_segments_with_qualifier("RFF", 0, "Z56");
798 let nad_segs = ctx.find_segments_with_qualifier("NAD", 0, "MS");
799 let rff_mp_id = rff_segs
800 .first()
801 .and_then(|s| s.elements.first())
802 .and_then(|e| e.get(1))
803 .map(|s| s.as_str());
804 let nad_mp_id = nad_segs
805 .first()
806 .and_then(|s| s.elements.get(1))
807 .and_then(|e| e.first())
808 .map(|s| s.as_str());
809 match (rff_mp_id, nad_mp_id) {
810 (Some(rff), Some(nad)) if !rff.is_empty() && !nad.is_empty() => {
811 ConditionResult::from(rff == nad)
812 }
813 _ => ConditionResult::Unknown,
814 }
815 }
816
817 /// [51] Wenn BGM+Z54 vorhanden
818 fn evaluate_51(&self, ctx: &EvaluationContext) -> ConditionResult {
819 ctx.has_qualifier("BGM", 0, "Z54")
820 }
821
822 /// [52] Wenn BGM+Z54 nicht vorhanden
823 fn evaluate_52(&self, ctx: &EvaluationContext) -> ConditionResult {
824 ctx.lacks_qualifier("BGM", 0, "Z54")
825 }
826
827 /// [53] Diese SG40 darf genau einmal in der SG36 angegeben werden
828 // 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)
829 fn evaluate_53(&self, ctx: &EvaluationContext) -> ConditionResult {
830 // Diese SG40 darf genau einmal in der SG36 angegeben werden
831 // For each SG36 instance, verify SG40 appears exactly once as a child group
832 let nav = match ctx.navigator() {
833 Some(n) => n,
834 None => return ConditionResult::Unknown,
835 };
836 let sg36_count = nav.group_instance_count(&["SG17", "SG36"]);
837 if sg36_count == 0 {
838 return ConditionResult::Unknown;
839 }
840 for i in 0..sg36_count {
841 let sg40_count = nav.child_group_instance_count(&["SG17", "SG36"], i, "SG40");
842 if sg40_count != 1 {
843 return ConditionResult::False;
844 }
845 }
846 ConditionResult::True
847 }
848
849 /// [64] Wenn der Zeitpunkt im DTM+157 DE2380 ≥ 01.01.2026, 00:00 Uhr gesetzlicher deutscher Zeit
850 fn evaluate_64(&self, ctx: &EvaluationContext) -> ConditionResult {
851 let dtm_segs = ctx.find_segments_with_qualifier("DTM", 0, "157");
852 match dtm_segs.first() {
853 Some(dtm) => {
854 let value = dtm
855 .elements
856 .first()
857 .and_then(|e| e.get(1))
858 .map(|s| s.as_str())
859 .unwrap_or("");
860 if value.len() >= 8 {
861 ConditionResult::from(&value[..8] >= "20260101")
862 } else {
863 ConditionResult::Unknown
864 }
865 }
866 None => ConditionResult::False, // segment absent → condition not applicable
867 }
868 }
869
870 /// [492] wenn MP-ID in NAD+MR aus Sparte Strom
871 /// EXTERNAL: Requires context from outside the message.
872 // 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)
873 fn evaluate_492(&self, ctx: &EvaluationContext) -> ConditionResult {
874 ctx.external.evaluate("recipient_is_strom")
875 }
876
877 /// [494] Das hier genannte Datum muss der Zeitpunkt sein, zu dem das Dokument erstellt wurde, oder ein Zeitpunkt, der davor liegt
878 fn evaluate_494(&self, _ctx: &EvaluationContext) -> ConditionResult {
879 // Hinweis: Das hier genannte Datum muss der Zeitpunkt sein, zu dem das Dokument
880 // erstellt wurde, oder ein Zeitpunkt, der davor liegt. Informational annotation
881 // about the semantic meaning of the date field — always applies unconditionally.
882 ConditionResult::True
883 }
884
885 /// [495] Der Zeitpunkt muss ≤ dem Wert im DE2380 des DTM+137 sein
886 // 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)
887 fn evaluate_495(&self, ctx: &EvaluationContext) -> ConditionResult {
888 // Der Zeitpunkt (DTM+492 Betrachtungszeitintervall) muss <= DTM+137 (Nachrichtendatum) sein
889 let dtm_137_segs = ctx.find_segments_with_qualifier("DTM", 0, "137");
890 let dtm_137 = match dtm_137_segs.first() {
891 Some(s) => s,
892 None => return ConditionResult::Unknown,
893 };
894 let threshold = match dtm_137.elements.first().and_then(|e| e.get(1)) {
895 Some(v) => v.clone(),
896 None => return ConditionResult::Unknown,
897 };
898 ctx.dtm_le("492", &threshold)
899 }
900
901 /// [502] Hinweis: Preis in Euro je MWh
902 fn evaluate_502(&self, _ctx: &EvaluationContext) -> ConditionResult {
903 ConditionResult::True
904 }
905
906 /// [503] Hinweis: Hier ist immer der Wert 1000 einzutragen, da in DE5118 der Preis in €/MWh angegeben wird.
907 fn evaluate_503(&self, _ctx: &EvaluationContext) -> ConditionResult {
908 ConditionResult::True
909 }
910
911 /// [504] Hinweis: Dokumentennummer der PRICAT
912 fn evaluate_504(&self, _ctx: &EvaluationContext) -> ConditionResult {
913 ConditionResult::True
914 }
915
916 /// [511] Hinweis: 1. Der genannte Wert gehört nicht zum Intervall. 2. Wenn in dieser SG36 die letzte Ziffer von LIN DE7140 >1, dann ist der Wert identisch mit dem Wert des DE6152 im RNG-Segment, in der...
917 fn evaluate_511(&self, _ctx: &EvaluationContext) -> ConditionResult {
918 // Hinweis: informational annotation about RNG value semantics and ordering rules across Artikel-IDs
919 ConditionResult::True
920 }
921
922 /// [512] Hinweis: Der genannte Wert gehört zum Intervall
923 fn evaluate_512(&self, _ctx: &EvaluationContext) -> ConditionResult {
924 ConditionResult::True
925 }
926
927 /// [513] Hinweis: Die zum Preis gehörende Einheit ist in der Codeliste definiert
928 fn evaluate_513(&self, _ctx: &EvaluationContext) -> ConditionResult {
929 ConditionResult::True
930 }
931
932 /// [519] Hinweis: Es darf nur eine Information im DE3148 übermittelt werden
933 fn evaluate_519(&self, _ctx: &EvaluationContext) -> ConditionResult {
934 ConditionResult::True
935 }
936
937 /// [520] Hinweis: Falls der Preis des Artikels gezont ist, ist diese SG40 so oft zu wiederholen, bis alle Preise zu diesem Artikel genannt sind
938 fn evaluate_520(&self, _ctx: &EvaluationContext) -> ConditionResult {
939 ConditionResult::True
940 }
941
942 /// [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...
943 fn evaluate_521(&self, _ctx: &EvaluationContext) -> ConditionResult {
944 // Hinweis: informational annotation about RNG DE value relationships — one RNG must have value 0,
945 // all others must equal DE6152 of another RNG for the same Artikel-ID
946 ConditionResult::True
947 }
948
949 /// [902] Format: Möglicher Wert: ≥ 0
950 fn evaluate_902(&self, _ctx: &EvaluationContext) -> ConditionResult {
951 ConditionResult::True
952 }
953
954 /// [908] Format: Mögliche Werte: 1 bis n
955 fn evaluate_908(&self, _ctx: &EvaluationContext) -> ConditionResult {
956 ConditionResult::True
957 }
958
959 /// [909] Format: Mögliche Werte: 0 bis n
960 // 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)
961 fn evaluate_909(&self, ctx: &EvaluationContext) -> ConditionResult {
962 // Format: Mögliche Werte 0 bis n — numeric value must be >= 0
963 // Applied to LIN DE1082 (Positionsnummer) which can start from 0
964 let lin_segs = ctx.find_segments("LIN");
965 if lin_segs.is_empty() {
966 return ConditionResult::Unknown;
967 }
968 for seg in &lin_segs {
969 let val = match seg.elements.first().and_then(|e| e.first()) {
970 Some(v) => v,
971 None => return ConditionResult::Unknown,
972 };
973 match validate_numeric(val, ">=", 0.0) {
974 ConditionResult::False => return ConditionResult::False,
975 ConditionResult::Unknown => return ConditionResult::Unknown,
976 ConditionResult::True => {}
977 }
978 }
979 ConditionResult::True
980 }
981
982 /// [911] Format: Mögliche Werte: 1 bis n, je Nachricht oder Segmentgruppe bei 1 beginnend und fortlaufend aufsteigend
983 // 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)
984 fn evaluate_911(&self, ctx: &EvaluationContext) -> ConditionResult {
985 // Format: Mögliche Werte 1 bis n, sequential per message/segment group starting at 1
986 // Validates LIN DE1082 (Positionsnummer) is sequential starting from 1
987 let lin_segs = ctx.find_segments("LIN");
988 if lin_segs.is_empty() {
989 return ConditionResult::Unknown;
990 }
991 let mut expected: u64 = 1;
992 for seg in &lin_segs {
993 let pos_str = match seg.elements.first().and_then(|e| e.first()) {
994 Some(v) => v,
995 None => return ConditionResult::Unknown,
996 };
997 let pos: u64 = match pos_str.parse() {
998 Ok(n) => n,
999 Err(_) => return ConditionResult::False,
1000 };
1001 if pos != expected {
1002 return ConditionResult::False;
1003 }
1004 expected += 1;
1005 }
1006 ConditionResult::True
1007 }
1008
1009 /// [912] Format: max. 6 Nachkommastellen
1010 // 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)
1011 fn evaluate_912(&self, ctx: &EvaluationContext) -> ConditionResult {
1012 ctx.format_check("PRI", 0, 1, |val| validate_max_decimal_places(val, 6))
1013 }
1014
1015 /// [926] Format: Möglicher Wert: 0
1016 // 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)
1017 fn evaluate_926(&self, ctx: &EvaluationContext) -> ConditionResult {
1018 ctx.format_check("QTY", 0, 1, |val| validate_numeric(val, "==", 0.0))
1019 }
1020
1021 /// [929] Format: Möglicher Wert: 1000
1022 // 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)
1023 fn evaluate_929(&self, ctx: &EvaluationContext) -> ConditionResult {
1024 ctx.format_check("QTY", 0, 1, |val| validate_numeric(val, "==", 1000.0))
1025 }
1026
1027 /// [931] Format: ZZZ = +00
1028 // 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)
1029 fn evaluate_931(&self, ctx: &EvaluationContext) -> ConditionResult {
1030 // ZZZ = +00: timezone offset in DTM value must be +00
1031 // DTM format 303: YYYYMMDDHHMMzzz, timezone encoded as last part of elements[0][1]
1032 let dtm_segments = ctx.find_segments("DTM");
1033 for seg in &dtm_segments {
1034 if let Some(value) = seg.elements.first().and_then(|e| e.get(1)) {
1035 if !value.is_empty() {
1036 // Timezone +00 appears at end of value string
1037 if value.ends_with("+00") {
1038 return ConditionResult::True;
1039 } else {
1040 return ConditionResult::False;
1041 }
1042 }
1043 }
1044 }
1045 ConditionResult::Unknown
1046 }
1047
1048 /// [932] Format: HHMM = 2200
1049 // 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)
1050 fn evaluate_932(&self, ctx: &EvaluationContext) -> ConditionResult {
1051 // HHMM = 2200: time value in DTM must be 2200
1052 // In DTM format 203 (YYYYMMDDHHMM) or 401 (HHMM), the time portion is HHMM
1053 let dtm_segments = ctx.find_segments("DTM");
1054 for seg in &dtm_segments {
1055 if let Some(value) = seg.elements.first().and_then(|e| e.get(1)) {
1056 let format_code = seg
1057 .elements
1058 .first()
1059 .and_then(|e| e.get(2))
1060 .map(|s| s.as_str())
1061 .unwrap_or("");
1062 let time_part = match format_code {
1063 "203" if value.len() >= 12 => &value[8..12],
1064 "401" if value.len() >= 4 => &value[0..4],
1065 _ if value.len() >= 12 => &value[8..12],
1066 _ => continue,
1067 };
1068 if time_part == "2200" {
1069 return ConditionResult::True;
1070 } else {
1071 return ConditionResult::False;
1072 }
1073 }
1074 }
1075 ConditionResult::Unknown
1076 }
1077
1078 /// [933] Format: HHMM = 2300
1079 // 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)
1080 fn evaluate_933(&self, ctx: &EvaluationContext) -> ConditionResult {
1081 // HHMM = 2300: time value in DTM must be 2300
1082 let dtm_segments = ctx.find_segments("DTM");
1083 for seg in &dtm_segments {
1084 if let Some(value) = seg.elements.first().and_then(|e| e.get(1)) {
1085 let format_code = seg
1086 .elements
1087 .first()
1088 .and_then(|e| e.get(2))
1089 .map(|s| s.as_str())
1090 .unwrap_or("");
1091 let time_part = match format_code {
1092 "203" if value.len() >= 12 => &value[8..12],
1093 "401" if value.len() >= 4 => &value[0..4],
1094 _ if value.len() >= 12 => &value[8..12],
1095 _ => continue,
1096 };
1097 if time_part == "2300" {
1098 return ConditionResult::True;
1099 } else {
1100 return ConditionResult::False;
1101 }
1102 }
1103 }
1104 ConditionResult::Unknown
1105 }
1106
1107 /// [937] Format: keine Nachkommastelle
1108 // 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)
1109 fn evaluate_937(&self, ctx: &EvaluationContext) -> ConditionResult {
1110 ctx.format_check("QTY", 0, 1, |val| validate_max_decimal_places(val, 0))
1111 }
1112
1113 /// [939] Format: Die Zeichenkette muss die Zeichen @ und . enthalten
1114 // 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)
1115 fn evaluate_939(&self, ctx: &EvaluationContext) -> ConditionResult {
1116 // Email format: string must contain both '@' and '.'
1117 // Typically applies to COM segment (communication channel EM = email)
1118 let com_segments = ctx.find_segments("COM");
1119 for seg in &com_segments {
1120 // COM: elements[0][0] = communication address, elements[0][1] = channel code
1121 let channel = seg
1122 .elements
1123 .first()
1124 .and_then(|e| e.get(1))
1125 .map(|s| s.as_str())
1126 .unwrap_or("");
1127 if channel == "EM" {
1128 if let Some(address) = seg.elements.first().and_then(|e| e.first()) {
1129 let contains_at = address.contains('@');
1130 let contains_dot = address.contains('.');
1131 return ConditionResult::from(contains_at && contains_dot);
1132 }
1133 }
1134 }
1135 // Fallback: check any COM value for @ and .
1136 for seg in &com_segments {
1137 if let Some(address) = seg.elements.first().and_then(|e| e.first()) {
1138 if !address.is_empty() {
1139 return ConditionResult::from(address.contains('@') && address.contains('.'));
1140 }
1141 }
1142 }
1143 ConditionResult::Unknown
1144 }
1145
1146 /// [940] Format: Die Zeichenkette muss mit dem Zeichen + beginnen und danach dürfen nur noch Ziffern folgen
1147 // 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)
1148 fn evaluate_940(&self, ctx: &EvaluationContext) -> ConditionResult {
1149 // Phone number format: must start with '+' followed only by digits
1150 // Typically applies to COM segment (TEL/FAX channel)
1151 let com_segments = ctx.find_segments("COM");
1152 for seg in &com_segments {
1153 let channel = seg
1154 .elements
1155 .first()
1156 .and_then(|e| e.get(1))
1157 .map(|s| s.as_str())
1158 .unwrap_or("");
1159 if matches!(channel, "TE" | "FX" | "AJ") {
1160 if let Some(number) = seg.elements.first().and_then(|e| e.first()) {
1161 if !number.is_empty() {
1162 let valid = number.starts_with('+')
1163 && number.len() > 1
1164 && number[1..].chars().all(|c| c.is_ascii_digit());
1165 return ConditionResult::from(valid);
1166 }
1167 }
1168 }
1169 }
1170 // Fallback: check any COM value for +digit format
1171 for seg in &com_segments {
1172 if let Some(number) = seg.elements.first().and_then(|e| e.first()) {
1173 if !number.is_empty() {
1174 let valid = number.starts_with('+')
1175 && number.len() > 1
1176 && number[1..].chars().all(|c| c.is_ascii_digit());
1177 return ConditionResult::from(valid);
1178 }
1179 }
1180 }
1181 ConditionResult::Unknown
1182 }
1183
1184 /// [941] Format: Artikelnummer
1185 // 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)
1186 fn evaluate_941(&self, ctx: &EvaluationContext) -> ConditionResult {
1187 ctx.format_check("PIA", 1, 0, |val| {
1188 validate_artikel_pattern(val, &[1, 2, 1, 3])
1189 })
1190 }
1191
1192 /// [942] Format: n1-n2-n1-n3
1193 fn evaluate_942(&self, ctx: &EvaluationContext) -> ConditionResult {
1194 ctx.format_check("PIA", 1, 0, |val| {
1195 validate_artikel_pattern(val, &[1, 2, 1, 3])
1196 })
1197 }
1198
1199 /// [946] Format: max. 11 Nachkommastellen
1200 // 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)
1201 fn evaluate_946(&self, ctx: &EvaluationContext) -> ConditionResult {
1202 ctx.format_check("PRI", 0, 1, |val| validate_max_decimal_places(val, 11))
1203 }
1204
1205 /// [948] Format: n1-n2-n1-n8-n2
1206 fn evaluate_948(&self, ctx: &EvaluationContext) -> ConditionResult {
1207 ctx.format_check("PIA", 1, 0, |val| {
1208 validate_artikel_pattern(val, &[1, 2, 1, 8, 2])
1209 })
1210 }
1211
1212 /// [949] Format: n1-n2-n1-n8-n2-n1
1213 fn evaluate_949(&self, ctx: &EvaluationContext) -> ConditionResult {
1214 ctx.format_check("PIA", 1, 0, |val| {
1215 validate_artikel_pattern(val, &[1, 2, 1, 8, 2, 1])
1216 })
1217 }
1218
1219 /// [957] Format: n1-n2-n1-n8
1220 fn evaluate_957(&self, ctx: &EvaluationContext) -> ConditionResult {
1221 ctx.format_check("PIA", 1, 0, |val| {
1222 validate_artikel_pattern(val, &[1, 2, 1, 8])
1223 })
1224 }
1225
1226 /// [959] Format: n13-n2
1227 // 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)
1228 fn evaluate_959(&self, ctx: &EvaluationContext) -> ConditionResult {
1229 ctx.format_check("PIA", 1, 0, |val| validate_artikel_pattern(val, &[13, 2]))
1230 }
1231
1232 /// [968] Format: Möglicher Wert: ≤ 0
1233 // 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)
1234 fn evaluate_968(&self, ctx: &EvaluationContext) -> ConditionResult {
1235 ctx.format_check("PRI", 0, 1, |val| validate_numeric(val, "<=", 0.0))
1236 }
1237}