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<(String, String)>,
}
#[derive(Debug, Clone)]
struct LocationDraft {
qualifier: String,
code: Option<String>,
quantities: Vec<QuantityDraft>,
}
#[derive(Debug, Clone)]
struct QuantityDraft {
qualifier: String,
value: 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(
mut self,
qualifier: impl Into<String>,
value: impl Into<String>,
period: DvgwPeriod,
) -> Self {
let location = self
.locations
.last_mut()
.expect("call Position::location before Position::quantity");
location.quantities.push(QuantityDraft {
qualifier: qualifier.into(),
value: value.into(),
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(mut self, role: impl Into<String>, code: impl Into<String>) -> Self {
self.parties.push((role.into(), code.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>,
references: Vec<(String, String)>,
parties: Vec<(String, String)>,
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,
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(mut self, code: impl Into<String>) -> Self {
self.parties.push((nad::ABSENDER.to_owned(), code.into()));
self
}
#[must_use]
pub fn receiver(mut self, code: impl Into<String>) -> Self {
self.parties.push((nad::EMPFAENGER.to_owned(), code.into()));
self
}
#[must_use]
pub fn party(mut self, role: impl Into<String>, code: impl Into<String>) -> Self {
self.parties.push((role.into(), code.into()));
self
}
#[must_use]
pub fn clearingnummer(self, value: impl Into<String>) -> Self {
self.reference(rff::CLEARINGNUMMER, value)
}
#[must_use]
pub fn original_nomination_ref(self, value: impl Into<String>) -> Self {
self.reference(rff::ORIGINAL_NOMINIERUNG, value)
}
#[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(|(r, _)| r == 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 version = self.version.as_deref().unwrap_or_default();
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)
));
for (qualifier, value) in &self.references {
segments.push(format!("RFF+{}:{}", esc(qualifier), esc(value)));
}
segments.push(format!("RFF+{}:{pid}", rff::PRUEFIDENTIFIKATOR));
for (role, code) in &self.parties {
segments.push(format!("NAD+{}+{}::{agency}", esc(role), esc(code)));
}
for (index, position) in self.positions.iter().enumerate() {
self.render_position(position, index, agency, &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,
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 {
match &location.code {
Some(code) => segments.push(format!(
"LOC+{}+{}::{agency}",
esc(&location.qualifier),
esc(code)
)),
None => segments.push(format!("LOC+{}", esc(&location.qualifier))),
}
for quantity in &location.quantities {
segments.push(format!(
"DTM+2:{}:719",
format_period(quantity.period, self.timezone)
));
segments.push(format!(
"QTY+{}:{}:KW1",
esc(&quantity.qualifier),
esc(&quantity.value)
));
for status in &quantity.status {
segments.push(format!("STS+{}::{agency}", esc(status)));
}
}
}
for (role, code) in &position.parties {
segments.push(format!("NAD+{}+{}::{agency}", esc(role), esc(code)));
}
}
}
}