use time::{OffsetDateTime, UtcOffset};
use crate::{
datetime::{DvgwPeriod, format_instant, format_period},
document::{DVGW_AGENCY_CODE, DvgwDocument},
error::Error,
model::{nad, rff},
};
#[derive(Debug, Clone, Default)]
pub struct Position {
number: Option<String>,
item_type: Option<String>,
description: Option<String>,
locations: Vec<LocationDraft>,
parties: Vec<PartyDraft>,
}
#[derive(Debug, Clone)]
struct PartyDraft {
role: String,
code: String,
agency: String,
}
#[derive(Debug, Clone)]
struct LocationDraft {
qualifier: String,
code: Option<String>,
quantities: Vec<QuantityDraft>,
}
#[derive(Debug, Clone)]
struct QuantityDraft {
qualifier: String,
value: String,
unit: Option<String>,
period: DvgwPeriod,
status: Vec<String>,
}
impl Position {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn number(mut self, number: impl Into<String>) -> Self {
self.number = Some(number.into());
self
}
#[must_use]
pub fn item_type(mut self, code: impl Into<String>) -> Self {
self.item_type = Some(code.into());
self
}
#[must_use]
pub fn description(mut self, code: impl Into<String>) -> Self {
self.description = Some(code.into());
self
}
#[must_use]
pub fn location(mut self, qualifier: impl Into<String>, code: Option<&str>) -> Self {
self.locations.push(LocationDraft {
qualifier: qualifier.into(),
code: code.map(str::to_owned),
quantities: Vec::new(),
});
self
}
#[must_use]
pub fn quantity(
self,
qualifier: impl Into<String>,
value: impl Into<String>,
period: DvgwPeriod,
) -> Self {
self.push_quantity(qualifier.into(), value.into(), None, period)
}
#[must_use]
pub fn quantity_in(
self,
qualifier: impl Into<String>,
value: impl Into<String>,
unit: impl Into<String>,
period: DvgwPeriod,
) -> Self {
self.push_quantity(qualifier.into(), value.into(), Some(unit.into()), period)
}
fn push_quantity(
mut self,
qualifier: String,
value: String,
unit: Option<String>,
period: DvgwPeriod,
) -> Self {
let location = self
.locations
.last_mut()
.expect("call Position::location before Position::quantity");
location.quantities.push(QuantityDraft {
qualifier,
value,
unit,
period,
status: Vec::new(),
});
self
}
#[must_use]
pub fn status(mut self, code: impl Into<String>) -> Self {
self.locations
.last_mut()
.and_then(|l| l.quantities.last_mut())
.expect("call Position::quantity before Position::status")
.status
.push(code.into());
self
}
#[must_use]
pub fn party(self, role: impl Into<String>, code: impl Into<String>) -> Self {
self.party_coded(role, code, DVGW_AGENCY_CODE)
}
#[must_use]
pub fn party_coded(
mut self,
role: impl Into<String>,
code: impl Into<String>,
agency: impl Into<String>,
) -> Self {
self.parties.push(PartyDraft {
role: role.into(),
code: code.into(),
agency: agency.into(),
});
self
}
}
fn esc(value: &str) -> String {
let mut out = String::with_capacity(value.len());
for c in value.chars() {
if matches!(c, '+' | ':' | '\'' | '?') {
out.push('?');
}
out.push(c);
}
out
}
#[derive(Debug, Clone)]
pub struct MessageBuilder {
document: DvgwDocument,
message_ref: String,
document_number: String,
version: Option<String>,
timezone: UtcOffset,
pruefidentifikator: Option<u32>,
message_datetime: Option<OffsetDateTime>,
validity_period: Option<DvgwPeriod>,
clearingnummer: Option<String>,
original_nomination: Option<(String, OffsetDateTime)>,
references: Vec<(String, String)>,
parties: Vec<PartyDraft>,
positions: Vec<Position>,
}
impl MessageBuilder {
#[must_use]
pub fn new(document: DvgwDocument) -> Self {
Self {
document,
message_ref: "1".to_owned(),
document_number: String::new(),
version: None,
timezone: UtcOffset::UTC,
pruefidentifikator: None,
message_datetime: None,
validity_period: None,
clearingnummer: None,
original_nomination: None,
references: Vec::new(),
parties: Vec::new(),
positions: Vec::new(),
}
}
#[must_use]
pub fn message_ref(mut self, value: impl Into<String>) -> Self {
self.message_ref = value.into();
self
}
#[must_use]
pub fn document_number(mut self, value: impl Into<String>) -> Self {
self.document_number = value.into();
self
}
#[must_use]
pub fn version(mut self, value: impl Into<String>) -> Self {
self.version = Some(value.into());
self
}
#[must_use]
pub fn pruefidentifikator(mut self, pid: u32) -> Self {
self.pruefidentifikator = Some(pid);
self
}
#[must_use]
pub fn message_datetime(mut self, value: OffsetDateTime) -> Self {
self.message_datetime = Some(value);
self
}
#[must_use]
pub fn validity_period(mut self, period: DvgwPeriod) -> Self {
self.validity_period = Some(period);
self
}
#[must_use]
pub fn sender(self, code: impl Into<String>) -> Self {
self.party(nad::ABSENDER, code)
}
#[must_use]
pub fn sender_coded(self, code: impl Into<String>, agency: impl Into<String>) -> Self {
self.party_coded(nad::ABSENDER, code, agency)
}
#[must_use]
pub fn receiver(self, code: impl Into<String>) -> Self {
self.party(nad::EMPFAENGER, code)
}
#[must_use]
pub fn receiver_coded(self, code: impl Into<String>, agency: impl Into<String>) -> Self {
self.party_coded(nad::EMPFAENGER, code, agency)
}
#[must_use]
pub fn party(self, role: impl Into<String>, code: impl Into<String>) -> Self {
self.party_coded(role, code, DVGW_AGENCY_CODE)
}
#[must_use]
pub fn party_coded(
mut self,
role: impl Into<String>,
code: impl Into<String>,
agency: impl Into<String>,
) -> Self {
self.parties.push(PartyDraft {
role: role.into(),
code: code.into(),
agency: agency.into(),
});
self
}
#[must_use]
pub fn clearingnummer(mut self, value: impl Into<String>) -> Self {
self.clearingnummer = Some(value.into());
self
}
#[must_use]
pub fn original_nomination(
mut self,
value: impl Into<String>,
processed_at: OffsetDateTime,
) -> Self {
self.original_nomination = Some((value.into(), processed_at));
self
}
#[must_use]
pub fn reference(mut self, qualifier: impl Into<String>, value: impl Into<String>) -> Self {
self.references.push((qualifier.into(), value.into()));
self
}
#[must_use]
pub fn position(mut self, position: Position) -> Self {
self.positions.push(position);
self
}
pub fn build(&self) -> Result<Vec<u8>, Error> {
let missing = |what: &str| Error::Serialize(format!("{what} is required but was not set"));
if self.document_number.is_empty() {
return Err(missing("BGM C106 DE 1004 Dokumentennummer"));
}
let pid = self
.pruefidentifikator
.ok_or_else(|| missing("SG1 RFF+Z13 Prüfidentifikator"))?;
let message_datetime = self
.message_datetime
.ok_or_else(|| missing("DTM+137 Datum und Zeit der Nachricht"))?;
let validity = self
.validity_period
.ok_or_else(|| missing("DTM+Z01 Gültigkeitszeitraum"))?;
for role in [nad::ABSENDER, nad::EMPFAENGER] {
if !self.parties.iter().any(|p| p.role == role) {
return Err(missing(&format!("NAD+{role}")));
}
}
if self.positions.is_empty() {
return Err(missing("at least one LIN position"));
}
let agency = DVGW_AGENCY_CODE;
let mut segments: Vec<String> = Vec::new();
let family = self.document.message_type();
let version = self
.version
.as_deref()
.unwrap_or_else(|| family.anwendungscode());
segments.push(format!(
"UNH+{}+{}:D:07A:UN:{}",
esc(&self.message_ref),
self.document.carrier().as_str(),
esc(version)
));
segments.push(format!(
"BGM+{}::{agency}+{}",
self.document.code(),
esc(&self.document_number)
));
segments.push(format!("DTM+Z05:{}:805", self.timezone.whole_hours()));
segments.push(format!(
"DTM+137:{}:203",
format_instant(message_datetime, self.timezone)
));
segments.push(format!(
"DTM+Z01:{}:719",
format_period(validity, self.timezone)
));
if let Some(clearing) = &self.clearingnummer {
segments.push(format!("RFF+{}:{}", rff::CLEARINGNUMMER, esc(clearing)));
}
segments.push(format!("RFF+{}:{pid}", rff::PRUEFIDENTIFIKATOR));
if let Some((original, processed_at)) = &self.original_nomination {
segments.push(format!(
"RFF+{}:{}",
rff::ORIGINAL_NOMINIERUNG,
esc(original)
));
segments.push(format!(
"DTM+9:{}:203",
format_instant(*processed_at, self.timezone)
));
}
for (qualifier, value) in &self.references {
segments.push(format!("RFF+{}:{}", esc(qualifier), esc(value)));
}
for party in &self.parties {
segments.push(format!(
"NAD+{}+{}::{}",
esc(&party.role),
esc(&party.code),
esc(&party.agency)
));
}
let default_unit = family.admitted_units()[0];
for (index, position) in self.positions.iter().enumerate() {
self.render_position(position, index, agency, default_unit, &mut segments);
}
segments.push("UNS+S".to_owned());
segments.push(format!(
"UNT+{}+{}",
segments.len() + 1,
esc(&self.message_ref)
));
let mut out = String::new();
for segment in segments {
out.push_str(&segment);
out.push('\'');
}
Ok(out.into_bytes())
}
fn render_position(
&self,
position: &Position,
index: usize,
agency: &str,
default_unit: &str,
segments: &mut Vec<String>,
) {
{
let number = position
.number
.clone()
.unwrap_or_else(|| (index + 1).to_string());
let number = esc(&number);
match &position.item_type {
Some(item_type) => {
segments.push(format!("LIN+{number}++:{}::{agency}", esc(item_type)));
}
None => segments.push(format!("LIN+{number}")),
}
if let Some(code) = &position.description {
segments.push(format!("IMD++05G+{}::{agency}", esc(code)));
}
for location in &position.locations {
let loc = match &location.code {
Some(code) => {
format!("LOC+{}+{}::{agency}", esc(&location.qualifier), esc(code))
}
None => format!("LOC+{}", esc(&location.qualifier)),
};
for quantity in &location.quantities {
segments.push(loc.clone());
segments.push(format!(
"DTM+2:{}:719",
format_period(quantity.period, self.timezone)
));
segments.push(format!(
"QTY+{}:{}:{}",
esc(&quantity.qualifier),
esc(&quantity.value),
esc(quantity.unit.as_deref().unwrap_or(default_unit))
));
for status in &quantity.status {
segments.push(format!("STS+{}::{agency}", esc(status)));
}
}
if location.quantities.is_empty() {
segments.push(loc);
}
}
for party in &position.parties {
segments.push(format!(
"NAD+{}+{}::{}",
esc(&party.role),
esc(&party.code),
esc(&party.agency)
));
}
}
}
}