Skip to main content

edi_energy/messages/
segments.rs

1//! Typed EDIFACT segment structs shared across all EDI@Energy message types.
2//!
3//! Each struct maps directly to one EDIFACT segment via the `#[edifact(segment = "TAG")]`
4//! derive attribute and implements both [`EdifactDeserialize`] and [`EdifactSerialize`].
5//!
6//! # Element / component indexing
7//!
8//! EDIFACT uses 0-based indices:
9//! - `#[edifact(element = N)]` — selects component 0 of the Nth element.
10//! - `#[edifact(element = N, component = M)]` — selects component M of the Nth element.
11//!
12//! For composite data elements (e.g. C507 in DTM) you need multiple fields
13//! sharing the same `element = N` but with different `component` indices.
14//!
15//! [`EdifactDeserialize`]: edifact_rs::EdifactDeserialize
16//! [`EdifactSerialize`]: edifact_rs::EdifactSerialize
17
18use edifact_rs::{EdifactDeserialize, EdifactSerialize};
19
20// ── BGM ───────────────────────────────────────────────────────────────────────
21
22/// `BGM` — Beginning of Message.
23///
24/// Structure: `BGM+<document_code>+<document_id>+<function>'`
25///
26/// | Element | DE   | Meaning                                                  |
27/// |---------|------|----------------------------------------------------------|
28/// | 0       | 1001 | Document / message name code (e.g. `E01`, `1000`)        |
29/// | 1       | 1004 | Document / message number (Pruefidentifikator value)     |
30/// | 2       | 1225 | Message function, coded (e.g. `9` = original)            |
31#[derive(Debug, Clone, PartialEq, Eq, EdifactDeserialize, EdifactSerialize)]
32#[edifact(segment = "BGM")]
33pub struct Bgm {
34    /// DE 1001 — document / message name code.  In EDI@Energy this is the
35    /// message category identifier (e.g. `"E01"` for grid-feed-in UTILMD,
36    /// `"1000"` for a positive APERAK).
37    #[edifact(element = 0)]
38    pub document_code: String,
39    /// DE 1004 — document / message number.  Carries the Pruefidentifikator
40    /// value (e.g. `"11001"`) or an 8-digit document reference number.
41    #[edifact(element = 1)]
42    pub document_id: Option<String>,
43    /// DE 1225 — message function, coded (e.g. `"9"` = original).
44    #[edifact(element = 2)]
45    pub function: Option<String>,
46}
47
48impl Bgm {
49    /// Parse `document_id` as a [`crate::Pruefidentifikator`].
50    ///
51    /// Returns `None` when the field is absent or the value is not a valid
52    /// 5-digit Pruefidentifikator.
53    #[must_use]
54    pub fn pruefidentifikator(&self) -> Option<crate::Pruefidentifikator> {
55        self.document_id
56            .as_deref()
57            .and_then(|s| s.parse::<u32>().ok())
58            .and_then(|code| crate::Pruefidentifikator::new(code).ok())
59    }
60}
61
62// ── DTM ───────────────────────────────────────────────────────────────────────
63
64/// `DTM` — Date/Time/Period.
65///
66/// Structure: `DTM+<qualifier>:<value>:<format>'`  (C507 composite in element 0)
67///
68/// | Element | Component | DE   | Meaning                                 |
69/// |---------|-----------|------|-----------------------------------------|
70/// | 0       | 0         | 2005 | Date/time/period function qualifier     |
71/// | 0       | 1         | 2380 | Date/time/period text value             |
72/// | 0       | 2         | 2379 | Date/time/period format qualifier       |
73#[derive(Debug, Clone, PartialEq, Eq, EdifactDeserialize, EdifactSerialize)]
74#[edifact(segment = "DTM")]
75pub struct Dtm {
76    /// DE 2005 — date/time/period qualifier (e.g. `"137"` = document date,
77    /// `"163"` = beginning of delivery period).
78    #[edifact(element = 0)]
79    pub qualifier: String,
80    /// DE 2380 — date/time/period value (e.g. `"20230101"`).
81    #[edifact(element = 0, component = 1)]
82    pub value: Option<String>,
83    /// DE 2379 — date/time/period format qualifier (e.g. `"102"` = CCYYMMDD).
84    #[edifact(element = 0, component = 2)]
85    pub format: Option<String>,
86}
87
88impl Dtm {
89    /// Returns `true` when this is the document date / creation timestamp
90    /// (`qualifier == "137"`).
91    #[must_use]
92    pub fn is_document_date(&self) -> bool {
93        self.qualifier == "137"
94    }
95
96    /// Returns `true` when this marks the beginning of a supply/delivery period
97    /// (`qualifier == "163"`).
98    #[must_use]
99    pub fn is_period_start(&self) -> bool {
100        self.qualifier == "163"
101    }
102
103    /// Returns `true` when this marks the end of a supply/delivery period
104    /// (`qualifier == "164"`).
105    #[must_use]
106    pub fn is_period_end(&self) -> bool {
107        self.qualifier == "164"
108    }
109
110    /// Returns the date/time value as a `&str`, if present.
111    #[must_use]
112    pub fn value_str(&self) -> Option<&str> {
113        self.value.as_deref()
114    }
115}
116
117// ── NAD ───────────────────────────────────────────────────────────────────────
118
119/// `NAD` — Name and Address, qualified by element 0 (DE 3035).
120///
121/// Structure: `NAD+<qualifier>+<party_id>::<agency>'`
122///
123/// | Element | Component | DE   | Meaning                              |
124/// |---------|-----------|------|--------------------------------------|
125/// | 0       | 0         | 3035 | Party function qualifier             |
126/// | 1       | 0         | 3039 | Party identification                 |
127/// | 1       | 2         | 3055 | Code list responsible agency         |
128/// | 3       | 0         | 3036 | Party name                           |
129///
130/// Common qualifiers:
131/// - `"MS"` — message sender (DE 3035)
132/// - `"MR"` — message recipient (DE 3035)
133/// - `"AG"` — authorised/requesting agent
134#[derive(Debug, Clone, PartialEq, Eq, EdifactDeserialize, EdifactSerialize)]
135#[edifact(segment = "NAD", qualifier_from = 0)]
136pub struct Nad {
137    /// DE 3035 — party function qualifier (e.g. `"MS"` = message sender).
138    #[edifact(element = 0)]
139    pub qualifier: String,
140    /// DE 3039 — party identification code (GLN / BDEW code), component 0 of C082.
141    #[edifact(element = 1)]
142    pub party_id: Option<String>,
143    /// DE 3055 — code list responsible agency, component 2 of C082.
144    ///
145    /// Common values in EDI@Energy:
146    /// - `"293"` — BDEW (the standard for German `MaKo` market participants)
147    /// - `"9"` — GS1/EAN (global GLN scheme, rare in German `MaKo`)
148    /// - `"305"` — ECOD/ENTSO-E (EIC codes for TSOs and Regelzonen)
149    /// - `"332"` — DVGW (legacy gas-sector codes)
150    ///
151    /// Use [`crate::AgencyCode`] to parse or format this value.
152    #[edifact(element = 1, component = 2)]
153    pub agency_code: Option<String>,
154    /// DE 3036 — party name, component 0 of C080.
155    #[edifact(element = 3)]
156    pub party_name: Option<String>,
157}
158
159// ── RFF ───────────────────────────────────────────────────────────────────────
160
161/// `RFF` — Reference.
162///
163/// Structure: `RFF+<qualifier>:<reference>'`  (C506 composite in element 0)
164///
165/// | Element | Component | DE   | Meaning                             |
166/// |---------|-----------|------|-------------------------------------|
167/// | 0       | 0         | 1153 | Reference function qualifier        |
168/// | 0       | 1         | 1154 | Reference identifier                |
169///
170/// Common qualifiers in EDI@Energy:
171/// - `"ACW"` — acknowledgement reference (APERAK)
172/// - `"TN"` — transaction reference number
173/// - `"Z13"` — Pruefidentifikator of referenced message
174#[derive(Debug, Clone, PartialEq, Eq, EdifactDeserialize, EdifactSerialize)]
175#[edifact(segment = "RFF")]
176pub struct Rff {
177    /// DE 1153 — reference function qualifier (e.g. `"ACW"`, `"TN"`, `"Z13"`).
178    #[edifact(element = 0)]
179    pub qualifier: String,
180    /// DE 1154 — reference identifier value.
181    #[edifact(element = 0, component = 1)]
182    pub reference: Option<String>,
183}
184
185// ── IDE ───────────────────────────────────────────────────────────────────────
186
187/// `IDE` — Identity.
188///
189/// Used in UTILMD to carry market location IDs (Marktlokation / Messlokation).
190///
191/// | Element | Component | DE   | Meaning                             |
192/// |---------|-----------|------|-------------------------------------|
193/// | 0       | 0         | 7495 | Object type qualifier               |
194/// | 1       | 0         | 7402 | Identity number (MaLo / MeLo ID)    |
195#[derive(Debug, Clone, PartialEq, Eq, EdifactDeserialize, EdifactSerialize)]
196#[edifact(segment = "IDE")]
197pub struct Ide {
198    /// DE 7495 — object type qualifier (e.g. `"Z18"` = Marktlokation,
199    /// `"Z19"` = Messlokation).
200    #[edifact(element = 0)]
201    pub qualifier: String,
202    /// DE 7402 — object identity number (component 0 of C206).
203    /// Must be exactly 11 upper-case alphanumeric characters for EDI@Energy.
204    #[edifact(element = 1)]
205    pub object_id: Option<String>,
206}
207
208// ── LOC ───────────────────────────────────────────────────────────────────────
209
210/// `LOC` — Place/Location Identification.
211///
212/// | Element | Component | DE   | Meaning                             |
213/// |---------|-----------|------|-------------------------------------|
214/// | 0       | 0         | 3227 | Location function qualifier         |
215/// | 1       | 0         | 3225 | Location name code                  |
216#[derive(Debug, Clone, PartialEq, Eq, EdifactDeserialize, EdifactSerialize)]
217#[edifact(segment = "LOC")]
218pub struct Loc {
219    /// DE 3227 — location function qualifier (e.g. `"172"` = measurement point).
220    #[edifact(element = 0)]
221    pub qualifier: String,
222    /// DE 3225 — location name code / identifier, component 0 of C517.
223    #[edifact(element = 1)]
224    pub location_id: Option<String>,
225}
226
227// ── ERC ───────────────────────────────────────────────────────────────────────
228
229/// `ERC` — Application Error Information.
230///
231/// Used in APERAK to carry application-level error codes.
232///
233/// | Element | Component | DE   | Meaning                             |
234/// |---------|-----------|------|-------------------------------------|
235/// | 0       | 0         | 9321 | Application error code              |
236#[derive(Debug, Clone, PartialEq, Eq, EdifactDeserialize, EdifactSerialize)]
237#[edifact(segment = "ERC")]
238pub struct Erc {
239    /// DE 9321 — application error code (component 0 of C901).
240    #[edifact(element = 0)]
241    pub error_code: String,
242    /// DE 1131 — code list identification (component 1 of C901), if present.
243    #[edifact(element = 0, component = 1)]
244    pub code_list_id: Option<String>,
245    /// DE 3055 — responsible agency code (component 2 of C901), if present.
246    #[edifact(element = 0, component = 2)]
247    pub agency_code: Option<String>,
248}
249
250// ── FTX ───────────────────────────────────────────────────────────────────────
251
252/// `FTX` — Free Text.
253///
254/// Used for human-readable notes and, in APERAK, for error descriptions.
255///
256/// | Element | Component | DE   | Meaning                             |
257/// |---------|-----------|------|-------------------------------------|
258/// | 0       | 0         | 4451 | Text subject qualifier              |
259/// | 1       | 0         | 4453 | Text function, coded                |
260/// | 3       | 0         | 4440 | Free text (first line)              |
261#[derive(Debug, Clone, PartialEq, Eq, EdifactDeserialize, EdifactSerialize)]
262#[edifact(segment = "FTX")]
263pub struct Ftx {
264    /// DE 4451 — text subject qualifier (e.g. `"AAI"` = general information,
265    /// `"AIM"` = APERAK error text, `"ZZZ"` = mutually defined).
266    #[edifact(element = 0)]
267    pub qualifier: String,
268    /// DE 4440 — free text, component 0 of C108 (element 3).
269    #[edifact(element = 3)]
270    pub text: Option<String>,
271}
272
273// ── QTY ───────────────────────────────────────────────────────────────────────
274
275/// `QTY` — Quantity.
276///
277/// Used in MSCONS to carry metered quantity values.
278///
279/// | Element | Component | DE   | Meaning                             |
280/// |---------|-----------|------|-------------------------------------|
281/// | 0       | 0         | 6063 | Quantity type code qualifier        |
282/// | 0       | 1         | 6060 | Quantity value                      |
283/// | 0       | 2         | 6411 | Measurement unit code               |
284#[derive(Debug, Clone, PartialEq, Eq, EdifactDeserialize, EdifactSerialize)]
285#[edifact(segment = "QTY")]
286pub struct Qty {
287    /// DE 6063 — quantity type code qualifier (e.g. `"220"` = metered quantity).
288    #[edifact(element = 0)]
289    pub qualifier: String,
290    /// DE 6060 — quantity value as string (e.g. `"1234.5"`).
291    #[edifact(element = 0, component = 1)]
292    pub value: Option<String>,
293    /// DE 6411 — measurement unit code (e.g. `"KWH"`, `"MWH"`, `"M3"`).
294    #[edifact(element = 0, component = 2)]
295    pub unit: Option<String>,
296}
297
298impl Qty {
299    /// Parse `value` as an `f64`.
300    ///
301    /// Returns `None` when the field is absent or the string is not a valid
302    /// decimal number.  The EDIFACT decimal mark may be either `.` or `,`.
303    #[must_use]
304    pub fn value_f64(&self) -> Option<f64> {
305        self.value
306            .as_deref()
307            .map(|s| s.replace(',', "."))
308            .and_then(|s| s.parse::<f64>().ok())
309    }
310
311    /// Returns `true` when this is a metered quantity (`qualifier == "220"`).
312    #[must_use]
313    pub fn is_metered(&self) -> bool {
314        self.qualifier == "220"
315    }
316}
317
318// ── UCI ───────────────────────────────────────────────────────────────────────
319
320/// `UCI` — Interchange Response.
321///
322/// The mandatory segment in CONTRL messages.  Carries the interchange
323/// control reference and the acknowledgement action code.
324///
325/// | Element | Component | DE   | Meaning                              |
326/// |---------|-----------|------|--------------------------------------|
327/// | 0       | 0         | 0020 | Interchange control reference        |
328/// | 1       | 0         | 0004 | Sender identification                |
329/// | 2       | 0         | 0010 | Recipient identification             |
330/// | 3       | 0         | 0083 | Action, coded (4/7/8)                |
331#[derive(Debug, Clone, PartialEq, Eq, EdifactDeserialize, EdifactSerialize)]
332#[edifact(segment = "UCI")]
333pub struct Uci {
334    /// DE 0020 — interchange control reference.
335    #[edifact(element = 0)]
336    pub interchange_ref: String,
337    /// DE 0004 — sender identification (S002 component 0).
338    #[edifact(element = 1)]
339    pub sender: Option<String>,
340    /// DE 0010 — recipient identification (S003 component 0).
341    #[edifact(element = 2)]
342    pub recipient: Option<String>,
343    /// DE 0083 — action, coded: `"4"` = acknowledged, `"7"` = rejected (group),
344    /// `"8"` = rejected (interchange).
345    #[edifact(element = 3)]
346    pub action_code: Option<String>,
347}
348
349// ── UNH ───────────────────────────────────────────────────────────────────────
350
351/// `UNH` — Message Header.
352///
353/// | Element | Component | DE   | Meaning                              |
354/// |---------|-----------|------|--------------------------------------|
355/// | 0       | 0         | 0062 | Message reference number             |
356/// | 1       | 0         | 0065 | Message type identifier              |
357/// | 1       | 4         | 0057 | Association assigned code (release)  |
358#[derive(Debug, Clone, PartialEq, Eq, EdifactDeserialize, EdifactSerialize)]
359#[edifact(segment = "UNH")]
360pub struct Unh {
361    /// DE 0062 — message reference number.
362    #[edifact(element = 0)]
363    pub message_ref: String,
364    /// DE 0065 — message type identifier (e.g. `"UTILMD"`, `"MSCONS"`).
365    #[edifact(element = 1)]
366    pub message_type: String,
367    /// DE 0057 — association assigned code, component 4 of S009
368    /// (e.g. `"5.5.3a"`).  This is the EDI@Energy release identifier.
369    #[edifact(element = 1, component = 4)]
370    pub assoc_code: Option<String>,
371}
372
373// ── UNT ───────────────────────────────────────────────────────────────────────
374
375/// `UNT` — Message Trailer.
376///
377/// | Element | Component | DE   | Meaning                              |
378/// |---------|-----------|------|--------------------------------------|
379/// | 0       | 0         | 0074 | Number of segments in message        |
380/// | 1       | 0         | 0062 | Message reference number             |
381#[derive(Debug, Clone, PartialEq, Eq, EdifactDeserialize, EdifactSerialize)]
382#[edifact(segment = "UNT")]
383pub struct Unt {
384    /// DE 0074 — number of segments in the message (including UNH and UNT).
385    #[edifact(element = 0)]
386    pub segment_count: String,
387    /// DE 0062 — message reference number (must match UNH element 0).
388    #[edifact(element = 1)]
389    pub message_ref: String,
390}
391
392// ── helpers ───────────────────────────────────────────────────────────────────
393
394/// Find the first `NAD` segment with the given `qualifier` in a segment slice.
395///
396/// Used by message constructors to extract sender (`"MS"`) and receiver (`"MR"`).
397pub(crate) fn find_nad(segments: &[edifact_rs::Segment<'_>], qualifier: &str) -> Option<Nad> {
398    segments
399        .iter()
400        .filter(|s| s.tag == "NAD")
401        .find(|s| s.element_str(0) == Some(qualifier))
402        .and_then(|seg| Nad::edifact_deserialize(std::slice::from_ref(seg)).ok())
403}
404
405/// Find all `DTM` segments in a segment slice.
406pub(crate) fn collect_dtm(segments: &[edifact_rs::Segment<'_>]) -> Vec<Dtm> {
407    segments
408        .iter()
409        .filter(|s| s.tag == "DTM")
410        .filter_map(|seg| Dtm::edifact_deserialize(std::slice::from_ref(seg)).ok())
411        .collect()
412}
413
414/// Find the first `BGM` segment.
415pub(crate) fn find_bgm(segments: &[edifact_rs::Segment<'_>]) -> Option<Bgm> {
416    segments
417        .iter()
418        .filter(|s| s.tag == "BGM")
419        .find_map(|seg| Bgm::edifact_deserialize(std::slice::from_ref(seg)).ok())
420}
421
422/// Find the first `UCI` segment.
423pub(crate) fn find_uci(segments: &[edifact_rs::Segment<'_>]) -> Option<Uci> {
424    segments
425        .iter()
426        .filter(|s| s.tag == "UCI")
427        .find_map(|seg| Uci::edifact_deserialize(std::slice::from_ref(seg)).ok())
428}
429
430/// Find the first `RFF` segment with the given qualifier.
431pub(crate) fn find_rff(segments: &[edifact_rs::Segment<'_>], qualifier: &str) -> Option<Rff> {
432    segments
433        .iter()
434        .filter(|s| s.tag == "RFF")
435        .find(|s| s.element_str(0) == Some(qualifier))
436        .and_then(|seg| Rff::edifact_deserialize(std::slice::from_ref(seg)).ok())
437}
438
439/// Collect all `COM` segments from a segment slice.
440///
441/// Used by `PartinMessage` to extract communication channels
442/// (AS4 endpoint, email, phone) declared by the described party.
443#[cfg(feature = "partin")]
444pub(crate) fn collect_com(segments: &[edifact_rs::Segment<'_>]) -> Vec<Com> {
445    segments
446        .iter()
447        .filter(|s| s.tag == "COM")
448        .filter_map(|seg| Com::edifact_deserialize(std::slice::from_ref(seg)).ok())
449        .collect()
450}
451
452// ── UCM ───────────────────────────────────────────────────────────────────────
453
454/// `UCM` — Message Response (CONTRL SG1).
455///
456/// Acknowledges or rejects one specific message within the interchange.
457///
458/// | Element | Component | DE   | Meaning                              |
459/// |---------|-----------|------|--------------------------------------|
460/// | 0       | 0         | 0062 | Message reference number             |
461/// | 1       | 0         | 0065 | Message type identifier              |
462/// | 1       | 4         | 0057 | Association assigned code            |
463/// | 2       | 0         | 0083 | Action, coded (`"4"` = acknowledged, `"7"` = rejected) |
464/// | 3       | 0         | 0085 | Syntax error code (if rejected)      |
465#[derive(Debug, Clone, PartialEq, Eq, EdifactDeserialize, EdifactSerialize)]
466#[edifact(segment = "UCM")]
467pub struct Ucm {
468    /// DE 0062 — message reference number (matches UNH element 0).
469    #[edifact(element = 0)]
470    pub message_ref: String,
471    /// DE 0065 — message type identifier (e.g. `"UTILMD"`).
472    #[edifact(element = 1)]
473    pub message_type: String,
474    /// DE 0057 — association assigned code (component 4 of S009).
475    #[edifact(element = 1, component = 4)]
476    pub assoc_code: Option<String>,
477    /// DE 0083 — action, coded: `"4"` = acknowledged, `"7"` = rejected.
478    #[edifact(element = 2)]
479    pub action_code: String,
480    /// DE 0085 — syntax error code (component 0 of S011), present when rejected.
481    #[edifact(element = 3)]
482    pub syntax_error: Option<String>,
483}
484
485// ── UCS ───────────────────────────────────────────────────────────────────────
486
487/// `UCS` — Segment Identification (CONTRL SG2).
488///
489/// Identifies the erroneous segment within a message.
490///
491/// | Element | Component | DE   | Meaning                             |
492/// |---------|-----------|------|-------------------------------------|
493/// | 0       | 0         | 0096 | Segment position in message body    |
494/// | 0       | 1         | 0135 | Service segment tag (if applicable) |
495/// | 1       | 0         | 0085 | Syntax error code                   |
496#[derive(Debug, Clone, PartialEq, Eq, EdifactDeserialize, EdifactSerialize)]
497#[edifact(segment = "UCS")]
498pub struct Ucs {
499    /// DE 0096 — segment position in message body (component 0 of S011).
500    #[edifact(element = 0)]
501    pub segment_position: String,
502    /// DE 0135 — service segment tag (component 1 of S011).
503    #[edifact(element = 0, component = 1)]
504    pub segment_tag: Option<String>,
505    /// DE 0085 — syntax error, coded.
506    #[edifact(element = 1)]
507    pub error_code: Option<String>,
508}
509
510// ── UCD ───────────────────────────────────────────────────────────────────────
511
512/// `UCD` — Data Element Error Identification (CONTRL SG3).
513///
514/// | Element | Component | DE   | Meaning                             |
515/// |---------|-----------|------|-------------------------------------|
516/// | 0       | 0         | 0085 | Syntax error code                   |
517/// | 1       | 0         | 0098 | Data element position               |
518/// | 1       | 1         | 0104 | Component position (optional)       |
519#[derive(Debug, Clone, PartialEq, Eq, EdifactDeserialize, EdifactSerialize)]
520#[edifact(segment = "UCD")]
521pub struct Ucd {
522    /// DE 0085 — syntax error, coded.
523    #[edifact(element = 0)]
524    pub error_code: String,
525    /// DE 0098 — data element position (component 0 of C085).
526    #[edifact(element = 1)]
527    pub element_position: Option<String>,
528    /// DE 0104 — component data element position (component 1 of C085).
529    #[edifact(element = 1, component = 1)]
530    pub component_position: Option<String>,
531}
532
533// ── LIN ───────────────────────────────────────────────────────────────────────
534
535/// `LIN` — Line Item (MSCONS SG9).
536///
537/// Marks the start of a new metered-quantity line item.
538///
539/// | Element | Component | DE   | Meaning                             |
540/// |---------|-----------|------|-------------------------------------|
541/// | 0       | 0         | 1082 | Line item number (sequential)       |
542#[derive(Debug, Clone, PartialEq, Eq, EdifactDeserialize, EdifactSerialize)]
543#[edifact(segment = "LIN")]
544pub struct Lin {
545    /// DE 1082 — line item number.
546    #[edifact(element = 0)]
547    pub line_number: Option<String>,
548}
549
550// ── PIA ───────────────────────────────────────────────────────────────────────
551
552/// `PIA` — Additional Product ID (MSCONS SG9).
553///
554/// Carries the OBIS code or metering location identifier.
555///
556/// `PIA` — Additional product ID (MSCONS SG8).
557///
558/// In MSCONS, carries the OBIS measurement identifier for a line item.
559///
560/// `PIA` — Additional Product ID (MSCONS SG9, item identification).
561///
562/// Carries the OBIS measurement identifier for the current line item.
563///
564/// | Element | Component | DE   | Meaning                             |
565/// |---------|-----------|------|-------------------------------------|
566/// | 0       | —         | 4347 | Product ID function qualifier       |
567/// | 1       | 0         | 7140 | OBIS code (full, e.g. `1-0:1.8.0`) |
568/// | 1       | 1         | 7143 | Item type code (`Z12`, `SRW`, …)   |
569///
570/// ## EDIFACT release characters and OBIS codes
571///
572/// OBIS codes (IEC 62056-61) use `:` as part of their notation, which is also
573/// the EDIFACT composite component separator.  The BDEW MSCONS AHB (since
574/// FV2025-10-01) mandates the **EDIFACT release character `?`** to escape each
575/// `:` inside the OBIS value:
576///
577/// ```text
578/// PIA+5+1-1?:1.9.1:SRW'
579///          ^^         ^^ release-char escapes the OBIS colon
580///                     ^^ unescaped colon → component separator → DE 7143 = "SRW"
581/// ```
582///
583/// `edifact-rs` correctly processes release characters: after parsing,
584/// `item_number` contains the full clean OBIS string (`"1-1:1.9.1"`)
585/// and `item_type` contains the DE 7143 qualifier (`"SRW"` or `"Z12"`).
586///
587/// **Builder note:** write the PIA composite through
588/// `Writer::write_composites` (the builders' `emit_comp!`), where the OBIS is
589/// one component and its colons are escaped structurally — never pre-join the
590/// composite with `:` and hand it to `write_raw`.
591#[derive(Debug, Clone, PartialEq, Eq, EdifactDeserialize, EdifactSerialize)]
592#[edifact(segment = "PIA")]
593pub struct Pia {
594    /// DE 4347 — product ID function qualifier (e.g. `"5"` = product ID).
595    #[edifact(element = 0)]
596    pub qualifier: String,
597    /// DE 7140 — full OBIS code (e.g. `"1-0:1.8.0"`).
598    ///
599    /// This field contains the complete OBIS identifier because the BDEW AHB
600    /// requires the `:` inside OBIS values to be escaped with `?:`.
601    /// `edifact-rs` strips the release character during parsing.
602    #[edifact(element = 1)]
603    pub item_number: Option<String>,
604    /// DE 7143 — item type code (e.g. `"Z12"` for OBIS, `"SRW"` for
605    /// Strom-Richtung-Wirkleistung).
606    #[edifact(element = 1, component = 1)]
607    pub item_type: Option<String>,
608}
609
610impl Pia {
611    /// Return the OBIS code from DE 7140.
612    ///
613    /// Convenience accessor for `item_number`.  Returns the full OBIS string
614    /// (e.g. `"1-0:1.8.0"`) after release-character processing by `edifact-rs`.
615    ///
616    /// # Example
617    ///
618    /// ```
619    /// # use edi_energy::messages::segments::Pia;
620    /// let pia = Pia {
621    ///     qualifier: "5".to_owned(),
622    ///     item_number: Some("1-0:1.8.0".to_owned()),
623    ///     item_type: Some("Z12".to_owned()),
624    /// };
625    /// assert_eq!(pia.obis_code().as_deref(), Some("1-0:1.8.0"));
626    /// assert_eq!(pia.item_type.as_deref(), Some("Z12"));
627    /// ```
628    #[must_use]
629    pub fn obis_code(&self) -> Option<&str> {
630        self.item_number.as_deref()
631    }
632}
633
634// ── CCI ───────────────────────────────────────────────────────────────────────
635
636/// `CCI` — Characteristic/Class ID (MSCONS SG8).
637///
638/// In MSCONS, carries the time-series type (Zeitreihentyp).
639///
640/// | Element | Component | DE   | Meaning                              |
641/// |---------|-----------|------|--------------------------------------|
642/// | 0       | 0         | 7059 | Property class code (optional)       |
643/// | 2       | 0         | 7037 | Characteristic ID (Zeitreihentyp)    |
644/// | 2       | 1         | 1131 | Code list identification             |
645/// | 2       | 2         | 3055 | Responsible agency code              |
646#[derive(Debug, Clone, PartialEq, Eq, EdifactDeserialize, EdifactSerialize)]
647#[edifact(segment = "CCI")]
648pub struct Cci {
649    /// DE 7059 — property class code (element 0), usually empty in MSCONS.
650    #[edifact(element = 0)]
651    pub category: Option<String>,
652    /// DE 7037 — characteristic identifier (component 0 of C240, element 2).
653    /// In MSCONS: time-series type code, e.g. `"Z05"` = measured quantity.
654    #[edifact(element = 2)]
655    pub characteristic_id: Option<String>,
656    /// DE 1131 — code list identification (component 1 of C240).
657    #[edifact(element = 2, component = 1)]
658    pub code_list_id: Option<String>,
659    /// DE 3055 — responsible agency code (component 2 of C240).
660    #[edifact(element = 2, component = 2)]
661    pub agency_code: Option<String>,
662}
663
664// ── STS ───────────────────────────────────────────────────────────────────────
665
666/// `STS` — Status (MSCONS SG10).
667///
668/// Carries the quality / validation status of a quantity reading.
669///
670/// | Element | Component | DE   | Meaning                             |
671/// |---------|-----------|------|-------------------------------------|
672/// | 0       | 0         | 9015 | Status category code                |
673/// | 1       | 0         | 9011 | Status value code                   |
674/// | 1       | 1         | 4405 | Status value sub-qualifier          |
675/// | 1       | 2         | 3055 | Agency code for status value        |
676#[derive(Debug, Clone, PartialEq, Eq, EdifactDeserialize, EdifactSerialize)]
677#[edifact(segment = "STS")]
678pub struct Sts {
679    /// DE 9015 — status category (component 0 of C601, e.g. `"7"` = measurement).
680    #[edifact(element = 0)]
681    pub category: Option<String>,
682    /// DE 9011 — status value code (component 0 of C555, e.g. `"Z03"` = validated).
683    #[edifact(element = 1)]
684    pub status_code: Option<String>,
685    /// DE 4405 — status value sub-qualifier (component 1 of C555).
686    ///
687    /// Present in APERAK and CONTRL `STS` segments that carry a two-level qualifier,
688    /// e.g. `STS+E10:ZF3'` where `ZF3` is the sub-qualifier.
689    #[edifact(element = 1, component = 1)]
690    pub sub_qualifier: Option<String>,
691    /// DE 3055 — agency code (component 2 of C555).
692    #[edifact(element = 1, component = 2)]
693    pub agency_code: Option<String>,
694}
695
696// ── UNS ───────────────────────────────────────────────────────────────────────
697
698/// `UNS` — Section Control (MSCONS).
699///
700/// Marks the transition from the header section to the detail section.
701///
702/// | Element | Component | DE   | Meaning                             |
703/// |---------|-----------|------|-------------------------------------|
704/// | 0       | 0         | 0081 | Section identification              |
705#[derive(Debug, Clone, PartialEq, Eq, EdifactDeserialize, EdifactSerialize)]
706#[edifact(segment = "UNS")]
707pub struct Uns {
708    /// DE 0081 — section identification (`"D"` = detail section).
709    #[edifact(element = 0)]
710    pub section_id: String,
711}
712
713// ── CTA ───────────────────────────────────────────────────────────────────────
714
715/// `CTA` — Contact Information (MSCONS SG4).
716///
717/// | Element | Component | DE   | Meaning                             |
718/// |---------|-----------|------|-------------------------------------|
719/// | 0       | 0         | 3139 | Contact function code               |
720/// | 1       | 0         | 3413 | Department / employee name          |
721#[derive(Debug, Clone, PartialEq, Eq, EdifactDeserialize, EdifactSerialize)]
722#[edifact(segment = "CTA")]
723pub struct Cta {
724    /// DE 3139 — contact function, coded.
725    #[edifact(element = 0)]
726    pub function_code: Option<String>,
727    /// DE 3413 — department or employee name (component 0 of C056).
728    #[edifact(element = 1)]
729    pub name: Option<String>,
730}
731
732// ── COM ───────────────────────────────────────────────────────────────────────
733
734/// `COM` — Communication Contact (MSCONS SG4).
735///
736/// | Element | Component | DE   | Meaning                             |
737/// |---------|-----------|------|-------------------------------------|
738/// | 0       | 0         | 3148 | Communication number                |
739/// | 0       | 1         | 3155 | Communication channel qualifier     |
740#[derive(Debug, Clone, PartialEq, Eq, EdifactDeserialize, EdifactSerialize)]
741#[edifact(segment = "COM")]
742pub struct Com {
743    /// DE 3148 — communication number (component 0 of C076).
744    #[edifact(element = 0)]
745    pub number: Option<String>,
746    /// DE 3155 — communication channel qualifier (component 1 of C076,
747    /// e.g. `"EM"` = email, `"TE"` = telephone).
748    #[edifact(element = 0, component = 1)]
749    pub channel: Option<String>,
750}
751
752// ── additional helpers ────────────────────────────────────────────────────────
753
754/// Deserialize a single segment tag from a segment slice, returning `None`
755/// on failure.  Convenience wrapper used by group parsers.
756pub(crate) fn try_deserialize<T: edifact_rs::EdifactDeserialize>(
757    seg: &edifact_rs::Segment<'_>,
758) -> Option<T> {
759    T::edifact_deserialize(std::slice::from_ref(seg)).ok()
760}