Skip to main content

dvgw_edi/
document.rs

1//! Message identity: the UN/EDIFACT carrier, the DVGW document-name code, and
2//! the logical message family they resolve to.
3//!
4//! # Why the document code and not `UNH`
5//!
6//! Every DVGW gas-transport format is a *subset of a UN/EDIFACT D.07A message*.
7//! The `UNH` message type therefore names the carrier — `ORDERS` for a
8//! nomination, `ORDRSP` for an allocation or a nomination response — and never
9//! the DVGW message:
10//!
11//! ```text
12//! UNH+1+ORDERS:D:07A:UN:DVGW18'      ← NOMINT 4.6
13//! BGM+01G::332+NOMINT00052'          ← *this* says NOMINT
14//! ```
15//!
16//! Reading `UNH` for the message name makes every conformant message
17//! unrecognisable, so identity is resolved from `BGM` C002 DE 1001 and the
18//! carrier is used only as a cross-check.
19//!
20//! Sources: DVGW-Nachrichtenbeschreibungen ALOCAT 5.11a (ORDRSP / UN D.07A S3),
21//! NOMINT 4.6 (ORDERS / UN D.07A S3), NOMRES 4.7 (ORDRSP / UN D.07A S3),
22//! SSQNOT 5.7 (ORDRSP / UN D.07A S3).
23
24use std::fmt;
25
26/// The UN/EDIFACT message that carries a DVGW format on the wire (`UNH` DE 0065).
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
29pub enum Carrier {
30    /// `ORDERS` — Purchase Order. Carries NOMINT.
31    Orders,
32    /// `ORDRSP` — Purchase Order Response. Carries ALOCAT, NOMRES and SSQNOT.
33    Ordrsp,
34}
35
36impl Carrier {
37    /// The `UNH` DE 0065 value.
38    #[must_use]
39    pub fn as_str(self) -> &'static str {
40        match self {
41            Self::Orders => "ORDERS",
42            Self::Ordrsp => "ORDRSP",
43        }
44    }
45
46    /// Parse a `UNH` DE 0065 value; `None` for anything that is not a DVGW carrier.
47    #[must_use]
48    pub fn from_unh_code(code: &str) -> Option<Self> {
49        match code {
50            "ORDERS" => Some(Self::Orders),
51            "ORDRSP" => Some(Self::Ordrsp),
52            _ => None,
53        }
54    }
55}
56
57impl fmt::Display for Carrier {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        f.write_str(self.as_str())
60    }
61}
62
63/// The logical DVGW message family.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
65#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
66pub enum DvgwMessageType {
67    /// ALOCAT — Allokationsnachricht (NB ↔ MGV ↔ BKV).
68    Alocat,
69    /// NOMINT — Nominierung (Transportkunde → Netz-/Marktgebietsbetreiber).
70    Nomint,
71    /// NOMRES — Nominierungsantwort / Matching-Benachrichtigung.
72    Nomres,
73    /// SSQNOT — Mehr-/Mindermengenmeldung zur Führung des Netzkontos (NB → MGV).
74    Ssqnot,
75}
76
77impl DvgwMessageType {
78    /// The DVGW message name as it appears in the Nachrichtenbeschreibung.
79    #[must_use]
80    pub fn as_str(self) -> &'static str {
81        match self {
82            Self::Alocat => "ALOCAT",
83            Self::Nomint => "NOMINT",
84            Self::Nomres => "NOMRES",
85            Self::Ssqnot => "SSQNOT",
86        }
87    }
88
89    /// The UN/EDIFACT carrier this family is transmitted on.
90    #[must_use]
91    pub fn carrier(self) -> Carrier {
92        match self {
93            Self::Nomint => Carrier::Orders,
94            Self::Alocat | Self::Nomres | Self::Ssqnot => Carrier::Ordrsp,
95        }
96    }
97
98    /// The `UNH` S009 DE 0057 value the Nachrichtenbeschreibung prescribes —
99    /// the message version for ALOCAT (`5.11a`), the Nachrichtentypen-Paket
100    /// for the others (`DVGW17`).
101    #[must_use]
102    pub fn anwendungscode(self) -> &'static str {
103        match self {
104            Self::Alocat => "5.11a",
105            Self::Nomint | Self::Nomres | Self::Ssqnot => "DVGW17",
106        }
107    }
108
109    /// The `QTY` C186 DE 6411 units the Segmentlayout admits, the default first.
110    ///
111    /// ALOCAT states rates (`KW1` kWh/h, `KW2` kWh/d); a nomination states a
112    /// rate or an energy (`KW1`, `KWH`); a Mehr-/Mindermengenmeldung is energy
113    /// only (`KWH`).
114    #[must_use]
115    pub fn admitted_units(self) -> &'static [&'static str] {
116        use crate::model::unit;
117        match self {
118            Self::Alocat => &[unit::KWH_PER_HOUR, unit::KWH_PER_DAY],
119            Self::Nomint | Self::Nomres => &[unit::KWH_PER_HOUR, unit::KWH],
120            Self::Ssqnot => &[unit::KWH],
121        }
122    }
123
124    /// The `QTY` C186 DE 6063 qualifiers the Segmentlayout admits.
125    #[must_use]
126    pub fn admitted_quantity_qualifiers(self) -> &'static [&'static str] {
127        use crate::model::qty;
128        match self {
129            Self::Alocat | Self::Nomint | Self::Nomres => &[qty::EINSPEISUNG, qty::AUSSPEISUNG],
130            Self::Ssqnot => &[qty::MEHRMENGE, qty::MINDERMENGE],
131        }
132    }
133
134    /// The `LOC` DE 3227 qualifiers the Segmentlayout admits.
135    #[must_use]
136    pub fn admitted_location_qualifiers(self) -> &'static [&'static str] {
137        match self {
138            // „In der Nachricht ist keine Angabe eines spezifischen Ortes
139            // erforderlich" — the segment is `LOC+Z99` and nothing else.
140            Self::Alocat | Self::Ssqnot => &["Z99"],
141            Self::Nomint | Self::Nomres => &["172", "Z17", "Z19"],
142        }
143    }
144
145    /// Every document-name code that resolves to this family.
146    #[must_use]
147    pub fn documents(self) -> &'static [DvgwDocument] {
148        use DvgwDocument as D;
149        match self {
150            Self::Alocat => &[
151                D::AllokationSlp,
152                D::KorrigierteMengenmeldungNkp,
153                D::SlpErsatzwerte,
154                D::UntertaegigeAllokation,
155                D::EndgueltigeAllokation,
156                D::KorrigierteAllokationBilanzierungsbrennwert,
157                D::KorrigierteAllokationAbrechnungsbrennwert,
158                D::TaeglicheMengenmeldungNkp,
159            ],
160            Self::Nomint => &[
161                D::NominierungTransportkunde,
162                D::NominierungVirtuellerHandelspunkt,
163                D::Flexibilitaetsuebertragung,
164                D::NominierungGebuendelteKapazitaet,
165                D::NominierungsweitergabeNetzbetreiber,
166            ],
167            Self::Nomres => &[
168                D::MatchingBenachrichtigung,
169                D::Bestaetigung,
170                D::VhpMatchingBenachrichtigung,
171                D::VhpBestaetigung,
172                D::BestaetigungFlexibilitaetsuebertragung,
173            ],
174            Self::Ssqnot => &[D::MehrMindermengenmeldung],
175        }
176    }
177
178    /// All families, in catalogue order.
179    pub const ALL: [Self; 4] = [Self::Alocat, Self::Nomint, Self::Nomres, Self::Ssqnot];
180}
181
182impl fmt::Display for DvgwMessageType {
183    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184        f.write_str(self.as_str())
185    }
186}
187
188/// The DVGW document-name code from `BGM` C002 DE 1001 — the field that says
189/// which business message this actually is.
190///
191/// The variant set is exhaustive for the current Nachrichtentypen-Paket; a code
192/// outside it surfaces as [`Error::UnknownDocumentCode`](crate::Error::UnknownDocumentCode)
193/// rather than being guessed at.
194#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
195#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
196#[non_exhaustive]
197pub enum DvgwDocument {
198    // ── ALOCAT (ORDRSP) ──────────────────────────────────────────────────────
199    /// `X1G` — Allokation anhand von Standardlastprofilen (SLP).
200    AllokationSlp,
201    /// `X2G` — Korrigierte Mengenmeldung NKP je Netzkonto.
202    KorrigierteMengenmeldungNkp,
203    /// `X3G` — SLP-Ersatzwerte.
204    SlpErsatzwerte,
205    /// `X4G` — Untertägige Allokation (Intraday).
206    UntertaegigeAllokation,
207    /// `X5G` — Endgültige Allokation (Bilanzierungsbrennwert).
208    EndgueltigeAllokation,
209    /// `X6G` — Korrigierte Allokation (Bilanzierungsbrennwert).
210    KorrigierteAllokationBilanzierungsbrennwert,
211    /// `X7G` — Korrigierte Allokation (Abrechnungsbrennwert).
212    KorrigierteAllokationAbrechnungsbrennwert,
213    /// `XBG` — Tägliche Mengenmeldung NKP je Netzkonto.
214    TaeglicheMengenmeldungNkp,
215
216    // ── NOMINT (ORDERS) ──────────────────────────────────────────────────────
217    /// `01G` — Nominierung von einem Transportkunden.
218    NominierungTransportkunde,
219    /// `55G` — Nominierung an einem Virtuellen Handelspunkt.
220    NominierungVirtuellerHandelspunkt,
221    /// `Y1G` — Flexibilitätsübertragung.
222    Flexibilitaetsuebertragung,
223    /// `Y6G` — Nominierung gebündelter Kapazität an MÜP und GÜP.
224    NominierungGebuendelteKapazitaet,
225    /// `Y7G` — Nominierungsweitergabe zwischen Netzbetreibern.
226    NominierungsweitergabeNetzbetreiber,
227
228    // ── NOMRES (ORDRSP) ──────────────────────────────────────────────────────
229    /// `07G` — Matching-Benachrichtigung.
230    MatchingBenachrichtigung,
231    /// `08G` — Bestätigung.
232    Bestaetigung,
233    /// `19G` — Virtueller Handelspunkt: Matching-Benachrichtigung.
234    VhpMatchingBenachrichtigung,
235    /// `20G` — Virtueller Handelspunkt: Bestätigung.
236    VhpBestaetigung,
237    /// `Y2G` — Bestätigung Flexibilitätsübertragung.
238    BestaetigungFlexibilitaetsuebertragung,
239
240    // ── SSQNOT (ORDRSP) ──────────────────────────────────────────────────────
241    /// `BAG` — Mehr-/Mindermengenmeldung zur Führung des Netzkontos.
242    MehrMindermengenmeldung,
243}
244
245/// `(wire code, document, German description)` — the single table every lookup
246/// on [`DvgwDocument`] reads, so a new code is one line rather than three.
247const CATALOGUE: &[(&str, DvgwDocument, &str)] = {
248    use DvgwDocument as D;
249    &[
250        (
251            "X1G",
252            D::AllokationSlp,
253            "Allokation anhand von Standardlastprofilen (SLP)",
254        ),
255        (
256            "X2G",
257            D::KorrigierteMengenmeldungNkp,
258            "Korrigierte Mengenmeldung NKP je Netzkonto",
259        ),
260        ("X3G", D::SlpErsatzwerte, "SLP-Ersatzwerte"),
261        (
262            "X4G",
263            D::UntertaegigeAllokation,
264            "Untertägige Allokation (Intraday)",
265        ),
266        (
267            "X5G",
268            D::EndgueltigeAllokation,
269            "Endgültige Allokation (Bilanzierungsbrennwert)",
270        ),
271        (
272            "X6G",
273            D::KorrigierteAllokationBilanzierungsbrennwert,
274            "Korrigierte Allokation (Bilanzierungsbrennwert)",
275        ),
276        (
277            "X7G",
278            D::KorrigierteAllokationAbrechnungsbrennwert,
279            "Korrigierte Allokation (Abrechnungsbrennwert)",
280        ),
281        (
282            "XBG",
283            D::TaeglicheMengenmeldungNkp,
284            "Tägliche Mengenmeldung NKP je Netzkonto",
285        ),
286        (
287            "01G",
288            D::NominierungTransportkunde,
289            "Nominierung von einem Transportkunden",
290        ),
291        (
292            "55G",
293            D::NominierungVirtuellerHandelspunkt,
294            "Nominierung an einem Virtuellen Handelspunkt",
295        ),
296        (
297            "Y1G",
298            D::Flexibilitaetsuebertragung,
299            "Flexibilitätsübertragung",
300        ),
301        (
302            "Y6G",
303            D::NominierungGebuendelteKapazitaet,
304            "Nominierung gebündelter Kapazität an MÜP und GÜP",
305        ),
306        (
307            "Y7G",
308            D::NominierungsweitergabeNetzbetreiber,
309            "Nominierungsweitergabe zwischen Netzbetreibern",
310        ),
311        (
312            "07G",
313            D::MatchingBenachrichtigung,
314            "Matching-Benachrichtigung",
315        ),
316        ("08G", D::Bestaetigung, "Bestätigung"),
317        (
318            "19G",
319            D::VhpMatchingBenachrichtigung,
320            "Virtueller Handelspunkt: Matching-Benachrichtigung",
321        ),
322        (
323            "20G",
324            D::VhpBestaetigung,
325            "Virtueller Handelspunkt: Bestätigung",
326        ),
327        (
328            "Y2G",
329            D::BestaetigungFlexibilitaetsuebertragung,
330            "Bestätigung Flexibilitätsübertragung",
331        ),
332        (
333            "BAG",
334            D::MehrMindermengenmeldung,
335            "Mehr-/Mindermengenmeldung zur Führung des Netzkontos",
336        ),
337    ]
338};
339
340impl DvgwDocument {
341    /// The `BGM` C002 DE 1001 wire code.
342    #[must_use]
343    pub fn code(self) -> &'static str {
344        CATALOGUE
345            .iter()
346            .find(|(_, d, _)| *d == self)
347            .map_or("", |(c, _, _)| *c)
348    }
349
350    /// Parse a `BGM` DE 1001 value; `None` for codes outside the DVGW catalogue.
351    #[must_use]
352    pub fn from_code(code: &str) -> Option<Self> {
353        CATALOGUE
354            .iter()
355            .find(|(c, _, _)| *c == code)
356            .map(|(_, d, _)| *d)
357    }
358
359    /// The German description from the Nachrichtenbeschreibung.
360    #[must_use]
361    pub fn description(self) -> &'static str {
362        CATALOGUE
363            .iter()
364            .find(|(_, d, _)| *d == self)
365            .map_or("", |(_, _, t)| *t)
366    }
367
368    /// The logical message family this code belongs to.
369    #[must_use]
370    pub fn message_type(self) -> DvgwMessageType {
371        for mt in DvgwMessageType::ALL {
372            if mt.documents().contains(&self) {
373                return mt;
374            }
375        }
376        unreachable!("every DvgwDocument is listed in exactly one DvgwMessageType::documents()")
377    }
378
379    /// The UN/EDIFACT carrier that transmits this document.
380    #[must_use]
381    pub fn carrier(self) -> Carrier {
382        self.message_type().carrier()
383    }
384
385    /// Every document code in catalogue order.
386    pub fn all() -> impl Iterator<Item = Self> {
387        CATALOGUE.iter().map(|(_, d, _)| *d)
388    }
389
390    /// The document-name code the Anwendungsfall column of `pid` admits in
391    /// `BGM` DE 1001 — every published column marks exactly one.
392    ///
393    /// Source: ALOCAT 5.11a §4, NOMINT 4.6 §4, NOMRES 4.7 §4, SSQNOT 5.7 §4.
394    /// `None` for a code no shipped column publishes.
395    #[must_use]
396    pub fn for_pid(pid: u32) -> Option<Self> {
397        use DvgwDocument as D;
398        Some(match pid {
399            70001 | 70008 | 70013 | 70018 | 70022 => D::AllokationSlp,
400            70002 | 70011 | 70023 => D::KorrigierteMengenmeldungNkp,
401            70003 | 70012 => D::TaeglicheMengenmeldungNkp,
402            70004 | 70014 => D::UntertaegigeAllokation,
403            70005 | 70015 => D::EndgueltigeAllokation,
404            70006 | 70009 | 70016 | 70019 => D::KorrigierteAllokationBilanzierungsbrennwert,
405            70007 | 70010 | 70017 | 70020 => D::KorrigierteAllokationAbrechnungsbrennwert,
406            70021 => D::SlpErsatzwerte,
407            70030 => D::NominierungTransportkunde,
408            70031 => D::NominierungVirtuellerHandelspunkt,
409            70032 => D::Flexibilitaetsuebertragung,
410            70033 => D::NominierungGebuendelteKapazitaet,
411            70034 => D::NominierungsweitergabeNetzbetreiber,
412            70035 => D::MatchingBenachrichtigung,
413            70036 => D::Bestaetigung,
414            70037 => D::VhpMatchingBenachrichtigung,
415            70038 => D::VhpBestaetigung,
416            70039 => D::BestaetigungFlexibilitaetsuebertragung,
417            70095 | 70096 => D::MehrMindermengenmeldung,
418            _ => return None,
419        })
420    }
421}
422
423/// The code-list responsible agency DVGW stamps on every coded value
424/// (`DE 3055` = `332`).
425pub const DVGW_AGENCY_CODE: &str = "332";
426
427impl fmt::Display for DvgwDocument {
428    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
429        f.write_str(self.code())
430    }
431}
432
433#[cfg(test)]
434mod tests {
435    use super::*;
436
437    #[test]
438    fn every_document_round_trips_through_its_code() {
439        for doc in DvgwDocument::all() {
440            assert!(!doc.code().is_empty(), "{doc:?} has no wire code");
441            assert!(!doc.description().is_empty(), "{doc:?} has no description");
442            assert_eq!(DvgwDocument::from_code(doc.code()), Some(doc));
443        }
444    }
445
446    #[test]
447    fn the_catalogue_has_no_duplicate_codes() {
448        let mut codes: Vec<&str> = CATALOGUE.iter().map(|(c, _, _)| *c).collect();
449        let total = codes.len();
450        codes.sort_unstable();
451        codes.dedup();
452        assert_eq!(
453            codes.len(),
454            total,
455            "duplicate BGM 1001 code in the catalogue"
456        );
457    }
458
459    #[test]
460    fn message_type_documents_partition_the_catalogue() {
461        let listed: usize = DvgwMessageType::ALL
462            .iter()
463            .map(|m| m.documents().len())
464            .sum();
465        assert_eq!(
466            listed,
467            CATALOGUE.len(),
468            "a document code is unreachable from its family"
469        );
470    }
471
472    /// Every published Anwendungsfall names one document, of its own family.
473    #[test]
474    fn every_catalogued_pid_names_a_document_of_its_family() {
475        for info in crate::pruefidentifikator::catalogue() {
476            let doc = DvgwDocument::for_pid(info.pid)
477                .unwrap_or_else(|| panic!("{} has no BGM code", info.pid));
478            assert_eq!(doc.message_type(), info.message_type, "{}", info.pid);
479        }
480        assert_eq!(DvgwDocument::for_pid(70_500), None);
481    }
482
483    /// The carrier is the cross-check, so it must follow the family exactly.
484    #[test]
485    fn nomint_rides_orders_and_the_rest_ride_ordrsp() {
486        assert_eq!(DvgwMessageType::Nomint.carrier(), Carrier::Orders);
487        assert_eq!(DvgwMessageType::Alocat.carrier(), Carrier::Ordrsp);
488        assert_eq!(DvgwMessageType::Nomres.carrier(), Carrier::Ordrsp);
489        assert_eq!(DvgwMessageType::Ssqnot.carrier(), Carrier::Ordrsp);
490        assert_eq!(
491            DvgwDocument::from_code("BAG"),
492            Some(DvgwDocument::MehrMindermengenmeldung)
493        );
494        assert_eq!(
495            Carrier::from_unh_code("ALOCAT"),
496            None,
497            "ALOCAT is not a wire carrier"
498        );
499    }
500}