use edifact_rs::{
EdifactDeserialize, EdifactSerialize, EventEmitter, OwnedSegment, ProfileRulePack,
ValidationIssue, ValidationSeverity,
};
use crate::{
MessageType,
messages::{
core::MessageCore,
segments::{
Bgm, Cav, Cci, Dtm, Ftx, Ide, Loc, Nad, Pia, Rff, Seq, Sts, collect_dtm, find_bgm,
find_nad, try_deserialize,
},
},
};
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct UtilmdReference {
pub rff: Rff,
pub dtm: Vec<Dtm>,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct UtilmdTransaction {
pub ide: Ide,
pub dtm: Vec<Dtm>,
pub locations: Vec<Loc>,
pub references: Vec<Rff>,
pub sts: Vec<Sts>,
pub ftx: Vec<Ftx>,
transaktionsgrund: Option<crate::utilmd_codes::Transaktionsgrund>,
antwort: Option<crate::utilmd_codes::AntwortStatus>,
pub parties: Vec<UtilmdParty>,
pub sequences: Vec<UtilmdSequence>,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct UtilmdParty {
pub nad: Nad,
pub name_parts: Vec<String>,
}
impl UtilmdParty {
#[must_use]
pub fn name(&self) -> Option<String> {
(!self.name_parts.is_empty()).then(|| self.name_parts.join(" "))
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct UtilmdSequence {
pub seq: Seq,
pub products: Vec<UtilmdProduct>,
pub references: Vec<Rff>,
pub quantities: Vec<crate::messages::segments::Qty>,
pub characteristics: Vec<UtilmdCharacteristic>,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct UtilmdProduct {
pub pia: Pia,
first_characteristic: usize,
}
impl UtilmdProduct {
#[must_use]
pub fn produkt_code(&self) -> Option<&str> {
self.pia.item_number.as_deref()
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct UtilmdCharacteristic {
pub cci: Cci,
pub values: Vec<Cav>,
}
impl UtilmdSequence {
#[must_use]
pub fn value(&self, code: &str) -> Option<&str> {
self.characteristics
.iter()
.flat_map(|c| c.values.iter())
.find(|v| v.value_code.as_deref() == Some(code))
.and_then(|v| v.value.as_deref())
}
#[must_use]
pub fn product_value(&self, produkt_code: &str, cav_code: &str) -> Option<&str> {
let idx = self
.products
.iter()
.position(|p| p.produkt_code() == Some(produkt_code))?;
let from = self.products[idx].first_characteristic;
let to = self
.products
.get(idx + 1)
.map_or(self.characteristics.len(), |p| p.first_characteristic);
self.characteristics
.get(from..to)?
.iter()
.flat_map(|c| c.values.iter())
.find(|v| v.value_code.as_deref() == Some(cav_code))
.and_then(|v| v.value.as_deref())
}
}
impl UtilmdTransaction {
#[must_use]
pub fn vorgangsnummer(&self) -> Option<&str> {
self.ide.object_id.as_deref()
}
#[must_use]
pub fn location(&self, lokationstyp: crate::Lokationstyp) -> Option<&str> {
let want = lokationstyp.qualifier_code();
self.locations
.iter()
.find(|l| l.qualifier == want)
.and_then(|l| l.location_id.as_deref())
}
#[must_use]
pub fn marktlokation(&self) -> Option<&str> {
self.location(crate::Lokationstyp::Marktlokation)
}
#[must_use]
pub fn messlokation(&self) -> Option<&str> {
self.location(crate::Lokationstyp::Messlokation)
}
#[must_use]
pub fn tranche(&self) -> Option<&str> {
self.location(crate::Lokationstyp::Tranche)
}
#[must_use]
pub fn lokation(&self) -> Option<&str> {
self.meldepunkt()
.or_else(|| self.marktlokation())
.or_else(|| self.tranche())
.or_else(|| self.messlokation())
}
#[must_use]
pub fn meldepunkt(&self) -> Option<&str> {
self.location(crate::Lokationstyp::Meldepunkt)
}
#[must_use]
pub fn bilanzkreis(&self) -> Option<&str> {
use crate::utilmd_codes::produkt;
let strom = self
.sequences
.iter()
.filter(|g| g.seq.action.as_deref() == Some(produkt::SEQ_PRODUKTPAKET))
.find_map(|g| g.product_value(produkt::BILANZKREIS, produkt::CAV_WERT));
if strom.is_some() {
return strom;
}
self.sequences
.iter()
.flat_map(|g| g.characteristics.iter())
.find(|c| c.cci.category.as_deref() == Some(produkt::CCI_BILANZKREIS_GAS))
.and_then(|c| c.cci.characteristic_id.as_deref())
}
#[must_use]
pub fn tranchengroesse(&self) -> Option<crate::utilmd_codes::Tranchengroesse> {
use crate::utilmd_codes::produkt;
self.sequences
.iter()
.filter(|g| g.seq.action.as_deref() == Some(produkt::SEQ_PRODUKTPAKET))
.find_map(|g| {
let wert = g.product_value(produkt::TRANCHENGROESSE, produkt::CAV_WERT)?;
Some(crate::utilmd_codes::Tranchengroesse {
eigenschaft: g
.product_value(produkt::TRANCHENGROESSE, produkt::CAV_EIGENSCHAFT)
.map(ToOwned::to_owned),
wert: wert.to_owned(),
})
})
}
#[must_use]
pub fn party(&self, qualifier: &str) -> Option<&UtilmdParty> {
self.parties.iter().find(|p| p.nad.qualifier == qualifier)
}
#[must_use]
pub fn kunde(&self) -> Option<&UtilmdParty> {
self.party(crate::utilmd_codes::nad::KUNDE_DES_LF)
}
#[must_use]
pub fn date(&self, qualifier: &str) -> Option<&str> {
self.dtm
.iter()
.find(|d| d.qualifier == qualifier)
.and_then(|d| d.value.as_deref())
}
#[must_use]
pub fn transaktionsgrund(&self) -> Option<crate::utilmd_codes::Transaktionsgrund> {
self.transaktionsgrund.clone()
}
#[must_use]
pub fn antwort(&self) -> Option<&crate::utilmd_codes::AntwortStatus> {
self.antwort.as_ref()
}
}
#[derive(Debug, Clone)]
pub struct UtilmdMessage {
pub(crate) core: MessageCore,
bgm: Option<Bgm>,
dtm: Vec<Dtm>,
sender: Option<Nad>,
receiver: Option<Nad>,
references: Vec<UtilmdReference>,
transactions: Vec<UtilmdTransaction>,
}
impl UtilmdMessage {
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, transactions) = (
find_bgm(&segments),
collect_dtm(&segments),
find_nad(&segments, "MS"),
find_nad(&segments, "MR"),
parse_references(&segments),
parse_transactions(&segments),
);
Self {
core: MessageCore::new(
segments,
message_ref,
assoc_code,
pruefidentifikator,
MessageType::Utilmd,
),
bgm,
dtm,
sender,
receiver,
references,
transactions,
}
}
#[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) -> &[UtilmdReference] {
&self.references
}
#[must_use]
pub fn transactions(&self) -> &[UtilmdTransaction] {
&self.transactions
}
}
impl EdifactDeserialize for UtilmdMessage {
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(edifact_rs::Segment::into_owned)
.collect();
Ok(Self::from_parts(owned, message_ref, assoc_code, pid))
}
}
impl EdifactSerialize for UtilmdMessage {
fn edifact_serialize<E: EventEmitter>(
&self,
emitter: &mut E,
) -> Result<(), edifact_rs::EdifactError> {
self.core.emit_segments(emitter)
}
}
impl_edi_energy_message!(UtilmdMessage, sem = utilmd_semantic_pack());
fn parse_references(segments: &[edifact_rs::Segment<'_>]) -> Vec<UtilmdReference> {
let end = segments
.iter()
.position(|s| s.tag == "IDE")
.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(UtilmdReference { rff, dtm });
i = j;
}
result
}
struct Sg4Acc {
dtm: Vec<Dtm>,
locations: Vec<Loc>,
references: Vec<Rff>,
sts: Vec<Sts>,
ftx: Vec<Ftx>,
transaktionsgrund: Option<crate::utilmd_codes::Transaktionsgrund>,
antwort: Option<crate::utilmd_codes::AntwortStatus>,
parties: Vec<UtilmdParty>,
sequences: Vec<UtilmdSequence>,
}
fn collect_sg4_segment(seg: &edifact_rs::Segment<'_>, ctx: &mut Sg4Acc) {
match &*seg.tag {
"DTM" => {
if let Some(d) = try_deserialize::<Dtm>(seg) {
ctx.dtm.push(d);
}
}
"LOC" => {
if let Some(l) = try_deserialize::<Loc>(seg) {
ctx.locations.push(l);
}
}
"RFF" => {
if let Some(r) = try_deserialize::<Rff>(seg) {
ctx.references.push(r);
}
}
"STS" => {
match sts_category(seg) {
Some(crate::utilmd_codes::STS_TRANSAKTIONSGRUND) => {
ctx.transaktionsgrund = parse_transaktionsgrund(seg);
}
Some(crate::utilmd_codes::STS_STATUS_ANTWORT) => {
ctx.antwort = parse_antwort(seg);
}
_ => {}
}
if let Some(s) = try_deserialize::<Sts>(seg) {
ctx.sts.push(s);
}
}
"FTX" => {
if let Some(f) = try_deserialize::<Ftx>(seg) {
ctx.ftx.push(f);
}
}
"SEQ" => {
if let Some(seq) = try_deserialize::<Seq>(seg) {
ctx.sequences.push(UtilmdSequence {
seq,
products: Vec::new(),
references: Vec::new(),
quantities: Vec::new(),
characteristics: Vec::new(),
});
}
}
"PIA" => {
if let (Some(group), Some(p)) = (ctx.sequences.last_mut(), try_deserialize::<Pia>(seg))
{
let first_characteristic = group.characteristics.len();
group.products.push(UtilmdProduct {
pia: p,
first_characteristic,
});
}
}
"QTY" => {
if let (Some(group), Some(q)) = (
ctx.sequences.last_mut(),
try_deserialize::<crate::messages::segments::Qty>(seg),
) {
group.quantities.push(q);
}
}
"CCI" => {
if let (Some(group), Some(cci)) =
(ctx.sequences.last_mut(), try_deserialize::<Cci>(seg))
{
group.characteristics.push(UtilmdCharacteristic {
cci,
values: Vec::new(),
});
}
}
"CAV" => {
if let (Some(merkmal), Some(cav)) = (
ctx.sequences
.last_mut()
.and_then(|g| g.characteristics.last_mut()),
try_deserialize::<Cav>(seg),
) {
merkmal.values.push(cav);
}
}
"NAD" => {
if let Some(nad) = try_deserialize::<Nad>(seg) {
ctx.parties.push(UtilmdParty {
nad,
name_parts: c080_name_parts(seg),
});
}
}
_ => {}
}
}
fn parse_transactions(segments: &[edifact_rs::Segment<'_>]) -> Vec<UtilmdTransaction> {
let mut result = Vec::new();
let mut i = 0;
while i < segments.len() {
if segments[i].tag != "IDE" {
i += 1;
continue;
}
let Some(ide) = try_deserialize::<Ide>(&segments[i]) else {
i += 1;
continue;
};
let mut ctx = Sg4Acc {
dtm: Vec::new(),
locations: Vec::new(),
references: Vec::new(),
sts: Vec::new(),
ftx: Vec::new(),
transaktionsgrund: None,
antwort: None,
parties: Vec::new(),
sequences: Vec::new(),
};
let mut j = i + 1;
while j < segments.len() && segments[j].tag != "IDE" && segments[j].tag != "UNT" {
collect_sg4_segment(&segments[j], &mut ctx);
j += 1;
}
result.push(UtilmdTransaction {
ide,
dtm: ctx.dtm,
locations: ctx.locations,
references: ctx.references,
sts: ctx.sts,
ftx: ctx.ftx,
transaktionsgrund: ctx.transaktionsgrund,
antwort: ctx.antwort,
parties: ctx.parties,
sequences: ctx.sequences,
});
i = j;
}
result
}
fn c080_name_parts(seg: &edifact_rs::Segment<'_>) -> Vec<String> {
seg.get_element(3)
.map(|e| {
(0..5)
.filter_map(|c| e.get_component(c))
.filter(|c| !c.is_empty())
.map(ToOwned::to_owned)
.collect()
})
.unwrap_or_default()
}
fn sts_category<'a>(seg: &'a edifact_rs::Segment<'_>) -> Option<&'a str> {
seg.get_element(0)
.and_then(|e| e.get_component(0))
.filter(|c| !c.is_empty())
}
fn parse_transaktionsgrund(
seg: &edifact_rs::Segment<'_>,
) -> Option<crate::utilmd_codes::Transaktionsgrund> {
let at = |idx: usize| {
seg.get_element(idx)
.and_then(|e| e.get_component(0))
.filter(|c| !c.is_empty())
.map(ToOwned::to_owned)
};
Some(crate::utilmd_codes::Transaktionsgrund {
grund: at(2)?,
ergaenzung: at(3),
befristet: at(4),
})
}
fn parse_antwort(seg: &edifact_rs::Segment<'_>) -> Option<crate::utilmd_codes::AntwortStatus> {
let c556 = seg.get_element(2)?;
let code = c556.get_component(0).filter(|c| !c.is_empty())?;
Some(crate::utilmd_codes::AntwortStatus {
code: code.to_owned(),
codeliste: c556
.get_component(1)
.filter(|c| !c.is_empty())
.map(ToOwned::to_owned),
})
}
fn utilmd_semantic_pack() -> ProfileRulePack {
ProfileRulePack::new("UTILMD-SEM")
.for_message_type("UTILMD")
.with_rule_fn(rule_sem_lokations_id_format)
}
fn rule_sem_lokations_id_format(
segments: &[edifact_rs::Segment<'_>],
issues: &mut Vec<ValidationIssue>,
) {
use crate::utilmd_codes::loc;
for seg in segments.iter().filter(|s| s.tag == "LOC") {
let qualifier = seg
.get_element(0)
.and_then(|e| e.get_component(0))
.unwrap_or("");
let expects_location_id = matches!(
qualifier,
q if q == loc::MARKTLOKATION
|| q == loc::RUHENDE_MARKTLOKATION
|| q == loc::MESSLOKATION
);
if !expects_location_id {
continue;
}
let id = seg
.get_element(1)
.and_then(|e| e.get_component(0))
.unwrap_or("");
if id.is_empty() || super::common::is_valid_location_id(id) {
continue;
}
issues.push(
ValidationIssue::new(
ValidationSeverity::Error,
format!(
"LOC+{qualifier} element 3225: value is neither a \
Marktlokations-ID ([A-Z0-9]{{11}}) nor a Messlokations-ID (33 characters)"
),
)
.with_span(seg.span)
.with_rule_id("SEM-UTILMD-LOKATIONS-ID")
.with_segment("LOC")
.with_suggestion(
"LOC+Z16 / LOC+Z22 carry an 11-character Marktlokations-ID matching \
[A-Z0-9]{11}; LOC+Z17 carries a 33-character Messlokations-ID starting \
with an ISO 3166-1 country code",
),
);
}
}