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 // HAND-EDITED: "in dieser/derselben SG36" — scoped to the enclosing group.
348 let values = ctx.this_group_values("LIN", 2, 0, &["SG36"]);
349 ConditionResult::from(values.iter().any(|(_, v)| v == "9990001000813"))
350 }
351
352 /// [3] Wenn IMD+X vorhanden
353 fn evaluate_3(&self, ctx: &EvaluationContext) -> ConditionResult {
354 ctx.has_qualifier("IMD", 0, "X")
355 }
356
357 /// [4] Wenn SG36 IMD+C in diesem IMD vorhanden
358 fn evaluate_4(&self, ctx: &EvaluationContext) -> ConditionResult {
359 ctx.any_group_has_qualifier("IMD", 0, "C", &["SG36"])
360 }
361
362 /// [5] Wenn SG36 IMD+X in diesem IMD vorhanden
363 fn evaluate_5(&self, ctx: &EvaluationContext) -> ConditionResult {
364 ctx.any_group_has_qualifier("IMD", 0, "X", &["SG36"])
365 }
366
367 /// [6] Wenn in dieser SG36 LIN in DE7140 9990001000798 vorhanden
368 fn evaluate_6(&self, ctx: &EvaluationContext) -> ConditionResult {
369 // HAND-EDITED: "in dieser/derselben SG36" — scoped to the enclosing group.
370 let values = ctx.this_group_values("LIN", 2, 0, &["SG36"]);
371 ConditionResult::from(values.iter().any(|(_, v)| v == "9990001000798"))
372 }
373
374 /// [7] Wenn in dieser SG36 LIN in DE7140 9990001000798 nicht vorhanden
375 fn evaluate_7(&self, ctx: &EvaluationContext) -> ConditionResult {
376 // HAND-EDITED: "in dieser/derselben SG36" — scoped to the enclosing group.
377 let values = ctx.this_group_values("LIN", 2, 0, &["SG36"]);
378 ConditionResult::from(!values.iter().any(|(_, v)| v == "9990001000798"))
379 }
380
381 /// [9] Wenn BGM DE1373 =11 nicht vorhanden
382 fn evaluate_9(&self, ctx: &EvaluationContext) -> ConditionResult {
383 match ctx.find_segment("BGM") {
384 None => ConditionResult::False, // segment absent → condition not applicable
385 Some(seg) => {
386 let val = seg
387 .elements
388 .get(4)
389 .and_then(|e| e.first())
390 .map(|s| s.as_str())
391 .unwrap_or("");
392 ConditionResult::from(val != "11")
393 }
394 }
395 }
396
397 /// [12] je UNB ist nur eine Nachricht mit BGM+Z04 in der Übertragungsdatei erlaubt (nur eine Nachricht je Übertragungsdatei)
398 // 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)
399 fn evaluate_12(&self, ctx: &EvaluationContext) -> ConditionResult {
400 ctx.has_qualifier("BGM", 0, "Z04")
401 }
402
403 /// [14] je UNB ist maximal je Code aus DE1001 eine Nachricht in der Übertragungsdatei erlaubt
404 /// EXTERNAL: Requires context from outside the message.
405 // 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)
406 fn evaluate_14(&self, ctx: &EvaluationContext) -> ConditionResult {
407 ctx.external.evaluate("max_one_message_per_type_in_unb")
408 }
409
410 /// [19] Nur MP-ID aus Sparte Strom
411 /// EXTERNAL: Requires context from outside the message.
412 // 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)
413 fn evaluate_19(&self, ctx: &EvaluationContext) -> ConditionResult {
414 ctx.external.evaluate("mp_id_is_strom_sector")
415 }
416
417 /// [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
418 /// EXTERNAL: Requires context from outside the message.
419 // 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)
420 fn evaluate_22(&self, ctx: &EvaluationContext) -> ConditionResult {
421 ctx.external.evaluate("article_id_has_pricat_price_flag")
422 }
423
424 /// [24] Wenn in dieser SG36 Wert von LIN DE7140 im Format n1-n2-n1-n8-n2-n1
425 fn evaluate_24(&self, ctx: &EvaluationContext) -> ConditionResult {
426 // HAND-EDITED: "in dieser/derselben SG36" — scoped to the enclosing group.
427 let values = ctx.this_group_values("LIN", 2, 0, &["SG36"]);
428 ConditionResult::from(values.iter().any(|(_, val)| {
429 let parts: Vec<&str> = val.split('-').collect();
430 if parts.len() != 6 {
431 return false;
432 }
433 let expected_lens = [1usize, 2, 1, 8, 2, 1];
434 parts
435 .iter()
436 .zip(expected_lens.iter())
437 .all(|(p, &len)| p.len() == len && p.chars().all(|ch| ch.is_ascii_digit()))
438 }))
439 }
440
441 /// [26] Wenn BGM DE1001 = Z70 vorhanden
442 fn evaluate_26(&self, ctx: &EvaluationContext) -> ConditionResult {
443 ctx.has_qualifier("BGM", 0, "Z70")
444 }
445
446 /// [27] Wenn BGM DE1001 = Z70 nicht vorhanden
447 fn evaluate_27(&self, ctx: &EvaluationContext) -> ConditionResult {
448 ctx.lacks_qualifier("BGM", 0, "Z70")
449 }
450
451 /// [28] Wenn die zugehörige Artikel-ID in der letzten Stelle eine 1 ist
452 // 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)
453 fn evaluate_28(&self, ctx: &EvaluationContext) -> ConditionResult {
454 // Wenn die zugehörige Artikel-ID in der letzten Stelle eine 1 ist
455 // Artikel-ID is in PIA segment, elements[1][0]
456 let segs = ctx.find_segments("PIA");
457 match segs
458 .first()
459 .and_then(|s| s.elements.get(1))
460 .and_then(|e| e.first())
461 {
462 Some(val) => match val.chars().last() {
463 Some('1') => ConditionResult::True,
464 Some(c) if c.is_ascii_digit() => ConditionResult::False,
465 Some(_) => ConditionResult::False,
466 None => ConditionResult::False, // segment absent → condition not applicable
467 },
468 None => ConditionResult::False, // segment absent → condition not applicable
469 }
470 }
471
472 /// [29] Wenn die zugehörige Artikel-ID in der letzten Stelle > 1 ist
473 // 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)
474 fn evaluate_29(&self, ctx: &EvaluationContext) -> ConditionResult {
475 // Wenn die zugehörige Artikel-ID in der letzten Stelle > 1 ist
476 // Artikel-ID is in PIA segment, elements[1][0]
477 let segs = ctx.find_segments("PIA");
478 match segs
479 .first()
480 .and_then(|s| s.elements.get(1))
481 .and_then(|e| e.first())
482 {
483 Some(val) => match val.chars().last() {
484 Some(c) if c.is_ascii_digit() && c > '1' => ConditionResult::True,
485 Some(c) if c.is_ascii_digit() => ConditionResult::False,
486 Some(_) => ConditionResult::False,
487 None => ConditionResult::False, // segment absent → condition not applicable
488 },
489 None => ConditionResult::False, // segment absent → condition not applicable
490 }
491 }
492
493 /// [30] wenn MP-ID in SG2 NAD+MR in der Rolle LF
494 /// EXTERNAL: Requires context from outside the message.
495 fn evaluate_30(&self, ctx: &EvaluationContext) -> ConditionResult {
496 ctx.external.evaluate("recipient_is_lf")
497 }
498
499 /// [31] wenn BGM DE1001 = Z32
500 fn evaluate_31(&self, ctx: &EvaluationContext) -> ConditionResult {
501 ctx.has_qualifier("BGM", 0, "Z32")
502 }
503
504 /// [32] wenn der Zeitpunkt im DTM+157 DE2380 < 01.01.2024 00:00 Uhr gesetzlicher deutscher Zeit
505 // 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)
506 fn evaluate_32(&self, ctx: &EvaluationContext) -> ConditionResult {
507 {
508 let dtms = ctx.find_segments_with_qualifier("DTM", 0, "157");
509 match dtms.first() {
510 Some(dtm) => {
511 match dtm
512 .elements
513 .first()
514 .and_then(|e| e.get(1))
515 .map(|s| s.as_str())
516 {
517 Some(value) if !value.is_empty() => {
518 // Format 303: YYYYMMDDHHMM — string comparison valid for numeric date strings
519 // 01.01.2024 00:00 German legal time → "202401010000"
520 ConditionResult::from(value < "202401010000")
521 }
522 _ => ConditionResult::Unknown,
523 }
524 }
525 None => ConditionResult::False, // segment absent → condition not applicable
526 }
527 }
528 }
529
530 /// [33] wenn der Zeitpunkt im DTM+157 DE2380 ≥ 01.01.2024 00:00 Uhr gesetzlicher deutscher Zeit
531 // REVIEW: Mirror of condition 32 with >= comparison. DTM+157 DE2380 at elements[0][1], format 303. Returns Unknown if segment absent. (medium confidence)
532 fn evaluate_33(&self, ctx: &EvaluationContext) -> ConditionResult {
533 {
534 let dtms = ctx.find_segments_with_qualifier("DTM", 0, "157");
535 match dtms.first() {
536 Some(dtm) => {
537 match dtm
538 .elements
539 .first()
540 .and_then(|e| e.get(1))
541 .map(|s| s.as_str())
542 {
543 Some(value) if !value.is_empty() => {
544 // Format 303: YYYYMMDDHHMM — string comparison valid for numeric date strings
545 // 01.01.2024 00:00 German legal time → "202401010000"
546 ConditionResult::from(value >= "202401010000")
547 }
548 _ => ConditionResult::Unknown,
549 }
550 }
551 None => ConditionResult::False, // segment absent → condition not applicable
552 }
553 }
554 }
555
556 /// [34] wenn BGM DE1001 = Z77
557 fn evaluate_34(&self, ctx: &EvaluationContext) -> ConditionResult {
558 ctx.has_qualifier("BGM", 0, "Z77")
559 }
560
561 /// [36] Wenn MP-ID in SG2 NAD+MR in der Rolle NB
562 /// EXTERNAL: Requires context from outside the message.
563 fn evaluate_36(&self, ctx: &EvaluationContext) -> ConditionResult {
564 ctx.external.evaluate("recipient_is_nb")
565 }
566
567 /// [37] Wenn im DE3155 in demselben COM der Code EM vorhanden ist
568 // 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)
569 fn evaluate_37(&self, ctx: &EvaluationContext) -> ConditionResult {
570 {
571 let coms = ctx.find_segments("COM");
572 ConditionResult::from(coms.iter().any(|s| {
573 s.elements
574 .first()
575 .and_then(|e| e.get(1))
576 .is_some_and(|v| v == "EM")
577 }))
578 }
579 }
580
581 /// [38] Wenn im DE3155 in demselben COM der Code TE / FX / AJ / AL vorhanden ist
582 // REVIEW: Same COM segment structure as condition 37. Checks DE3155 for any of TE (telephone), FX (fax), AJ, AL channel codes. (medium confidence)
583 fn evaluate_38(&self, ctx: &EvaluationContext) -> ConditionResult {
584 {
585 let coms = ctx.find_segments("COM");
586 ConditionResult::from(coms.iter().any(|s| {
587 s.elements
588 .first()
589 .and_then(|e| e.get(1))
590 .is_some_and(|v| matches!(v.as_str(), "TE" | "FX" | "AJ" | "AL"))
591 }))
592 }
593 }
594
595 /// [39] Es sind nur Werte aus der EDI@Energy Codeliste der Artikelnummern und Artikel-ID erlaubt, die in der Spalte MaBiS ein X haben
596 /// EXTERNAL: Requires context from outside the message.
597 fn evaluate_39(&self, ctx: &EvaluationContext) -> ConditionResult {
598 ctx.external.evaluate("article_id_in_mabis_codelist")
599 }
600
601 /// [40] Es sind nur Werte aus der EDI@Energy Codeliste der Artikelnummern und Artikel-ID erlaubt, die in der Spalte H ein X haben
602 /// EXTERNAL: Requires context from outside the message.
603 fn evaluate_40(&self, ctx: &EvaluationContext) -> ConditionResult {
604 ctx.external.evaluate("article_id_in_h_codelist")
605 }
606
607 /// [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
608 /// EXTERNAL: Requires context from outside the message.
609 fn evaluate_41(&self, ctx: &EvaluationContext) -> ConditionResult {
610 ctx.external.evaluate("article_id_in_pricat_codelist")
611 }
612
613 /// [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
614 /// EXTERNAL: Requires context from outside the message.
615 fn evaluate_42(&self, ctx: &EvaluationContext) -> ConditionResult {
616 ctx.external
617 .evaluate("article_id_valid_pricat_formation_rule")
618 }
619
620 /// [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
621 // 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)
622 fn evaluate_43(&self, ctx: &EvaluationContext) -> ConditionResult {
623 let bgm = match ctx.find_segment("BGM") {
624 Some(s) => s,
625 None => return ConditionResult::Unknown,
626 };
627 if bgm
628 .elements
629 .first()
630 .and_then(|e| e.first())
631 .map(|s| s.as_str())
632 != Some("Z32")
633 {
634 return ConditionResult::False;
635 }
636 let lin_segments = ctx.find_segments("LIN");
637 let has_hyphen_format = lin_segments.iter().any(|s| {
638 s.elements
639 .get(2)
640 .and_then(|e| e.first())
641 .map_or(false, |v| v.contains('-'))
642 });
643 if !has_hyphen_format {
644 return ConditionResult::False;
645 }
646 let dtm_segs = ctx.find_segments_with_qualifier("DTM", 0, "157");
647 match dtm_segs.first() {
648 Some(dtm) => {
649 let value = dtm
650 .elements
651 .first()
652 .and_then(|e| e.get(1))
653 .map(|s| s.as_str())
654 .unwrap_or("");
655 if value.len() >= 8 {
656 ConditionResult::from(&value[..8] >= "20240101")
657 } else {
658 ConditionResult::Unknown
659 }
660 }
661 None => ConditionResult::False, // segment absent → condition not applicable
662 }
663 }
664
665 /// [44] Wenn BGM DE1001 = Z77, dann muss der hier genannte Zeitpunkt ≥ 01.10.2023 00:00 Uhr gesetzlicher deutscher Zeit sein.
666 // 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)
667 fn evaluate_44(&self, ctx: &EvaluationContext) -> ConditionResult {
668 let bgm = match ctx.find_segment("BGM") {
669 Some(s) => s,
670 None => return ConditionResult::Unknown,
671 };
672 if bgm
673 .elements
674 .first()
675 .and_then(|e| e.first())
676 .map(|s| s.as_str())
677 != Some("Z77")
678 {
679 return ConditionResult::False;
680 }
681 let dtm_segs = ctx.find_segments_with_qualifier("DTM", 0, "157");
682 match dtm_segs.first() {
683 Some(dtm) => {
684 let value = dtm
685 .elements
686 .first()
687 .and_then(|e| e.get(1))
688 .map(|s| s.as_str())
689 .unwrap_or("");
690 if value.len() >= 8 {
691 ConditionResult::from(&value[..8] >= "20231001")
692 } else {
693 ConditionResult::Unknown
694 }
695 }
696 None => ConditionResult::False, // segment absent → condition not applicable
697 }
698 }
699
700 /// [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
701 // 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)
702 fn evaluate_47(&self, ctx: &EvaluationContext) -> ConditionResult {
703 let bgm = match ctx.find_segment("BGM") {
704 Some(s) => s,
705 None => return ConditionResult::Unknown,
706 };
707 if bgm
708 .elements
709 .first()
710 .and_then(|e| e.first())
711 .map(|s| s.as_str())
712 != Some("Z32")
713 {
714 return ConditionResult::False;
715 }
716 let lin_segments = ctx.find_segments("LIN");
717 let has_n13_format = lin_segments.iter().any(|s| {
718 s.elements
719 .get(2)
720 .and_then(|e| e.first())
721 .map_or(false, |v| {
722 !v.contains('-') && v.len() == 13 && v.chars().all(|c| c.is_ascii_digit())
723 })
724 });
725 if !has_n13_format {
726 return ConditionResult::False;
727 }
728 let dtm_segs = ctx.find_segments_with_qualifier("DTM", 0, "157");
729 match dtm_segs.first() {
730 Some(dtm) => {
731 let value = dtm
732 .elements
733 .first()
734 .and_then(|e| e.get(1))
735 .map(|s| s.as_str())
736 .unwrap_or("");
737 if value.len() >= 8 {
738 ConditionResult::from(&value[..8] < "20240101")
739 } else {
740 ConditionResult::Unknown
741 }
742 }
743 None => ConditionResult::False, // segment absent → condition not applicable
744 }
745 }
746
747 /// [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...
748 fn evaluate_48(&self, ctx: &EvaluationContext) -> ConditionResult {
749 // HAND-EDITED: "in dieser/derselben SG36" — scoped to the enclosing group.
750 const CODES: &[&str] = &[
751 "1-01-6-005",
752 "1-01-9-001",
753 "1-01-9-002",
754 "1-02-0-015",
755 "1-03-8-001",
756 "1-03-8-002",
757 "1-03-8-003",
758 "1-03-8-004",
759 "1-03-9-001",
760 "1-03-9-002",
761 "1-03-9-003",
762 "1-03-9-004",
763 "1-07-4-001",
764 ];
765 let lin_segments = ctx.scoped_find_segments_in("SG36", "LIN");
766 ConditionResult::from(lin_segments.iter().any(|s| {
767 s.elements
768 .get(2)
769 .and_then(|e| e.first())
770 .map_or(false, |v| CODES.contains(&v.as_str()))
771 }))
772 }
773
774 /// [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...
775 fn evaluate_49(&self, ctx: &EvaluationContext) -> ConditionResult {
776 // HAND-EDITED: "in dieser/derselben SG36" — scoped to the enclosing group.
777 const CODES: &[&str] = &[
778 "1-01-6-005",
779 "1-01-9-001",
780 "1-01-9-002",
781 "1-02-0-015",
782 "1-03-8-001",
783 "1-03-8-002",
784 "1-03-8-003",
785 "1-03-8-004",
786 "1-03-9-001",
787 "1-03-9-002",
788 "1-03-9-003",
789 "1-03-9-004",
790 "1-07-4-001",
791 ];
792 let lin_segments = ctx.scoped_find_segments_in("SG36", "LIN");
793 ConditionResult::from(!lin_segments.iter().any(|s| {
794 s.elements
795 .get(2)
796 .and_then(|e| e.first())
797 .map_or(false, |v| CODES.contains(&v.as_str()))
798 }))
799 }
800
801 /// [50] Wenn MP-ID aus RFF+Z56 mit MP-ID aus NAD+MS identisch ist
802 fn evaluate_50(&self, ctx: &EvaluationContext) -> ConditionResult {
803 let rff_segs = ctx.find_segments_with_qualifier("RFF", 0, "Z56");
804 let nad_segs = ctx.find_segments_with_qualifier("NAD", 0, "MS");
805 let rff_mp_id = rff_segs
806 .first()
807 .and_then(|s| s.elements.first())
808 .and_then(|e| e.get(1))
809 .map(|s| s.as_str());
810 let nad_mp_id = nad_segs
811 .first()
812 .and_then(|s| s.elements.get(1))
813 .and_then(|e| e.first())
814 .map(|s| s.as_str());
815 match (rff_mp_id, nad_mp_id) {
816 (Some(rff), Some(nad)) if !rff.is_empty() && !nad.is_empty() => {
817 ConditionResult::from(rff == nad)
818 }
819 _ => ConditionResult::Unknown,
820 }
821 }
822
823 /// [51] Wenn BGM+Z54 vorhanden
824 fn evaluate_51(&self, ctx: &EvaluationContext) -> ConditionResult {
825 ctx.has_qualifier("BGM", 0, "Z54")
826 }
827
828 /// [52] Wenn BGM+Z54 nicht vorhanden
829 fn evaluate_52(&self, ctx: &EvaluationContext) -> ConditionResult {
830 ctx.lacks_qualifier("BGM", 0, "Z54")
831 }
832
833 /// [53] Diese SG40 darf genau einmal in der SG36 angegeben werden
834 // 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)
835 fn evaluate_53(&self, ctx: &EvaluationContext) -> ConditionResult {
836 // Diese SG40 darf genau einmal in der SG36 angegeben werden
837 // For each SG36 instance, verify SG40 appears exactly once as a child group
838 let nav = match ctx.navigator() {
839 Some(n) => n,
840 None => return ConditionResult::Unknown,
841 };
842 let sg36_count = nav.group_instance_count(&["SG17", "SG36"]);
843 if sg36_count == 0 {
844 return ConditionResult::Unknown;
845 }
846 for i in 0..sg36_count {
847 let sg40_count = nav.child_group_instance_count(&["SG17", "SG36"], i, "SG40");
848 if sg40_count != 1 {
849 return ConditionResult::False;
850 }
851 }
852 ConditionResult::True
853 }
854
855 /// [64] Wenn der Zeitpunkt im DTM+157 DE2380 ≥ 01.01.2026, 00:00 Uhr gesetzlicher deutscher Zeit
856 fn evaluate_64(&self, ctx: &EvaluationContext) -> ConditionResult {
857 let dtm_segs = ctx.find_segments_with_qualifier("DTM", 0, "157");
858 match dtm_segs.first() {
859 Some(dtm) => {
860 let value = dtm
861 .elements
862 .first()
863 .and_then(|e| e.get(1))
864 .map(|s| s.as_str())
865 .unwrap_or("");
866 if value.len() >= 8 {
867 ConditionResult::from(&value[..8] >= "20260101")
868 } else {
869 ConditionResult::Unknown
870 }
871 }
872 None => ConditionResult::False, // segment absent → condition not applicable
873 }
874 }
875
876 /// [492] wenn MP-ID in NAD+MR aus Sparte Strom
877 /// EXTERNAL: Requires context from outside the message.
878 // 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)
879 fn evaluate_492(&self, ctx: &EvaluationContext) -> ConditionResult {
880 ctx.external.evaluate("recipient_is_strom")
881 }
882
883 /// [494] Das hier genannte Datum muss der Zeitpunkt sein, zu dem das Dokument erstellt wurde, oder ein Zeitpunkt, der davor liegt
884 fn evaluate_494(&self, _ctx: &EvaluationContext) -> ConditionResult {
885 // Hinweis: Das hier genannte Datum muss der Zeitpunkt sein, zu dem das Dokument
886 // erstellt wurde, oder ein Zeitpunkt, der davor liegt. Informational annotation
887 // about the semantic meaning of the date field — always applies unconditionally.
888 ConditionResult::True
889 }
890
891 /// [495] Der Zeitpunkt muss ≤ dem Wert im DE2380 des DTM+137 sein
892 fn evaluate_495(&self, ctx: &EvaluationContext) -> ConditionResult {
893 // HAND-EDITED: subject is the current DTM's DE2380 (via ctx.resolved_value),
894 // compared with the message date (DTM+137). Lexicographic compare is valid
895 // for fixed-width CCYYMMDDHHMM+ZZ (format 303).
896 let current = match ctx.resolved_value {
897 Some(v) if !v.is_empty() => v,
898 _ => return ConditionResult::Unknown,
899 };
900 let threshold = match ctx
901 .find_segments_with_qualifier("DTM", 0, "137")
902 .first()
903 .and_then(|s| s.elements.first())
904 .and_then(|e| e.get(1))
905 {
906 Some(v) if !v.is_empty() => v.clone(),
907 _ => return ConditionResult::Unknown,
908 };
909 ConditionResult::from(current <= threshold.as_str())
910 }
911
912 /// [502] Hinweis: Preis in Euro je MWh
913 fn evaluate_502(&self, _ctx: &EvaluationContext) -> ConditionResult {
914 ConditionResult::True
915 }
916
917 /// [503] Hinweis: Hier ist immer der Wert 1000 einzutragen, da in DE5118 der Preis in €/MWh angegeben wird.
918 fn evaluate_503(&self, _ctx: &EvaluationContext) -> ConditionResult {
919 ConditionResult::True
920 }
921
922 /// [504] Hinweis: Dokumentennummer der PRICAT
923 fn evaluate_504(&self, _ctx: &EvaluationContext) -> ConditionResult {
924 ConditionResult::True
925 }
926
927 /// [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...
928 fn evaluate_511(&self, _ctx: &EvaluationContext) -> ConditionResult {
929 // Hinweis: informational annotation about RNG value semantics and ordering rules across Artikel-IDs
930 ConditionResult::True
931 }
932
933 /// [512] Hinweis: Der genannte Wert gehört zum Intervall
934 fn evaluate_512(&self, _ctx: &EvaluationContext) -> ConditionResult {
935 ConditionResult::True
936 }
937
938 /// [513] Hinweis: Die zum Preis gehörende Einheit ist in der Codeliste definiert
939 fn evaluate_513(&self, _ctx: &EvaluationContext) -> ConditionResult {
940 ConditionResult::True
941 }
942
943 /// [519] Hinweis: Es darf nur eine Information im DE3148 übermittelt werden
944 fn evaluate_519(&self, _ctx: &EvaluationContext) -> ConditionResult {
945 ConditionResult::True
946 }
947
948 /// [520] Hinweis: Falls der Preis des Artikels gezont ist, ist diese SG40 so oft zu wiederholen, bis alle Preise zu diesem Artikel genannt sind
949 fn evaluate_520(&self, _ctx: &EvaluationContext) -> ConditionResult {
950 ConditionResult::True
951 }
952
953 /// [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...
954 fn evaluate_521(&self, _ctx: &EvaluationContext) -> ConditionResult {
955 // Hinweis: informational annotation about RNG DE value relationships — one RNG must have value 0,
956 // all others must equal DE6152 of another RNG for the same Artikel-ID
957 ConditionResult::True
958 }
959
960 /// [902] Format: Möglicher Wert: ≥ 0
961 fn evaluate_902(&self, ctx: &EvaluationContext) -> ConditionResult {
962 // HAND-EDITED: judged on the value it annotates; without one it
963 // says nothing.
964 match ctx.resolved_value.filter(|v| !v.is_empty()) {
965 Some(value) => validate_numeric(value, ">=", 0.0),
966 None => ConditionResult::True,
967 }
968 }
969
970 /// [908] Format: Mögliche Werte: 1 bis n
971 fn evaluate_908(&self, _ctx: &EvaluationContext) -> ConditionResult {
972 ConditionResult::True
973 }
974
975 /// [909] Format: Mögliche Werte: 0 bis n
976 // 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)
977 fn evaluate_909(&self, ctx: &EvaluationContext) -> ConditionResult {
978 // Format: Mögliche Werte 0 bis n — numeric value must be >= 0
979 // Applied to LIN DE1082 (Positionsnummer) which can start from 0
980 let lin_segs = ctx.find_segments("LIN");
981 if lin_segs.is_empty() {
982 return ConditionResult::Unknown;
983 }
984 for seg in &lin_segs {
985 let val = match seg.elements.first().and_then(|e| e.first()) {
986 Some(v) => v,
987 None => return ConditionResult::Unknown,
988 };
989 match validate_numeric(val, ">=", 0.0) {
990 ConditionResult::False => return ConditionResult::False,
991 ConditionResult::Unknown => return ConditionResult::Unknown,
992 ConditionResult::True => {}
993 }
994 }
995 ConditionResult::True
996 }
997
998 /// [911] Format: Mögliche Werte: 1 bis n, je Nachricht oder Segmentgruppe bei 1 beginnend und fortlaufend aufsteigend
999 // 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)
1000 fn evaluate_911(&self, ctx: &EvaluationContext) -> ConditionResult {
1001 // Format: Mögliche Werte 1 bis n, sequential per message/segment group starting at 1
1002 // Validates LIN DE1082 (Positionsnummer) is sequential starting from 1
1003 let lin_segs = ctx.find_segments("LIN");
1004 if lin_segs.is_empty() {
1005 return ConditionResult::Unknown;
1006 }
1007 let mut expected: u64 = 1;
1008 for seg in &lin_segs {
1009 let pos_str = match seg.elements.first().and_then(|e| e.first()) {
1010 Some(v) => v,
1011 None => return ConditionResult::Unknown,
1012 };
1013 let pos: u64 = match pos_str.parse() {
1014 Ok(n) => n,
1015 Err(_) => return ConditionResult::False,
1016 };
1017 if pos != expected {
1018 return ConditionResult::False;
1019 }
1020 expected += 1;
1021 }
1022 ConditionResult::True
1023 }
1024
1025 /// [912] Format: max. 6 Nachkommastellen
1026 // 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)
1027 fn evaluate_912(&self, ctx: &EvaluationContext) -> ConditionResult {
1028 ctx.format_check("PRI", 0, 1, |val| validate_max_decimal_places(val, 6))
1029 }
1030
1031 /// [926] Format: Möglicher Wert: 0
1032 // 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)
1033 fn evaluate_926(&self, ctx: &EvaluationContext) -> ConditionResult {
1034 ctx.format_check("QTY", 0, 1, |val| validate_numeric(val, "==", 0.0))
1035 }
1036
1037 /// [929] Format: Möglicher Wert: 1000
1038 // 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)
1039 fn evaluate_929(&self, ctx: &EvaluationContext) -> ConditionResult {
1040 ctx.format_check("QTY", 0, 1, |val| validate_numeric(val, "==", 1000.0))
1041 }
1042
1043 /// [931] Format: ZZZ = +00
1044 // 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)
1045 fn evaluate_931(&self, ctx: &EvaluationContext) -> ConditionResult {
1046 // ZZZ = +00: timezone offset in DTM value must be +00
1047 // DTM format 303: YYYYMMDDHHMMzzz, timezone encoded as last part of elements[0][1]
1048 let dtm_segments = ctx.find_segments("DTM");
1049 for seg in &dtm_segments {
1050 if let Some(value) = seg.elements.first().and_then(|e| e.get(1)) {
1051 if !value.is_empty() {
1052 // Timezone +00 appears at end of value string
1053 if value.ends_with("+00") {
1054 return ConditionResult::True;
1055 } else {
1056 return ConditionResult::False;
1057 }
1058 }
1059 }
1060 }
1061 ConditionResult::Unknown
1062 }
1063
1064 /// [932] Format: HHMM = 2200
1065 // 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)
1066 fn evaluate_932(&self, ctx: &EvaluationContext) -> ConditionResult {
1067 // HHMM = 2200: time value in DTM must be 2200
1068 // In DTM format 203 (YYYYMMDDHHMM) or 401 (HHMM), the time portion is HHMM
1069 let dtm_segments = ctx.find_segments("DTM");
1070 for seg in &dtm_segments {
1071 if let Some(value) = seg.elements.first().and_then(|e| e.get(1)) {
1072 let format_code = seg
1073 .elements
1074 .first()
1075 .and_then(|e| e.get(2))
1076 .map(|s| s.as_str())
1077 .unwrap_or("");
1078 let time_part = match format_code {
1079 "203" if value.len() >= 12 => &value[8..12],
1080 "401" if value.len() >= 4 => &value[0..4],
1081 _ if value.len() >= 12 => &value[8..12],
1082 _ => continue,
1083 };
1084 if time_part == "2200" {
1085 return ConditionResult::True;
1086 } else {
1087 return ConditionResult::False;
1088 }
1089 }
1090 }
1091 ConditionResult::Unknown
1092 }
1093
1094 /// [933] Format: HHMM = 2300
1095 // 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)
1096 fn evaluate_933(&self, ctx: &EvaluationContext) -> ConditionResult {
1097 // HHMM = 2300: time value in DTM must be 2300
1098 let dtm_segments = ctx.find_segments("DTM");
1099 for seg in &dtm_segments {
1100 if let Some(value) = seg.elements.first().and_then(|e| e.get(1)) {
1101 let format_code = seg
1102 .elements
1103 .first()
1104 .and_then(|e| e.get(2))
1105 .map(|s| s.as_str())
1106 .unwrap_or("");
1107 let time_part = match format_code {
1108 "203" if value.len() >= 12 => &value[8..12],
1109 "401" if value.len() >= 4 => &value[0..4],
1110 _ if value.len() >= 12 => &value[8..12],
1111 _ => continue,
1112 };
1113 if time_part == "2300" {
1114 return ConditionResult::True;
1115 } else {
1116 return ConditionResult::False;
1117 }
1118 }
1119 }
1120 ConditionResult::Unknown
1121 }
1122
1123 /// [937] Format: keine Nachkommastelle
1124 // 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)
1125 fn evaluate_937(&self, ctx: &EvaluationContext) -> ConditionResult {
1126 ctx.format_check("QTY", 0, 1, |val| validate_max_decimal_places(val, 0))
1127 }
1128
1129 /// [939] Format: Die Zeichenkette muss die Zeichen @ und . enthalten
1130 // 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)
1131 fn evaluate_939(&self, ctx: &EvaluationContext) -> ConditionResult {
1132 // Email format: string must contain both '@' and '.'
1133 // Typically applies to COM segment (communication channel EM = email)
1134 let com_segments = ctx.find_segments("COM");
1135 for seg in &com_segments {
1136 // COM: elements[0][0] = communication address, elements[0][1] = channel code
1137 let channel = seg
1138 .elements
1139 .first()
1140 .and_then(|e| e.get(1))
1141 .map(|s| s.as_str())
1142 .unwrap_or("");
1143 if channel == "EM" {
1144 if let Some(address) = seg.elements.first().and_then(|e| e.first()) {
1145 let contains_at = address.contains('@');
1146 let contains_dot = address.contains('.');
1147 return ConditionResult::from(contains_at && contains_dot);
1148 }
1149 }
1150 }
1151 // Fallback: check any COM value for @ and .
1152 for seg in &com_segments {
1153 if let Some(address) = seg.elements.first().and_then(|e| e.first()) {
1154 if !address.is_empty() {
1155 return ConditionResult::from(address.contains('@') && address.contains('.'));
1156 }
1157 }
1158 }
1159 ConditionResult::Unknown
1160 }
1161
1162 /// [940] Format: Die Zeichenkette muss mit dem Zeichen + beginnen und danach dürfen nur noch Ziffern folgen
1163 // 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)
1164 fn evaluate_940(&self, ctx: &EvaluationContext) -> ConditionResult {
1165 // Phone number format: must start with '+' followed only by digits
1166 // Typically applies to COM segment (TEL/FAX channel)
1167 let com_segments = ctx.find_segments("COM");
1168 for seg in &com_segments {
1169 let channel = seg
1170 .elements
1171 .first()
1172 .and_then(|e| e.get(1))
1173 .map(|s| s.as_str())
1174 .unwrap_or("");
1175 if matches!(channel, "TE" | "FX" | "AJ") {
1176 if let Some(number) = seg.elements.first().and_then(|e| e.first()) {
1177 if !number.is_empty() {
1178 let valid = number.starts_with('+')
1179 && number.len() > 1
1180 && number[1..].chars().all(|c| c.is_ascii_digit());
1181 return ConditionResult::from(valid);
1182 }
1183 }
1184 }
1185 }
1186 // Fallback: check any COM value for +digit format
1187 for seg in &com_segments {
1188 if let Some(number) = seg.elements.first().and_then(|e| e.first()) {
1189 if !number.is_empty() {
1190 let valid = number.starts_with('+')
1191 && number.len() > 1
1192 && number[1..].chars().all(|c| c.is_ascii_digit());
1193 return ConditionResult::from(valid);
1194 }
1195 }
1196 }
1197 ConditionResult::Unknown
1198 }
1199
1200 /// [941] Format: Artikelnummer
1201 // 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)
1202 fn evaluate_941(&self, ctx: &EvaluationContext) -> ConditionResult {
1203 ctx.format_check("PIA", 1, 0, |val| {
1204 validate_artikel_pattern(val, &[1, 2, 1, 3])
1205 })
1206 }
1207
1208 /// [942] Format: n1-n2-n1-n3
1209 fn evaluate_942(&self, ctx: &EvaluationContext) -> ConditionResult {
1210 ctx.format_check("PIA", 1, 0, |val| {
1211 validate_artikel_pattern(val, &[1, 2, 1, 3])
1212 })
1213 }
1214
1215 /// [946] Format: max. 11 Nachkommastellen
1216 // 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)
1217 fn evaluate_946(&self, ctx: &EvaluationContext) -> ConditionResult {
1218 ctx.format_check("PRI", 0, 1, |val| validate_max_decimal_places(val, 11))
1219 }
1220
1221 /// [948] Format: n1-n2-n1-n8-n2
1222 fn evaluate_948(&self, ctx: &EvaluationContext) -> ConditionResult {
1223 ctx.format_check("PIA", 1, 0, |val| {
1224 validate_artikel_pattern(val, &[1, 2, 1, 8, 2])
1225 })
1226 }
1227
1228 /// [949] Format: n1-n2-n1-n8-n2-n1
1229 fn evaluate_949(&self, ctx: &EvaluationContext) -> ConditionResult {
1230 ctx.format_check("PIA", 1, 0, |val| {
1231 validate_artikel_pattern(val, &[1, 2, 1, 8, 2, 1])
1232 })
1233 }
1234
1235 /// [957] Format: n1-n2-n1-n8
1236 fn evaluate_957(&self, ctx: &EvaluationContext) -> ConditionResult {
1237 ctx.format_check("PIA", 1, 0, |val| {
1238 validate_artikel_pattern(val, &[1, 2, 1, 8])
1239 })
1240 }
1241
1242 /// [959] Format: n13-n2
1243 // 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)
1244 fn evaluate_959(&self, ctx: &EvaluationContext) -> ConditionResult {
1245 ctx.format_check("PIA", 1, 0, |val| validate_artikel_pattern(val, &[13, 2]))
1246 }
1247
1248 /// [968] Format: Möglicher Wert: ≤ 0
1249 // 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)
1250 fn evaluate_968(&self, ctx: &EvaluationContext) -> ConditionResult {
1251 ctx.format_check("PRI", 0, 1, |val| validate_numeric(val, "<=", 0.0))
1252 }
1253}