use edifact_rs::{
EdifactDeserialize, EdifactSerialize, EventEmitter, OwnedSegment, ProfileRulePack,
ValidationIssue, ValidationSeverity,
};
use crate::{
MessageType,
messages::{
core::MessageCore,
segments::{
Bgm, Cci, Dtm, Lin, Loc, Nad, Pia, Qty, Rff, Sts, collect_dtm, find_bgm, find_nad,
try_deserialize,
},
},
};
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct MsconsReference {
pub rff: Rff,
pub dtm: Vec<Dtm>,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct MsconsDeliveryPoint {
pub nad: Nad,
pub time_series: Vec<MsconsTimeSeries>,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct MsconsTimeSeries {
pub loc: Loc,
pub dtm: Vec<Dtm>,
pub references: Vec<Rff>,
pub time_series_type: Option<Cci>,
pub items: Vec<MsconsLineItem>,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct MsconsLineItem {
pub lin: Lin,
pub pia: Option<Pia>,
pub quantities: Vec<MsconsQuantity>,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct MsconsQuantity {
pub qty: Qty,
pub dtm: Vec<Dtm>,
pub status: Vec<Sts>,
}
#[derive(Debug, Clone)]
pub struct MsconsMessage {
pub(crate) core: MessageCore,
bgm: Option<Bgm>,
dtm: Vec<Dtm>,
sender: Option<Nad>,
receiver: Option<Nad>,
references: Vec<MsconsReference>,
delivery_points: Vec<MsconsDeliveryPoint>,
}
impl MsconsMessage {
pub(crate) fn from_parts(
segments: Vec<OwnedSegment>,
message_ref: impl Into<Box<str>>,
assoc_code: impl Into<Box<str>>,
pruefidentifikator: Option<u32>,
) -> Self {
let (bgm, dtm, sender, receiver, references, delivery_points) = {
let borrowed: Vec<edifact_rs::Segment<'_>> =
segments.iter().map(|s| s.as_borrowed()).collect();
(
find_bgm(&borrowed),
collect_dtm_header(&borrowed),
find_nad(&borrowed, "MS"),
find_nad(&borrowed, "MR"),
parse_references(&borrowed),
parse_delivery_points(&borrowed),
)
};
Self {
core: MessageCore::new(
segments,
message_ref,
assoc_code,
pruefidentifikator,
MessageType::Mscons,
),
bgm,
dtm,
sender,
receiver,
references,
delivery_points,
}
}
#[must_use]
pub fn assoc_code(&self) -> &str {
&self.core.assoc_code
}
#[must_use]
pub fn segments(&self) -> &[OwnedSegment] {
&self.core.segments
}
#[must_use]
pub fn bgm(&self) -> Option<&Bgm> {
self.bgm.as_ref()
}
#[must_use]
pub fn dtm(&self) -> &[Dtm] {
&self.dtm
}
#[must_use]
pub fn sender(&self) -> Option<&Nad> {
self.sender.as_ref()
}
#[must_use]
pub fn receiver(&self) -> Option<&Nad> {
self.receiver.as_ref()
}
#[must_use]
pub fn references(&self) -> &[MsconsReference] {
&self.references
}
#[must_use]
pub fn delivery_points(&self) -> &[MsconsDeliveryPoint] {
&self.delivery_points
}
}
impl EdifactDeserialize for MsconsMessage {
fn edifact_deserialize(
segments: &[edifact_rs::Segment<'_>],
) -> Result<Self, edifact_rs::EdifactError> {
let (message_ref, assoc_code) = MessageCore::extract_unh_fields(segments)?;
let pid = MessageCore::extract_bgm_pid(segments);
let owned: Vec<OwnedSegment> = segments.iter().cloned().map(OwnedSegment::from).collect();
Ok(Self::from_parts(owned, message_ref, assoc_code, pid))
}
}
impl EdifactSerialize for MsconsMessage {
fn edifact_serialize<E: EventEmitter>(
&self,
emitter: &mut E,
) -> Result<(), edifact_rs::EdifactError> {
self.core.emit_segments(emitter)
}
}
impl_edi_energy_message!(MsconsMessage, sem = mscons_semantic_pack());
fn collect_dtm_header(segments: &[edifact_rs::Segment<'_>]) -> Vec<Dtm> {
let end = segments
.iter()
.position(|s| s.tag == "UNS")
.unwrap_or(segments.len());
collect_dtm(&segments[..end])
}
fn parse_references(segments: &[edifact_rs::Segment<'_>]) -> Vec<MsconsReference> {
let end = segments
.iter()
.position(|s| s.tag == "UNS")
.unwrap_or(segments.len());
let header = &segments[..end];
let mut result = Vec::new();
let mut i = 0;
while i < header.len() {
if header[i].tag != "RFF" {
i += 1;
continue;
}
let Some(rff) = try_deserialize::<Rff>(&header[i]) else {
i += 1;
continue;
};
let mut dtm = Vec::new();
let mut j = i + 1;
while j < header.len() && header[j].tag == "DTM" {
if let Some(d) = try_deserialize::<Dtm>(&header[j]) {
dtm.push(d);
}
j += 1;
}
result.push(MsconsReference { rff, dtm });
i = j;
}
result
}
fn parse_delivery_points(segments: &[edifact_rs::Segment<'_>]) -> Vec<MsconsDeliveryPoint> {
let start = match segments.iter().position(|s| s.tag == "UNS") {
Some(pos) => pos + 1,
None => return Vec::new(),
};
let detail = &segments[start..];
let mut result = Vec::new();
let mut i = 0;
while i < detail.len() {
if detail[i].tag != "NAD" {
i += 1;
continue;
}
let Some(nad) = try_deserialize::<Nad>(&detail[i]) else {
i += 1;
continue;
};
i += 1;
let (time_series, next_i) = parse_sg6_groups(detail, i);
i = next_i;
result.push(MsconsDeliveryPoint { nad, time_series });
}
result
}
const SG6_TERMINATORS: &[&str] = &["NAD", "UNT"];
const SG9_TERMINATORS: &[&str] = &["LIN", "LOC", "NAD", "UNT"];
const SG10_TERMINATORS: &[&str] = &["QTY", "LIN", "LOC", "NAD", "UNT"];
fn parse_sg6_groups(
detail: &[edifact_rs::Segment<'_>],
from: usize,
) -> (Vec<MsconsTimeSeries>, usize) {
let mut series = Vec::new();
let mut i = from;
while i < detail.len() {
if SG6_TERMINATORS.iter().any(|t| &detail[i].tag == t) {
break;
}
if detail[i].tag != "LOC" {
i += 1;
continue;
}
let Some(loc) = try_deserialize::<Loc>(&detail[i]) else {
i += 1;
continue;
};
i += 1;
let mut dtm = Vec::new();
let mut references = Vec::new();
let mut time_series_type: Option<Cci> = None;
while i < detail.len() && !SG6_TERMINATORS.iter().any(|t| &detail[i].tag == t) {
match detail[i].tag {
"DTM" => {
if let Some(d) = try_deserialize::<Dtm>(&detail[i]) {
dtm.push(d);
}
i += 1;
}
"RFF" => {
if let Some(r) = try_deserialize::<Rff>(&detail[i]) {
references.push(r);
}
i += 1;
}
"CCI" => {
time_series_type = try_deserialize::<Cci>(&detail[i]);
i += 1;
}
"LIN" | "LOC" => break, _ => {
i += 1;
}
}
}
let (items, next_i) = parse_sg9_items(detail, i);
i = next_i;
series.push(MsconsTimeSeries {
loc,
dtm,
references,
time_series_type,
items,
});
}
(series, i)
}
fn parse_sg9_items(
detail: &[edifact_rs::Segment<'_>],
from: usize,
) -> (Vec<MsconsLineItem>, usize) {
let mut items = Vec::new();
let mut i = from;
while i < detail.len() {
if SG9_TERMINATORS[1..].iter().any(|t| &detail[i].tag == t) {
break;
}
if detail[i].tag != "LIN" {
i += 1;
continue;
}
let Some(lin) = try_deserialize::<Lin>(&detail[i]) else {
i += 1;
continue;
};
i += 1;
let pia = if i < detail.len() && detail[i].tag == "PIA" {
let p = try_deserialize::<Pia>(&detail[i]);
i += 1;
p
} else {
None
};
let (quantities, next_i) = parse_sg10_quantities(detail, i);
i = next_i;
items.push(MsconsLineItem {
lin,
pia,
quantities,
});
}
(items, i)
}
fn parse_sg10_quantities(
detail: &[edifact_rs::Segment<'_>],
from: usize,
) -> (Vec<MsconsQuantity>, usize) {
let mut quantities = Vec::new();
let mut i = from;
while i < detail.len() {
if SG10_TERMINATORS[1..].iter().any(|t| &detail[i].tag == t) {
break;
}
if detail[i].tag != "QTY" {
i += 1;
continue;
}
let Some(qty) = try_deserialize::<Qty>(&detail[i]) else {
i += 1;
continue;
};
i += 1;
let mut dtm = Vec::new();
let mut status = Vec::new();
while i < detail.len() && !SG10_TERMINATORS.iter().any(|t| &detail[i].tag == t) {
match detail[i].tag {
"DTM" => {
if let Some(d) = try_deserialize::<Dtm>(&detail[i]) {
dtm.push(d);
}
}
"STS" => {
if let Some(s) = try_deserialize::<Sts>(&detail[i]) {
status.push(s);
}
}
_ => {}
}
i += 1;
}
quantities.push(MsconsQuantity { qty, dtm, status });
}
(quantities, i)
}
fn mscons_semantic_pack() -> ProfileRulePack {
ProfileRulePack::new("MSCONS-SEM")
.for_message_type("MSCONS")
.with_stateless_rule_fn(rule_sem_location_format)
.with_stateless_rule_fn(rule_sem_period_order)
.with_stateless_rule_fn(rule_sem_unit_unknown)
}
fn rule_sem_location_format(
segments: &[edifact_rs::Segment<'_>],
issues: &mut Vec<ValidationIssue>,
) {
for seg in segments.iter().filter(|s| s.tag == "LOC") {
let qualifier = seg.element_str(0).unwrap_or("");
if qualifier != "172" {
continue;
}
let id = seg
.get_element(1)
.and_then(|e| e.get_component(0))
.unwrap_or("");
if id.is_empty() {
continue;
}
if !super::common::is_valid_location_id(id) {
issues.push(
ValidationIssue::new(
ValidationSeverity::Error,
"LOC+172 element 3225 (C517 component 0): value is neither a \
Marktlokations-ID ([A-Z0-9]{11}) nor a Messlokations-ID (33 characters)"
.to_owned(),
)
.with_span(seg.span)
.with_rule_id("SEM-MSCONS-LOCATION-FORMAT")
.with_segment("LOC")
.with_suggestion(
"The Meldepunkt in LOC+172 must be either an 11-character \
Marktlokations-ID matching [A-Z0-9]{11} or a 33-character \
Messlokations-ID starting with an ISO 3166-1 country code",
),
);
}
}
}
fn rule_sem_period_order(segments: &[edifact_rs::Segment<'_>], issues: &mut Vec<ValidationIssue>) {
let mut start: Option<(&str, edifact_rs::Span)> = None;
let mut end: Option<(&str, edifact_rs::Span)> = None;
for seg in segments.iter().filter(|s| s.tag == "DTM") {
let Some(c507) = seg.get_element(0) else {
continue;
};
let qualifier = c507.get_component(0).unwrap_or("");
let value = c507.get_component(1).unwrap_or("");
match qualifier {
"163" => start = Some((value, seg.span)),
"164" => end = Some((value, seg.span)),
_ => {}
}
}
if let (Some((start_val, start_span)), Some((end_val, _))) = (start, end) {
if !start_val.is_empty() && !end_val.is_empty() && start_val > end_val {
issues.push(
ValidationIssue::new(
ValidationSeverity::Error,
"DTM: period-start (qualifier 163) is after period-end (qualifier 164)"
.to_owned(),
)
.with_span(start_span)
.with_rule_id("SEM-MSCONS-PERIOD-ORDER")
.with_segment("DTM")
.with_suggestion(
"Ensure DTM+163 (Beginn Lieferzeitraum) is not later than \
DTM+164 (Ende Lieferzeitraum) — date values must be in \
ascending chronological order",
),
);
}
}
}
const APPROVED_UNITS: &[&str] = &[
"KWH", "MWH", "GWH", "KW", "KWT", "MW", "GW", "KVA", "MVA", "KVAR", "MVAR", "M3", "M3H", "HM3",
"GJ", "MJ", "J", "D54", "MTS", "Z03", "Z12", "Z14",
];
fn rule_sem_unit_unknown(segments: &[edifact_rs::Segment<'_>], issues: &mut Vec<ValidationIssue>) {
for seg in segments.iter().filter(|s| s.tag == "QTY") {
let unit = seg
.get_element(0)
.and_then(|e| e.get_component(2))
.unwrap_or("");
if unit.is_empty() {
continue; }
if !APPROVED_UNITS.contains(&unit) {
issues.push(
ValidationIssue::new(
ValidationSeverity::Error,
"QTY C186 component 2 (DE 6411): unit-of-measure code is not \
in the EDI@Energy approved set for MSCONS"
.to_owned(),
)
.with_span(seg.span)
.with_rule_id("SEM-MSCONS-UNIT-UNKNOWN")
.with_segment("QTY")
.with_suggestion(
"Use one of the EDI@Energy MSCONS approved units (Code List 6411): \
KWH MWH GWH KW KWT MW GW KVA MVA KVAR MVAR M3 M3H HM3 GJ MJ J \
D54 MTS Z03 Z12 Z14",
),
);
}
}
}