use std::fmt;
use edifact_rs::OwnedSegment;
use crate::{
DvgwMessageType,
message::{MessageCore, find_all_segments, find_segment, impl_dvgw_message},
};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum DelresStatus {
Accepted,
Modified,
Rejected,
Other(String),
}
impl DelresStatus {
fn from_sts(qualifier: &str) -> Self {
match qualifier {
"Z01" | "AC" | "1" => Self::Accepted,
"Z02" | "PA" => Self::Modified,
"Z03" | "RE" | "7" => Self::Rejected,
other => Self::Other(other.to_owned()),
}
}
#[must_use]
pub fn as_str(&self) -> &str {
match self {
Self::Accepted => "Accepted",
Self::Modified => "Modified",
Self::Rejected => "Rejected",
Self::Other(code) => code.as_str(),
}
}
}
impl fmt::Display for DelresStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct DeliveryResponseLine {
pub location_code: String,
pub location_qualifier: String,
pub quantity: String,
pub quantity_qualifier: String,
pub unit: Option<String>,
pub status: Option<DelresStatus>,
pub delivery_start: Option<String>,
pub delivery_end: Option<String>,
}
impl DeliveryResponseLine {
#[must_use]
pub fn quantity_f64(&self) -> Option<f64> {
if self.quantity.is_empty() {
None
} else {
self.quantity.parse().ok()
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct DeliveryResponseMessage {
pub(crate) core: MessageCore,
pub response_ref: Option<String>,
pub order_ref: Option<String>,
pub status: Option<DelresStatus>,
pub rejection_reason: Option<String>,
pub gas_day: Option<String>,
pub lines: Vec<DeliveryResponseLine>,
}
impl_dvgw_message!(DeliveryResponseMessage);
impl DeliveryResponseMessage {
pub(crate) fn from_segments(segments: Vec<OwnedSegment>) -> Self {
let core = MessageCore::from_segments(segments, DvgwMessageType::Delres);
let response_ref = find_segment(&core.segments, "BGM")
.and_then(|s| s.component_str(1, 0))
.map(str::to_owned);
let order_ref = find_all_segments(&core.segments, "RFF")
.find(|s| s.component_str(0, 0) == Some("Z13"))
.and_then(|s| s.component_str(0, 1))
.map(str::to_owned);
let status = find_segment(&core.segments, "STS")
.and_then(|s| s.component_str(0, 0))
.map(DelresStatus::from_sts);
let rejection_reason = if matches!(status, Some(DelresStatus::Rejected)) {
find_segment(&core.segments, "FTX")
.and_then(|s| s.component_str(3, 0))
.map(str::to_owned)
} else {
None
};
let gas_day = extract_dtm(&core.segments, "137");
let lines = extract_lines(&core.segments);
Self {
core,
response_ref,
order_ref,
status,
rejection_reason,
gas_day,
lines,
}
}
}
fn extract_dtm(segs: &[OwnedSegment], qualifier: &str) -> Option<String> {
find_all_segments(segs, "DTM")
.find(|s| s.component_str(0, 0) == Some(qualifier))
.and_then(|s| s.component_str(0, 1))
.map(str::to_owned)
}
fn extract_lines(segs: &[OwnedSegment]) -> Vec<DeliveryResponseLine> {
let mut result = Vec::new();
let mut i = 0;
while i < segs.len() {
if segs[i].tag == "LOC" {
let location_qualifier = segs[i].element_str(0).unwrap_or("").to_owned();
let location_code = segs[i].component_str(1, 0).unwrap_or("").to_owned();
let mut quantity = String::new();
let mut quantity_qualifier = String::new();
let mut unit = None;
let mut status = None;
let mut delivery_start = None;
let mut delivery_end = None;
let mut j = i + 1;
while j < segs.len() && segs[j].tag != "LOC" {
match segs[j].tag.as_str() {
"QTY" => {
segs[j]
.component_str(0, 0)
.unwrap_or("")
.clone_into(&mut quantity_qualifier);
segs[j]
.component_str(0, 1)
.unwrap_or("")
.clone_into(&mut quantity);
unit = segs[j].component_str(0, 2).map(str::to_owned);
}
"STS" => {
status = segs[j].component_str(0, 0).map(DelresStatus::from_sts);
}
"DTM" => {
let q = segs[j].component_str(0, 0).unwrap_or("");
let v = segs[j].component_str(0, 1).map(str::to_owned);
match q {
"2" => delivery_start = v,
"3" => delivery_end = v,
_ => {}
}
}
_ => {}
}
j += 1;
}
if !location_code.is_empty() {
result.push(DeliveryResponseLine {
location_code,
location_qualifier,
quantity,
quantity_qualifier,
unit,
status,
delivery_start,
delivery_end,
});
}
i = j;
} else {
i += 1;
}
}
result
}
#[cfg(test)]
mod tests {
use crate::{DvgwMessageType, message::DvgwMessage};
use super::*;
fn seg(tag: &str, elements: Vec<Vec<&str>>) -> OwnedSegment {
use edifact_rs::{OwnedElement, Span};
OwnedSegment {
tag: tag.to_owned(),
span: Span::new(0, 0),
tag_span: Span::new(0, 0),
elements: elements
.into_iter()
.map(|comps| OwnedElement {
span: Span::new(0, 0),
components: comps
.into_iter()
.map(|c| (c.to_owned(), Span::new(0, 0)))
.collect(),
})
.collect(),
}
}
fn accepted_segments() -> Vec<OwnedSegment> {
vec![
seg(
"UNH",
vec![vec!["00001"], vec!["DELRES", "", "", "", "3.0"]],
),
seg("BGM", vec![vec![""], vec!["DRES-001"], vec!["9"]]),
seg("NAD", vec![vec!["MS"], vec!["21X000000001370C"]]),
seg("NAD", vec![vec!["MR"], vec!["21X000000001368S"]]),
seg("DTM", vec![vec!["137", "20260115", "102"]]),
seg("RFF", vec![vec!["Z13", "DORD-001"]]),
seg("STS", vec![vec!["Z01"]]),
seg("UNT", vec![vec!["7"], vec!["00001"]]),
]
}
fn rejected_segments() -> Vec<OwnedSegment> {
vec![
seg(
"UNH",
vec![vec!["00001"], vec!["DELRES", "", "", "", "3.0"]],
),
seg("BGM", vec![vec![""], vec!["DRES-002"], vec!["9"]]),
seg("NAD", vec![vec!["MS"], vec!["21X000000001370C"]]),
seg("NAD", vec![vec!["MR"], vec!["21X000000001368S"]]),
seg("DTM", vec![vec!["137", "20260115", "102"]]),
seg("RFF", vec![vec!["Z13", "DORD-002"]]),
seg("STS", vec![vec!["Z03"]]),
seg(
"FTX",
vec![
vec!["AAI"],
vec![""],
vec![""],
vec!["Insufficient capacity at delivery point"],
],
),
seg("UNT", vec![vec!["8"], vec!["00001"]]),
]
}
#[test]
fn from_segments_accepted() {
let msg = DeliveryResponseMessage::from_segments(accepted_segments());
assert_eq!(msg.message_type(), DvgwMessageType::Delres);
assert_eq!(msg.sender_eic(), Some("21X000000001370C"));
assert_eq!(msg.receiver_eic(), Some("21X000000001368S"));
assert_eq!(msg.response_ref.as_deref(), Some("DRES-001"));
assert_eq!(msg.order_ref.as_deref(), Some("DORD-001"));
assert_eq!(msg.status, Some(DelresStatus::Accepted));
assert!(msg.rejection_reason.is_none());
}
#[test]
fn from_segments_rejected_with_reason() {
let msg = DeliveryResponseMessage::from_segments(rejected_segments());
assert_eq!(msg.status, Some(DelresStatus::Rejected));
assert_eq!(
msg.rejection_reason.as_deref(),
Some("Insufficient capacity at delivery point")
);
assert_eq!(msg.order_ref.as_deref(), Some("DORD-002"));
}
#[test]
fn delres_status_display() {
assert_eq!(DelresStatus::Accepted.to_string(), "Accepted");
assert_eq!(DelresStatus::Modified.to_string(), "Modified");
assert_eq!(DelresStatus::Rejected.to_string(), "Rejected");
}
}