edi-energy 0.16.0

EDI@Energy EDIFACT parser and validator for the German energy market
Documentation
//! [`RemadvBuilder`] — fluent type-safe builder for REMADV messages.

use std::marker::PhantomData;

use edifact_rs::Writer;

use crate::AgencyCode;
use crate::{Error, Release};

use super::{Set, Unset, bytes_to_segments};

#[derive(Debug, Clone)]
struct RemadvBuilderInner {
    release: Release,
    sender_id: Option<String>,
    receiver_id: Option<String>,
    sender_agency: AgencyCode,
    receiver_agency: AgencyCode,
    message_ref: String,
    document_code: Option<String>,
    document_id: Option<String>,
    document_date: Option<String>,
    abweichungsgruende: Vec<Abweichungsgrund>,
}

/// `AJT` — an Abweichungsgrund on a REMADV Rückmeldung.
///
/// The invoice recipient's rejection reason: DE 4465 carries the **Antwortcode**
/// („Code des Prüfschritts") and DE 1082 the **EBD** it is drawn from —
/// `AJT+A70+E_0406'`. Structurally the REMADV twin of UTILMD's
/// `STS+E01++<code>:<ebd>`, and required for the same reason: a rejection
/// without its code gives the sender nothing to correct.
///
/// The MIG places it at two levels — `SG7` on the Kopfebene (once) and `SG12`
/// per Rechnungsposition (up to ten), which is the shape `E_0406`'s
/// Kopf-/Positions-/Summenebene traversal produces.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Abweichungsgrund {
    /// DE 4465 — the Antwortcode.
    pub code: String,
    /// DE 1082 — the EBD that publishes it (`E_0406`, `E_0519`, …).
    pub ebd: Option<String>,
}

impl Abweichungsgrund {
    /// An Abweichungsgrund drawn from a named EBD.
    #[must_use]
    pub fn new(code: impl Into<String>, ebd: impl Into<String>) -> Self {
        Self {
            code: code.into(),
            ebd: Some(ebd.into()),
        }
    }
}

/// Fluent builder for `REMADV` (Remittance Advice) messages.
///
/// Wire type string: `REMADV:D:05A:UN:{release}`.
///
/// # Type-state
///
/// [`build`](RemadvBuilder::build) is only available once both
/// [`sender`](RemadvBuilder::sender) and [`receiver`](RemadvBuilder::receiver)
/// have been called.
///
/// # Example
///
/// ```rust,no_run
/// use edi_energy::Release;
/// use edi_energy::builders::RemadvBuilder;
///
/// let msg = RemadvBuilder::new(Release::new("2.9e"))
///     .sender("4012345000023")
///     .receiver("9900357000004")
///     .build()?;
///
/// assert_eq!(msg.sender().unwrap().party_id.as_deref(), Some("4012345000023"));
/// # Ok::<(), edi_energy::Error>(())
/// ```
#[derive(Debug, Clone)]
#[must_use = "Builder must be consumed via .build() or .serialize()"]
pub struct RemadvBuilder<S = Unset, R = Unset> {
    _ph: PhantomData<fn() -> (S, R)>,
    inner: RemadvBuilderInner,
}

impl RemadvBuilder<Unset, Unset> {
    /// Create a builder targeting the given EDI@Energy release.
    pub fn new(release: Release) -> Self {
        Self {
            _ph: PhantomData,
            inner: RemadvBuilderInner {
                release,
                sender_id: None,
                receiver_id: None,
                sender_agency: AgencyCode::Bdew,
                receiver_agency: AgencyCode::Bdew,
                message_ref: "1".to_owned(),
                document_code: None,
                document_id: None,
                document_date: None,
                abweichungsgruende: Vec::new(),
            },
        }
    }
}

impl<S, R> RemadvBuilder<S, R> {
    fn transition<S2, R2>(self) -> RemadvBuilder<S2, R2> {
        RemadvBuilder {
            _ph: PhantomData,
            inner: self.inner,
        }
    }

    /// Set the message sender's market-participant identifier.
    pub fn sender(mut self, id: impl Into<String>) -> RemadvBuilder<Set, R> {
        self.inner.sender_id = Some(id.into());
        self.transition()
    }

    /// Set the message recipient's market-participant identifier.
    pub fn receiver(mut self, id: impl Into<String>) -> RemadvBuilder<S, Set> {
        self.inner.receiver_id = Some(id.into());
        self.transition()
    }

    /// Override the agency code for the sender's party identifier.
    ///
    /// Default: [`AgencyCode::Bdew`] (`"293"`). Use [`AgencyCode::Etso`] (`"305"`)
    /// for TSO/ÜNB parties that carry a 16-char EIC code.
    pub fn sender_agency(mut self, agency: crate::AgencyCode) -> Self {
        self.inner.sender_agency = agency;
        self
    }

    /// Override the agency code for the receiver's party identifier.
    ///
    /// Default: [`AgencyCode::Bdew`] (`"293"`).
    pub fn receiver_agency(mut self, agency: crate::AgencyCode) -> Self {
        self.inner.receiver_agency = agency;
        self
    }

    /// Override the BGM document type code.  Defaults to `"239"`.
    pub fn document_code(mut self, code: impl Into<String>) -> Self {
        self.inner.document_code = Some(code.into());
        self
    }

    /// Set the BGM document identifier (Avisnummer).
    pub fn document_id(mut self, id: impl Into<String>) -> Self {
        self.inner.document_id = Some(id.into());
        self
    }

    /// Override the message reference number. Defaults to `"1"`.
    pub fn message_ref(mut self, reference: impl Into<String>) -> Self {
        self.inner.message_ref = reference.into();
        self
    }

    /// Set the document date for DTM+137 (`YYYYMMDD`).
    pub fn document_date(mut self, date: impl Into<String>) -> Self {
        self.inner.document_date = Some(date.into());
        self
    }

    /// Add an `SG7 AJT` Abweichungsgrund (Kopfebene).
    ///
    /// A REMADV Abweisung must state why: DE 4465 the Antwortcode, DE 1082 the
    /// EBD it comes from.
    ///
    /// ```rust
    /// # use edi_energy::{Release, builders::{RemadvBuilder, Abweichungsgrund}};
    /// let edi = RemadvBuilder::new(Release::new("2.9e"))
    ///     .sender("9900987654321")
    ///     .receiver("9900123456789")
    ///     .abweichungsgrund(Abweichungsgrund::new("A70", "E_0406"))
    ///     .serialize()?;
    /// assert!(String::from_utf8(edi).unwrap().contains("AJT+A70+E_0406"));
    /// # Ok::<(), edi_energy::Error>(())
    /// ```
    pub fn abweichungsgrund(mut self, grund: Abweichungsgrund) -> Self {
        self.inner.abweichungsgruende.push(grund);
        self
    }

    fn to_bytes(&self) -> Result<Vec<u8>, Error> {
        let dtm_val = self
            .inner
            .document_date
            .as_deref()
            .map_or_else(super::now_ccyymmddhhmm, str::to_owned);

        let mut buf = Vec::new();
        let mut w = Writer::new(&mut buf);

        let code = self.inner.document_code.as_deref().unwrap_or("239");
        let doc_id = self.inner.document_id.as_deref().unwrap_or("");
        emit_comp!(
            w,
            "UNH",
            [&self.inner.message_ref],
            ["REMADV", "D", "05A", "UN", self.inner.release.as_str()]
        );
        emit_seg!(w, "BGM", code, doc_id);
        // `DTM+137` Dokumentendatum. Every EDI@Energy AHB gives DE 2379 as
        // `303` (`CCYYMMDDHHMMZZZ`) with condition `[931]` fixing the zone to
        // `+00`; `[494]` requires the stamp to be the creation moment or
        // earlier. There is no Anwendungsfall in any AHB that takes `102`.
        emit_comp!(w, "DTM", ["137", &super::ccyymmddhhmm_utc(&dtm_val), "303"]);
        for grund in &self.inner.abweichungsgruende {
            // `SG7 AJT` — Abweichungsgrund auf Kopfebene.
            if let Some(ebd) = grund.ebd.as_deref() {
                emit_seg!(w, "AJT", &grund.code, ebd);
            } else {
                emit_seg!(w, "AJT", &grund.code);
            }
        }
        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()]
            );
        }
        w.finish_unt(&self.inner.message_ref)
            .map_err(Error::Parse)?;
        Ok(buf)
    }
    /// Build and serialize the message to EDIFACT bytes.
    ///
    /// # Errors
    ///
    /// Returns an [`Error`] if serialization fails.
    pub fn serialize(self) -> Result<Vec<u8>, Error> {
        self.to_bytes()
    }
}

impl RemadvBuilder<Set, Set> {
    /// Build and return a fully-parsed [`crate::messages::remadv::RemadvMessage`].
    ///
    /// # Errors
    ///
    /// Returns an [`Error`] if EDIFACT serialization or parsing fails.
    pub fn build(self) -> Result<crate::messages::remadv::RemadvMessage, Error> {
        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::remadv::RemadvMessage::from_parts(
            segments,
            message_ref.as_str(),
            assoc_code.as_str(),
            None,
        ))
    }
}