1use edifact_rs::{
2 EdifactDeserialize, EdifactSerialize, EventEmitter, OwnedSegment, ProfileRulePack,
3 ValidationIssue, ValidationSeverity,
4};
5
6use crate::{
7 MessageType,
8 messages::{
9 core::MessageCore,
10 segments::{
11 Bgm, Cci, Dtm, Lin, Loc, Nad, Pia, Qty, Rff, Sts, collect_dtm, find_bgm, find_nad,
12 try_deserialize,
13 },
14 },
15};
16
17#[derive(Debug, Clone)]
24#[non_exhaustive]
25pub struct MsconsReference {
26 pub rff: Rff,
28 pub dtm: Vec<Dtm>,
30}
31
32#[derive(Debug, Clone)]
37#[non_exhaustive]
38pub struct MsconsDeliveryPoint {
39 pub nad: Nad,
41 pub time_series: Vec<MsconsTimeSeries>,
43}
44
45#[derive(Debug, Clone)]
49#[non_exhaustive]
50pub struct MsconsTimeSeries {
51 pub loc: Loc,
53 pub dtm: Vec<Dtm>,
55 pub references: Vec<Rff>,
57 pub time_series_type: Option<Cci>,
59 pub items: Vec<MsconsLineItem>,
61}
62
63#[derive(Debug, Clone)]
68#[non_exhaustive]
69pub struct MsconsLineItem {
70 pub lin: Lin,
72 pub pia: Option<Pia>,
74 pub quantities: Vec<MsconsQuantity>,
76}
77
78#[derive(Debug, Clone)]
82#[non_exhaustive]
83pub struct MsconsQuantity {
84 pub qty: Qty,
86 pub dtm: Vec<Dtm>,
88 pub status: Vec<Sts>,
90}
91
92#[derive(Debug, Clone)]
113pub struct MsconsMessage {
114 pub(crate) core: MessageCore,
115 bgm: Option<Bgm>,
117 dtm: Vec<Dtm>,
119 sender: Option<Nad>,
121 receiver: Option<Nad>,
123 references: Vec<MsconsReference>,
125 delivery_points: Vec<MsconsDeliveryPoint>,
127}
128
129impl MsconsMessage {
130 pub(crate) fn from_parts(
131 segments: Vec<OwnedSegment>,
132 message_ref: impl Into<Box<str>>,
133 assoc_code: impl Into<Box<str>>,
134 pruefidentifikator: Option<u32>,
135 ) -> Self {
136 let (bgm, dtm, sender, receiver, references, delivery_points) = {
137 let borrowed: Vec<edifact_rs::Segment<'_>> =
138 segments.iter().map(|s| s.as_borrowed()).collect();
139 (
140 find_bgm(&borrowed),
141 collect_dtm_header(&borrowed),
142 find_nad(&borrowed, "MS"),
143 find_nad(&borrowed, "MR"),
144 parse_references(&borrowed),
145 parse_delivery_points(&borrowed),
146 )
147 };
148 Self {
149 core: MessageCore::new(
150 segments,
151 message_ref,
152 assoc_code,
153 pruefidentifikator,
154 MessageType::Mscons,
155 ),
156 bgm,
157 dtm,
158 sender,
159 receiver,
160 references,
161 delivery_points,
162 }
163 }
164
165 #[must_use]
167 pub fn assoc_code(&self) -> &str {
168 &self.core.assoc_code
169 }
170
171 #[must_use]
173 pub fn segments(&self) -> &[OwnedSegment] {
174 &self.core.segments
175 }
176
177 #[must_use]
179 pub fn bgm(&self) -> Option<&Bgm> {
180 self.bgm.as_ref()
181 }
182
183 #[must_use]
185 pub fn dtm(&self) -> &[Dtm] {
186 &self.dtm
187 }
188
189 #[must_use]
191 pub fn sender(&self) -> Option<&Nad> {
192 self.sender.as_ref()
193 }
194
195 #[must_use]
197 pub fn receiver(&self) -> Option<&Nad> {
198 self.receiver.as_ref()
199 }
200
201 #[must_use]
203 pub fn references(&self) -> &[MsconsReference] {
204 &self.references
205 }
206
207 #[must_use]
209 pub fn delivery_points(&self) -> &[MsconsDeliveryPoint] {
210 &self.delivery_points
211 }
212}
213
214impl EdifactDeserialize for MsconsMessage {
217 fn edifact_deserialize(
218 segments: &[edifact_rs::Segment<'_>],
219 ) -> Result<Self, edifact_rs::EdifactError> {
220 let (message_ref, assoc_code) = MessageCore::extract_unh_fields(segments)?;
221 let pid = MessageCore::extract_bgm_pid(segments);
222 let owned: Vec<OwnedSegment> = segments.iter().cloned().map(OwnedSegment::from).collect();
223 Ok(Self::from_parts(owned, message_ref, assoc_code, pid))
224 }
225}
226
227impl EdifactSerialize for MsconsMessage {
230 fn edifact_serialize<E: EventEmitter>(
231 &self,
232 emitter: &mut E,
233 ) -> Result<(), edifact_rs::EdifactError> {
234 self.core.emit_segments(emitter)
235 }
236}
237impl_edi_energy_message!(MsconsMessage, sem = mscons_semantic_pack());
238
239fn collect_dtm_header(segments: &[edifact_rs::Segment<'_>]) -> Vec<Dtm> {
243 let end = segments
244 .iter()
245 .position(|s| s.tag == "UNS")
246 .unwrap_or(segments.len());
247 collect_dtm(&segments[..end])
248}
249
250fn parse_references(segments: &[edifact_rs::Segment<'_>]) -> Vec<MsconsReference> {
252 let end = segments
253 .iter()
254 .position(|s| s.tag == "UNS")
255 .unwrap_or(segments.len());
256 let header = &segments[..end];
257
258 let mut result = Vec::new();
259 let mut i = 0;
260 while i < header.len() {
261 if header[i].tag != "RFF" {
262 i += 1;
263 continue;
264 }
265 let Some(rff) = try_deserialize::<Rff>(&header[i]) else {
266 i += 1;
267 continue;
268 };
269 let mut dtm = Vec::new();
270 let mut j = i + 1;
271 while j < header.len() && header[j].tag == "DTM" {
272 if let Some(d) = try_deserialize::<Dtm>(&header[j]) {
273 dtm.push(d);
274 }
275 j += 1;
276 }
277 result.push(MsconsReference { rff, dtm });
278 i = j;
279 }
280 result
281}
282
283fn parse_delivery_points(segments: &[edifact_rs::Segment<'_>]) -> Vec<MsconsDeliveryPoint> {
286 let start = match segments.iter().position(|s| s.tag == "UNS") {
287 Some(pos) => pos + 1,
288 None => return Vec::new(),
289 };
290 let detail = &segments[start..];
291
292 let mut result = Vec::new();
293 let mut i = 0;
294
295 while i < detail.len() {
296 if detail[i].tag != "NAD" {
297 i += 1;
298 continue;
299 }
300 let Some(nad) = try_deserialize::<Nad>(&detail[i]) else {
301 i += 1;
302 continue;
303 };
304 i += 1;
305
306 let (time_series, next_i) = parse_sg6_groups(detail, i);
308 i = next_i;
309
310 result.push(MsconsDeliveryPoint { nad, time_series });
311 }
312 result
313}
314
315const SG6_TERMINATORS: &[&str] = &["NAD", "UNT"];
317const SG9_TERMINATORS: &[&str] = &["LIN", "LOC", "NAD", "UNT"];
319const SG10_TERMINATORS: &[&str] = &["QTY", "LIN", "LOC", "NAD", "UNT"];
321
322fn parse_sg6_groups(
325 detail: &[edifact_rs::Segment<'_>],
326 from: usize,
327) -> (Vec<MsconsTimeSeries>, usize) {
328 let mut series = Vec::new();
329 let mut i = from;
330
331 while i < detail.len() {
332 if SG6_TERMINATORS.iter().any(|t| &detail[i].tag == t) {
333 break;
334 }
335 if detail[i].tag != "LOC" {
336 i += 1;
337 continue;
338 }
339 let Some(loc) = try_deserialize::<Loc>(&detail[i]) else {
340 i += 1;
341 continue;
342 };
343 i += 1;
344
345 let mut dtm = Vec::new();
346 let mut references = Vec::new();
347 let mut time_series_type: Option<Cci> = None;
348
349 while i < detail.len() && !SG6_TERMINATORS.iter().any(|t| &detail[i].tag == t) {
351 match detail[i].tag {
352 "DTM" => {
353 if let Some(d) = try_deserialize::<Dtm>(&detail[i]) {
354 dtm.push(d);
355 }
356 i += 1;
357 }
358 "RFF" => {
359 if let Some(r) = try_deserialize::<Rff>(&detail[i]) {
360 references.push(r);
361 }
362 i += 1;
363 }
364 "CCI" => {
365 time_series_type = try_deserialize::<Cci>(&detail[i]);
366 i += 1;
367 }
368 "LIN" | "LOC" => break, _ => {
370 i += 1;
371 }
372 }
373 }
374
375 let (items, next_i) = parse_sg9_items(detail, i);
377 i = next_i;
378
379 series.push(MsconsTimeSeries {
380 loc,
381 dtm,
382 references,
383 time_series_type,
384 items,
385 });
386 }
387
388 (series, i)
389}
390
391fn parse_sg9_items(
394 detail: &[edifact_rs::Segment<'_>],
395 from: usize,
396) -> (Vec<MsconsLineItem>, usize) {
397 let mut items = Vec::new();
398 let mut i = from;
399
400 while i < detail.len() {
401 if SG9_TERMINATORS[1..].iter().any(|t| &detail[i].tag == t) {
402 break;
404 }
405 if detail[i].tag != "LIN" {
406 i += 1;
407 continue;
408 }
409 let Some(lin) = try_deserialize::<Lin>(&detail[i]) else {
410 i += 1;
411 continue;
412 };
413 i += 1;
414
415 let pia = if i < detail.len() && detail[i].tag == "PIA" {
417 let p = try_deserialize::<Pia>(&detail[i]);
418 i += 1;
419 p
420 } else {
421 None
422 };
423
424 let (quantities, next_i) = parse_sg10_quantities(detail, i);
426 i = next_i;
427
428 items.push(MsconsLineItem {
429 lin,
430 pia,
431 quantities,
432 });
433 }
434
435 (items, i)
436}
437
438fn parse_sg10_quantities(
441 detail: &[edifact_rs::Segment<'_>],
442 from: usize,
443) -> (Vec<MsconsQuantity>, usize) {
444 let mut quantities = Vec::new();
445 let mut i = from;
446
447 while i < detail.len() {
448 if SG10_TERMINATORS[1..].iter().any(|t| &detail[i].tag == t) {
449 break;
450 }
451 if detail[i].tag != "QTY" {
452 i += 1;
453 continue;
454 }
455 let Some(qty) = try_deserialize::<Qty>(&detail[i]) else {
456 i += 1;
457 continue;
458 };
459 i += 1;
460
461 let mut dtm = Vec::new();
462 let mut status = Vec::new();
463
464 while i < detail.len() && !SG10_TERMINATORS.iter().any(|t| &detail[i].tag == t) {
465 match detail[i].tag {
466 "DTM" => {
467 if let Some(d) = try_deserialize::<Dtm>(&detail[i]) {
468 dtm.push(d);
469 }
470 }
471 "STS" => {
472 if let Some(s) = try_deserialize::<Sts>(&detail[i]) {
473 status.push(s);
474 }
475 }
476 _ => {}
477 }
478 i += 1;
479 }
480
481 quantities.push(MsconsQuantity { qty, dtm, status });
482 }
483
484 (quantities, i)
485}
486
487fn mscons_semantic_pack() -> ProfileRulePack {
499 ProfileRulePack::new("MSCONS-SEM")
500 .for_message_type("MSCONS")
501 .with_stateless_rule_fn(rule_sem_location_format)
502 .with_stateless_rule_fn(rule_sem_period_order)
503 .with_stateless_rule_fn(rule_sem_unit_unknown)
504}
505
506fn rule_sem_location_format(
514 segments: &[edifact_rs::Segment<'_>],
515 issues: &mut Vec<ValidationIssue>,
516) {
517 for seg in segments.iter().filter(|s| s.tag == "LOC") {
518 let qualifier = seg.element_str(0).unwrap_or("");
521 if qualifier != "172" {
522 continue;
524 }
525 let id = seg
526 .get_element(1)
527 .and_then(|e| e.get_component(0))
528 .unwrap_or("");
529 if id.is_empty() {
530 continue;
531 }
532 if !super::common::is_valid_location_id(id) {
533 issues.push(
534 ValidationIssue::new(
535 ValidationSeverity::Error,
536 "LOC+172 element 3225 (C517 component 0): value is neither a \
537 Marktlokations-ID ([A-Z0-9]{11}) nor a Messlokations-ID (33 characters)"
538 .to_owned(),
539 )
540 .with_span(seg.span)
541 .with_rule_id("SEM-MSCONS-LOCATION-FORMAT")
542 .with_segment("LOC")
543 .with_suggestion(
544 "The Meldepunkt in LOC+172 must be either an 11-character \
545 Marktlokations-ID matching [A-Z0-9]{11} or a 33-character \
546 Messlokations-ID starting with an ISO 3166-1 country code",
547 ),
548 );
549 }
550 }
551}
552
553fn rule_sem_period_order(segments: &[edifact_rs::Segment<'_>], issues: &mut Vec<ValidationIssue>) {
560 let mut start: Option<(&str, edifact_rs::Span)> = None;
563 let mut end: Option<(&str, edifact_rs::Span)> = None;
564
565 for seg in segments.iter().filter(|s| s.tag == "DTM") {
566 let Some(c507) = seg.get_element(0) else {
570 continue;
571 };
572 let qualifier = c507.get_component(0).unwrap_or("");
573 let value = c507.get_component(1).unwrap_or("");
574 match qualifier {
575 "163" => start = Some((value, seg.span)),
576 "164" => end = Some((value, seg.span)),
577 _ => {}
578 }
579 }
580
581 if let (Some((start_val, start_span)), Some((end_val, _))) = (start, end) {
582 if !start_val.is_empty() && !end_val.is_empty() && start_val > end_val {
583 issues.push(
584 ValidationIssue::new(
585 ValidationSeverity::Error,
586 "DTM: period-start (qualifier 163) is after period-end (qualifier 164)"
587 .to_owned(),
588 )
589 .with_span(start_span)
590 .with_rule_id("SEM-MSCONS-PERIOD-ORDER")
591 .with_segment("DTM")
592 .with_suggestion(
593 "Ensure DTM+163 (Beginn Lieferzeitraum) is not later than \
594 DTM+164 (Ende Lieferzeitraum) — date values must be in \
595 ascending chronological order",
596 ),
597 );
598 }
599 }
600}
601
602const APPROVED_UNITS: &[&str] = &[
612 "KWH", "MWH", "GWH", "KW", "KWT", "MW", "GW", "KVA", "MVA", "KVAR", "MVAR", "M3", "M3H", "HM3",
613 "GJ", "MJ", "J", "D54", "MTS", "Z03", "Z12", "Z14",
614];
615
616fn rule_sem_unit_unknown(segments: &[edifact_rs::Segment<'_>], issues: &mut Vec<ValidationIssue>) {
619 for seg in segments.iter().filter(|s| s.tag == "QTY") {
620 let unit = seg
625 .get_element(0)
626 .and_then(|e| e.get_component(2))
627 .unwrap_or("");
628 if unit.is_empty() {
629 continue; }
631 if !APPROVED_UNITS.contains(&unit) {
632 issues.push(
633 ValidationIssue::new(
634 ValidationSeverity::Error,
635 "QTY C186 component 2 (DE 6411): unit-of-measure code is not \
636 in the EDI@Energy approved set for MSCONS"
637 .to_owned(),
638 )
639 .with_span(seg.span)
640 .with_rule_id("SEM-MSCONS-UNIT-UNKNOWN")
641 .with_segment("QTY")
642 .with_suggestion(
643 "Use one of the EDI@Energy MSCONS approved units (Code List 6411): \
644 KWH MWH GWH KW KWT MW GW KVA MVA KVAR MVAR M3 M3H HM3 GJ MJ J \
645 D54 MTS Z03 Z12 Z14",
646 ),
647 );
648 }
649 }
650}