Skip to main content

edi_energy/messages/
contrl.rs

1use edifact_rs::{
2    EdifactDeserialize, EdifactSerialize, EventEmitter, OwnedSegment, ProfileRulePack,
3    ValidationIssue, ValidationSeverity,
4};
5
6use crate::{
7    EdiEnergyMessage, EdiEnergyReport, Error, MessageType, Pruefidentifikator, Release,
8    messages::{
9        core::MessageCore,
10        segments::{Ucd, Uci, Ucm, Ucs, find_uci, try_deserialize},
11    },
12};
13
14// ── Segment group types ───────────────────────────────────────────────────────
15
16/// A message-level acknowledgement in CONTRL (SG1 — UCM group).
17///
18/// Each instance acknowledges or rejects one specific message within the
19/// acknowledged interchange.
20#[derive(Debug, Clone)]
21#[non_exhaustive]
22pub struct ContrlMessageResponse {
23    /// UCM — message acknowledgement (`action_code = "4"` = accepted, `"7"` = rejected).
24    pub ucm: Ucm,
25    /// SG2 — per-segment error reports (only present when message is rejected).
26    pub segment_errors: Vec<ContrlSegmentError>,
27}
28
29/// A segment-level error within a rejected message (CONTRL SG2 — UCS group).
30#[derive(Debug, Clone)]
31#[non_exhaustive]
32pub struct ContrlSegmentError {
33    /// UCS — segment position in the erroneous message.
34    pub ucs: Ucs,
35    /// SG3 — per-data-element error details (present when the error is in a DE).
36    pub element_errors: Vec<ContrlElementError>,
37}
38
39/// A data-element error within a rejected segment (CONTRL SG3 — UCD).
40#[derive(Debug, Clone)]
41#[non_exhaustive]
42pub struct ContrlElementError {
43    /// UCD — identifies the faulty data element and error code.
44    pub ucd: Ucd,
45}
46
47// ── ContrlMessage ─────────────────────────────────────────────────────────────
48
49/// CONTRL — Interchange Control Structure (syntax acknowledgement).
50///
51/// Acknowledges receipt and syntactic correctness of an EDIFACT interchange
52/// at the UNB / UNH level.
53///
54/// # Typed access
55///
56/// | Field               | Segment | Meaning                                         |
57/// |---------------------|---------|-------------------------------------------------|
58/// | `uci`               | UCI     | Interchange reference and acknowledgement code  |
59/// | `message_responses` | SG1/UCM | Per-message acknowledgement groups              |
60///
61/// Each [`ContrlMessageResponse`] holds a [`Ucm`] and zero or more
62/// [`ContrlSegmentError`] / [`ContrlElementError`] sub-groups.
63#[derive(Debug, Clone)]
64pub struct ContrlMessage {
65    pub(crate) core: MessageCore,
66    /// UCI — Interchange Control Response (always present in a valid CONTRL).
67    uci: Option<Uci>,
68    /// SG1 — per-message acknowledgement groups (`UCM` + optional `UCS`/`UCD`).
69    ///
70    /// Empty for a simple positive acknowledgement (all messages accepted).
71    message_responses: Vec<ContrlMessageResponse>,
72}
73
74impl ContrlMessage {
75    pub(crate) fn from_parts(
76        segments: Vec<OwnedSegment>,
77        message_ref: impl Into<Box<str>>,
78        assoc_code: impl Into<Box<str>>,
79        pruefidentifikator: Option<u32>,
80    ) -> Self {
81        let (uci, message_responses) = {
82            let borrowed: Vec<edifact_rs::Segment<'_>> =
83                segments.iter().map(|s| s.as_borrowed()).collect();
84            let uci = find_uci(&borrowed);
85            let responses = parse_message_responses(&borrowed);
86            (uci, responses)
87        };
88        Self {
89            core: MessageCore::new(
90                segments,
91                message_ref,
92                assoc_code,
93                pruefidentifikator,
94                MessageType::Contrl,
95            ),
96            uci,
97            message_responses,
98        }
99    }
100
101    /// The EDI@Energy release / association code from UNH (DE 0057).
102    #[must_use]
103    pub fn assoc_code(&self) -> &str {
104        &self.core.assoc_code
105    }
106
107    /// Raw parsed segments (authoritative for validation and serialization).
108    #[must_use]
109    pub fn segments(&self) -> &[OwnedSegment] {
110        &self.core.segments
111    }
112
113    /// UCI — Interchange Control Response.  Returns `None` when absent or malformed.
114    #[must_use]
115    pub fn uci(&self) -> Option<&Uci> {
116        self.uci.as_ref()
117    }
118
119    /// SG1 — per-message acknowledgement groups.
120    #[must_use]
121    pub fn message_responses(&self) -> &[ContrlMessageResponse] {
122        &self.message_responses
123    }
124}
125
126// ── EdifactDeserialize ────────────────────────────────────────────────────────
127
128impl EdifactDeserialize for ContrlMessage {
129    fn edifact_deserialize(
130        segments: &[edifact_rs::Segment<'_>],
131    ) -> Result<Self, edifact_rs::EdifactError> {
132        let (message_ref, assoc_code) = MessageCore::extract_unh_fields(segments)?;
133        let owned: Vec<OwnedSegment> = segments.iter().cloned().map(OwnedSegment::from).collect();
134        Ok(Self::from_parts(owned, message_ref, assoc_code, None))
135    }
136}
137
138// ── EdifactSerialize ──────────────────────────────────────────────────────────
139
140impl EdifactSerialize for ContrlMessage {
141    fn edifact_serialize<E: EventEmitter>(
142        &self,
143        emitter: &mut E,
144    ) -> Result<(), edifact_rs::EdifactError> {
145        self.core.emit_segments(emitter)
146    }
147}
148impl EdiEnergyMessage for ContrlMessage {
149    fn try_message_type(&self) -> Option<MessageType> {
150        Some(self.core.message_type())
151    }
152    fn detect_release(&self) -> Result<&Release, Error> {
153        self.core.detect_release()
154    }
155    fn message_ref(&self) -> &str {
156        &self.core.message_ref
157    }
158    /// CONTRL is the EDIFACT syntax acknowledgement message.
159    ///
160    /// It does **not** use Pruefidentifikatoren — always returns
161    /// [`Error::MissingPruefidentifikator`].
162    fn detect_pruefidentifikator(&self) -> Result<Pruefidentifikator, Error> {
163        Err(Error::MissingPruefidentifikator)
164    }
165    fn validate(&self) -> Result<EdiEnergyReport, Error> {
166        let release = self.core.detect_release()?;
167        self.core
168            .validate_against_with_semantic(release, Some(contrl_semantic_pack()))
169    }
170    fn validate_against(&self, release: &Release) -> Result<EdiEnergyReport, Error> {
171        self.core
172            .validate_against_with_semantic(release, Some(contrl_semantic_pack()))
173    }
174    fn serialize(&self) -> Result<Vec<u8>, Error> {
175        self.core.serialize()
176    }
177    fn segments(&self) -> &[edifact_rs::OwnedSegment] {
178        &self.core.segments
179    }
180    fn validate_with_pack(&self, extra: crate::CustomRulePack) -> Result<EdiEnergyReport, Error> {
181        self.core.validate_with_extra_pack(None, extra.into_inner())
182    }
183    fn validate_on_date(&self, reference_date: time::Date) -> Result<EdiEnergyReport, Error> {
184        let release = self.core.detect_release()?;
185        self.core
186            .validate_against_with_semantic_and_registry_on_date(
187                release,
188                Some(contrl_semantic_pack()),
189                crate::registry::ReleaseRegistry::global(),
190                Some(reference_date),
191            )
192    }
193}
194
195// ── segment group parsers ─────────────────────────────────────────────────────
196
197/// Parse all SG1 (UCM) groups from the segment list.
198///
199/// Each `UCM` segment triggers a new [`ContrlMessageResponse`].  Within it,
200/// `UCS` segments trigger [`ContrlSegmentError`] sub-groups (SG2), and each
201/// `UCD` after a `UCS` is wrapped in a [`ContrlElementError`] (SG3).
202fn parse_message_responses(segments: &[edifact_rs::Segment<'_>]) -> Vec<ContrlMessageResponse> {
203    let mut result = Vec::new();
204    let mut i = 0;
205
206    while i < segments.len() {
207        if segments[i].tag != "UCM" {
208            i += 1;
209            continue;
210        }
211        let Some(ucm) = try_deserialize::<Ucm>(&segments[i]) else {
212            i += 1;
213            continue;
214        };
215
216        // Collect SG2 groups until the next UCM or UNT.
217        let mut segment_errors = Vec::new();
218        let mut j = i + 1;
219        while j < segments.len() && segments[j].tag != "UCM" && segments[j].tag != "UNT" {
220            if segments[j].tag != "UCS" {
221                j += 1;
222                continue;
223            }
224            let Some(ucs_seg) = try_deserialize::<Ucs>(&segments[j]) else {
225                j += 1;
226                continue;
227            };
228
229            // Collect SG3 (UCD) until the next UCS, UCM, or UNT.
230            let mut element_errors = Vec::new();
231            let mut k = j + 1;
232            while k < segments.len() && segments[k].tag == "UCD" {
233                if let Some(ucd) = try_deserialize::<Ucd>(&segments[k]) {
234                    element_errors.push(ContrlElementError { ucd });
235                }
236                k += 1;
237            }
238            segment_errors.push(ContrlSegmentError {
239                ucs: ucs_seg,
240                element_errors,
241            });
242            j = k;
243        }
244
245        result.push(ContrlMessageResponse {
246            ucm,
247            segment_errors,
248        });
249        i = j;
250    }
251
252    result
253}
254
255// ── Layer 5: CONTRL semantic rule pack ───────────────────────────────────────
256
257/// UN/EDIFACT CONTRL acknowledgement codes (DE 0083).
258///
259/// - `4`: acknowledged (interchange accepted)
260/// - `7`: interchange rejected — at least one functional group rejected
261/// - `8`: interchange rejected — the entire interchange is rejected
262const VALID_UCI_CODES: &[&str] = &["4", "7", "8"];
263
264/// Build the CONTRL semantic rule pack (Layer 5).
265///
266/// Rules:
267/// - [`rule_sem_contrl_syntax_code`]: the `UCI` acknowledgement code (DE 0083)
268///   must be one of the UN/EDIFACT defined values `4`, `7`, or `8`.
269fn contrl_semantic_pack() -> ProfileRulePack {
270    ProfileRulePack::new("CONTRL-SEM")
271        .for_message_type("CONTRL")
272        .with_stateless_rule_fn(rule_sem_contrl_syntax_code)
273}
274
275/// `SEM-CONTRL-SYNTAX-CODE-UNKNOWN` — Validate the `UCI` acknowledgement code
276/// (DE 0083).
277///
278/// UN/EDIFACT defines exactly three valid values:
279/// - `4` — acknowledged (interchange accepted)
280/// - `7` — interchange rejected (group-level)
281/// - `8` — interchange rejected (interchange-level)
282///
283/// Any other value indicates a malformed CONTRL message.
284fn rule_sem_contrl_syntax_code(
285    segments: &[edifact_rs::Segment<'_>],
286    issues: &mut Vec<ValidationIssue>,
287) {
288    for seg in segments.iter().filter(|s| s.tag == "UCI") {
289        // UCI: element[3] = DE 0083 (acknowledgement code)
290        let code = seg.element_str(3).unwrap_or("");
291        if code.is_empty() {
292            continue;
293        }
294        if !VALID_UCI_CODES.contains(&code) {
295            issues.push(
296                ValidationIssue::new(
297                    ValidationSeverity::Error,
298                    format!(
299                        "UCI acknowledgement code '{code}' is not in the UN/EDIFACT \
300                         defined set (4=accepted, 7=rejected-group, 8=rejected-interchange)"
301                    ),
302                )
303                .with_span(seg.span)
304                .with_rule_id("SEM-CONTRL-SYNTAX-CODE-UNKNOWN")
305                .with_segment("UCI")
306                .with_suggestion(
307                    "Valid UN/EDIFACT UCI acknowledgement codes (DE 0083): \
308                     4 = interchange received, \
309                     7 = rejected at functional-group level, \
310                     8 = rejected at interchange level",
311                ),
312            );
313        }
314    }
315}