use std::marker::PhantomData;
use edifact_rs::Writer;
use crate::AgencyCode;
use crate::{Error, Release};
use super::{Set, Unset, bytes_to_segments};
fn midnight_303(date: &str) -> String {
format!("{date}0000+0000")
}
fn now_303() -> String {
let now = time::OffsetDateTime::now_utc();
let (y, mo, d, h, m) = (
now.year(),
now.month() as u8,
now.day(),
now.hour(),
now.minute(),
);
format!("{y:04}{mo:02}{d:02}{h:02}{m:02}+0000")
}
#[derive(Debug, Clone)]
pub struct PricatPriceEntry {
pub qualifier: String,
pub amount: String,
pub currency: String,
pub range_low: Option<String>,
pub range_high: Option<String>,
pub valid_from: Option<String>,
pub valid_to: Option<String>,
}
impl PricatPriceEntry {
pub fn new(
qualifier: impl Into<String>,
amount: impl Into<String>,
currency: impl Into<String>,
) -> Self {
Self {
qualifier: qualifier.into(),
amount: amount.into(),
currency: currency.into(),
range_low: None,
range_high: None,
valid_from: None,
valid_to: None,
}
}
pub fn range(mut self, low: impl Into<String>, high: impl Into<String>) -> Self {
self.range_low = Some(low.into());
self.range_high = Some(high.into());
self
}
pub fn valid_from(mut self, dt: impl Into<String>) -> Self {
self.valid_from = Some(dt.into());
self
}
pub fn valid_to(mut self, dt: impl Into<String>) -> Self {
self.valid_to = Some(dt.into());
self
}
}
#[derive(Debug, Clone)]
pub struct PricatLineItem {
pub position: Option<u32>,
pub schedule_id: Option<String>,
pub schedule_id_qualifier: Option<String>,
pub description: Option<String>,
pub prices: Vec<PricatPriceEntry>,
}
impl PricatLineItem {
#[must_use]
pub fn new(price: PricatPriceEntry) -> Self {
Self {
position: None,
schedule_id: None,
schedule_id_qualifier: None,
description: None,
prices: vec![price],
}
}
#[must_use]
pub fn position(mut self, pos: u32) -> Self {
self.position = Some(pos);
self
}
pub fn schedule_id(mut self, id: impl Into<String>) -> Self {
self.schedule_id = Some(id.into());
self
}
pub fn schedule_id_qualifier(mut self, q: impl Into<String>) -> Self {
self.schedule_id_qualifier = Some(q.into());
self
}
pub fn description(mut self, text: impl Into<String>) -> Self {
self.description = Some(text.into());
self
}
#[must_use]
pub fn add_price(mut self, price: PricatPriceEntry) -> Self {
self.prices.push(price);
self
}
}
#[derive(Debug, Clone)]
pub struct PricatPriceGroup {
pub group_type: String,
pub items: Vec<PricatLineItem>,
}
impl PricatPriceGroup {
pub fn new(group_type: impl Into<String>, item: PricatLineItem) -> Self {
Self {
group_type: group_type.into(),
items: vec![item],
}
}
#[must_use]
pub fn add_item(mut self, item: PricatLineItem) -> Self {
self.items.push(item);
self
}
}
#[derive(Debug, Clone)]
struct PricatBuilderInner {
release: Release,
sender_id: Option<String>,
receiver_id: Option<String>,
sender_agency: AgencyCode,
receiver_agency: AgencyCode,
message_ref: String,
document_code: String,
document_id: Option<String>,
document_date: Option<String>,
pruefidentifikator: Option<u32>,
price_groups: Vec<PricatPriceGroup>,
}
#[derive(Debug, Clone)]
#[must_use = "Builder must be consumed via .build() or .serialize()"]
pub struct PricatBuilder<S = Unset, R = Unset> {
_ph: PhantomData<fn() -> (S, R)>,
inner: PricatBuilderInner,
}
impl PricatBuilder<Unset, Unset> {
pub fn new(release: Release) -> Self {
Self {
_ph: PhantomData,
inner: PricatBuilderInner {
release,
sender_id: None,
receiver_id: None,
sender_agency: AgencyCode::Bdew,
receiver_agency: AgencyCode::Bdew,
message_ref: "1".to_owned(),
document_code: "Z04".to_owned(),
document_id: None,
document_date: None,
pruefidentifikator: None,
price_groups: Vec::new(),
},
}
}
}
impl<S, R> PricatBuilder<S, R> {
fn transition<S2, R2>(self) -> PricatBuilder<S2, R2> {
PricatBuilder {
_ph: PhantomData,
inner: self.inner,
}
}
pub fn sender(mut self, id: impl Into<String>) -> PricatBuilder<Set, R> {
self.inner.sender_id = Some(id.into());
self.transition()
}
pub fn receiver(mut self, id: impl Into<String>) -> PricatBuilder<S, Set> {
self.inner.receiver_id = Some(id.into());
self.transition()
}
pub fn sender_agency(mut self, agency: crate::AgencyCode) -> Self {
self.inner.sender_agency = agency;
self
}
pub fn receiver_agency(mut self, agency: crate::AgencyCode) -> Self {
self.inner.receiver_agency = agency;
self
}
pub fn document_id(mut self, id: impl Into<String>) -> Self {
self.inner.document_id = Some(id.into());
self
}
pub fn document_code(mut self, code: impl Into<String>) -> Self {
self.inner.document_code = code.into();
self
}
pub fn message_ref(mut self, reference: impl Into<String>) -> Self {
self.inner.message_ref = reference.into();
self
}
pub fn document_date(mut self, date: impl Into<String>) -> Self {
self.inner.document_date = Some(date.into());
self
}
pub fn document_datetime(mut self, datetime: impl Into<String>) -> Self {
self.inner.document_date = Some(format!("__303:{}", datetime.into()));
self
}
pub fn pruefidentifikator(mut self, pid: u32) -> Self {
self.inner.pruefidentifikator = Some(pid);
self
}
pub fn add_price_group(mut self, group: PricatPriceGroup) -> Self {
self.inner.price_groups.push(group);
self
}
fn to_bytes(&self) -> Result<Vec<u8>, Error> {
let dtm_val = match self.inner.document_date.as_deref() {
None => now_303(),
Some(s) if s.starts_with("__303:") => s["__303:".len()..].to_owned(),
Some(date) => midnight_303(date),
};
let mut buf = Vec::new();
let mut w = Writer::new(&mut buf);
let doc_id = self.inner.document_id.as_deref().unwrap_or("");
emit_comp!(
w,
"UNH",
[&self.inner.message_ref],
["PRICAT", "D", "20B", "UN", self.inner.release.as_str()]
);
emit_seg!(w, "BGM", &self.inner.document_code, doc_id);
if let Some(pid) = self.inner.pruefidentifikator {
emit_comp!(w, "RFF", ["Z13", &pid.to_string()]);
}
emit_comp!(w, "DTM", ["137", &dtm_val, "303"]);
if let Some(id) = &self.inner.sender_id {
emit_comp!(
w,
"NAD",
["MS"],
[id, "", self.inner.sender_agency.as_str()]
);
}
if let Some(id) = &self.inner.receiver_id {
emit_comp!(
w,
"NAD",
["MR"],
[id, "", self.inner.receiver_agency.as_str()]
);
}
for (group_idx, group) in self.inner.price_groups.iter().enumerate() {
emit_seg!(w, "PGI", &group.group_type);
for (item_idx, item) in group.items.iter().enumerate() {
#[expect(clippy::cast_possible_truncation)]
let pos = item
.position
.unwrap_or((group_idx * 1000 + item_idx + 1) as u32);
emit_seg!(w, "LIN", &pos.to_string());
if let Some(id) = &item.schedule_id {
let q = item.schedule_id_qualifier.as_deref().unwrap_or("1");
emit_comp!(w, "PIA", [q], [id, "", "ZZZ"]);
}
if let Some(desc) = &item.description {
emit_comp!(w, "IMD", ["F"], [""], ["", "", "", desc]);
}
for price in &item.prices {
emit_comp!(w, "PRI", [&price.qualifier, &price.amount, &price.currency]);
if let (Some(low), Some(high)) = (&price.range_low, &price.range_high) {
emit_comp!(w, "RNG", ["10"], [low, high]);
}
if let Some(from) = &price.valid_from {
emit_comp!(w, "DTM", ["163", from, "303"]);
}
if let Some(to) = &price.valid_to {
emit_comp!(w, "DTM", ["164", to, "303"]);
}
}
}
}
w.finish_unt(&self.inner.message_ref)
.map_err(Error::Parse)?;
Ok(buf)
}
pub fn serialize(self) -> Result<Vec<u8>, Error> {
self.to_bytes()
}
}
impl PricatBuilder<Set, Set> {
pub fn build(self) -> Result<crate::messages::pricat::PricatMessage, Error> {
let pid = self.inner.pruefidentifikator;
let message_ref = self.inner.message_ref.clone();
let assoc_code = self.inner.release.as_str().to_owned();
let segments = bytes_to_segments(&self.to_bytes()?)?;
Ok(crate::messages::pricat::PricatMessage::from_parts(
segments,
message_ref.as_str(),
assoc_code.as_str(),
pid,
))
}
}