CONTRL Acknowledgements

Generate the ISO 9735-4 syntax and service report message from a validation report — receipt, acknowledgement, and rejection with per-segment error placement.

On this page
  1. What CONTRL does and does not say
  2. The two messages
  3. Reporting a failed check
  4. Where a finding lands
    1. Groups
  5. Error-code mapping
  6. Sending it
  7. Parsing an inbound CONTRL
  8. Scope
  9. Further reading

CONTRL is how you tell a partner what happened to their interchange: that it arrived, that it was syntactically accepted, or that it was rejected and exactly where. It is defined by ISO 9735-4, part of the syntax standard rather than of a directory release — so, like the envelope segments, there is one correct answer and no version to choose.

edifact-rs builds one from a ValidationReport, which means the acknowledgement you send and the findings you logged cannot disagree.


What CONTRL does and does not say

ISO 9735-4 §5.2 is explicit: acknowledging an interchange with CONTRL reports on syntax, not on content. A CONTRL saying "acknowledged" means the interchange parsed and its envelope was consistent. It does not mean anyone agreed to the order.

Application-level replies — APERAK, ORDRSP, and the rest — are separate messages and are not this crate's business.


The two messages

§5.3.1 allows at most two CONTRL messages per subject interchange:

Action codeWhenBuilt with
Receipt8Immediately, before checkingContrl::receipt
Acknowledgement7After a clean syntax checkContrl::acknowledgement
Rejection4After a failed syntax checkContrl::from_report

The receipt is optional. The second message is mandatory whenever the subject interchange asked for an acknowledgement in UNB DE 0031 — check interchange.ack_requested() before deciding whether you owe one.

use edifact_rs::{Contrl, from_bytes, validate_envelope};

let raw = b"UNB+UNOC:3+SENDER+RECEIVER+260101:0900+IC4711'\
            UNH+MSG1+ORDERS:D:96A:UN'BGM+220+PO-1+9'UNT+3+MSG1'\
            UNZ+1+IC4711'";
let segments: Vec<_> = from_bytes(raw).collect::<Result<Vec<_>, _>>()?;
let validated = validate_envelope(&segments)?;

let wire = Contrl::acknowledgement(&validated)
    .with_message_reference("ACK1")
    .to_edifact_string()?;

assert_eq!(
    wire,
    "UNH+ACK1+CONTRL:4:1:UN'UCI+IC4711+SENDER+RECEIVER+7'UNT+3+ACK1'",
);
# Ok::<(), edifact_rs::EdifactError>(())

A clean interchange needs no UCM per message: §5.3.4 makes the UCI's acknowledgement cover every message inside it, and a redundant UCM adds nothing.


Reporting a failed check

Contrl::from_report takes the subject interchange, its segments, and the report. The segments are what turn a finding's byte span into the segment position UCS DE 0096 needs, so errors land where they happened instead of piling up on the interchange.

Use the lenient path: it is the only one that yields both an interchange and its faults, which is precisely what a CONTRL reports.

use edifact_rs::{Contrl, ValidationContext, from_bytes, validate_envelope_lenient};

// `FTX+   ` carries a value of nothing but spaces — ISO 9735-1 §9.3.
let raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'\
            UNH+M1+ORDERS:D:96A:UN'FTX+   'UNT+3+M1'\
            UNZ+1+IC1'";
let segments: Vec<_> = from_bytes(raw).collect::<Result<Vec<_>, _>>()?;

let validated = validate_envelope_lenient(&segments).interchange.unwrap();
let report = ValidationContext::builder()
    .with_envelope_validation()
    .with_syntax_validation()
    .build()
    .validate_lenient(&segments);

let wire = Contrl::from_report(&validated, &segments, &report).to_edifact_string()?;

// FTX is segment 2 of the message (UNH is 1); the value is data element
// position 2 (the tag is position 1), component 1.
assert!(wire.contains("UCS+2'"));
assert!(wire.contains("UCD+12+2:1'"));
# Ok::<(), edifact_rs::EdifactError>(())

Warnings are reported but do not reject: the interchange above stays acknowledged (7) while still naming the offending value. §5.3.3 allows exactly that — "errors may be reported even if the referenced-level is acknowledged".


Where a finding lands

The message nests five reporting levels, each naming a part of the subject interchange:

UCI   the interchange          → UNB / UNZ
 UCF  a group                  → UNG / UNE
  UCM a message or package     → UNH / UNT
   UCS a segment               → by position, UNH = 1
    UCD a data element         → by position within that segment

Which levels appear follows the subject interchange. §5.3.1 makes segment groups 1 and 3 mutually exclusive: an interchange that uses UNG/UNE is reported through UCF, and one that does not gets UCM directly under the UCI. There is nothing to configure — from_report reads the shape off the subject.

§5.3.3 asks for the lowest level that can express a fault, and forbids repeating the code further up. But Annex A decides which level may carry which code, and the two rules can pull in opposite directions: "duplicate detected" (26) is meaningless below the message, so pushing it down would emit a UCS that says a segment is wrong without saying how.

edifact-rs resolves that the way the standard requires — the lowest level that is both able to locate the fault and permitted to carry the code:

use edifact_rs::contrl::{ReportingLevel, SyntaxError};

// Legal everywhere, so it descends as far as the finding can locate it.
assert!(SyntaxError::InvalidValue.permitted_at(ReportingLevel::DataElement));

// Message level and above only — a UCS could not carry it.
assert!(SyntaxError::DuplicateDetected.permitted_at(ReportingLevel::Message));
assert!(!SyntaxError::DuplicateDetected.permitted_at(ReportingLevel::Segment));

// Segment level only.
assert!(SyntaxError::TooManyGroupRepetitions.permitted_at(ReportingLevel::Segment));
assert!(!SyntaxError::TooManyGroupRepetitions.permitted_at(ReportingLevel::Interchange));
# Ok::<(), edifact_rs::EdifactError>(())

with_interchange_error applies the same rule: a code Annex A forbids at the interchange level is dropped rather than written, because a CONTRL carrying an illegal code is one the partner's translator rejects.

Groups

A fault in a group's own UNG/UNE rejects that group and, implicitly, everything in it — while the interchange around it stays acknowledged:

# use edifact_rs::{Contrl, ValidationContext, from_bytes, validate_envelope_lenient};
// The UNE reference does not match its UNG.
let raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'\
            UNG+ORDERS+SND+RCV+260101:0900+GRP1+UN+D:96A'\
            UNH+M1+ORDERS:D:96A:UN'BGM+220'UNT+3+M1'\
            UNE+1+GRP-OTHER'\
            UNZ+1+IC1'";
let segments: Vec<_> = from_bytes(raw).collect::<Result<Vec<_>, _>>()?;
let validated = validate_envelope_lenient(&segments).interchange.unwrap();
let report = ValidationContext::builder()
    .with_envelope_validation()
    .build()
    .validate_lenient(&segments);

let wire = Contrl::from_report(&validated, &segments, &report).to_edifact_string()?;

// UCI stays at 7; the UCF carries the rejection and code 28.
assert_eq!(
    wire,
    "UNH+IC1+CONTRL:4:1:UN'UCI+IC1+S+R+7'UCF+GRP1+SND+RCV+4+28'UNT+4+IC1'",
);
# Ok::<(), edifact_rs::EdifactError>(())

Error-code mapping

Every EdifactError maps to the narrowest Annex A code that is true of it. §5.3.3 asks for exactly that: "if a precise error code is defined, a more general (and imprecise) error code should not be used."

FindingCONTRL code
MessageCountMismatch, SegmentCountMismatch29 control count does not match
QualifierMismatch (control references)28 references do not match
DuplicateReference26 duplicate detected
MissingRequiredElement, MissingSegment13 missing
InvalidCodeValue, BlankDataElementValue12 invalid value
InvalidSegmentForMessage15 not supported in this position
InvalidElementCount, InvalidComponentCount16 too many constituents
UnrecognisedSyntaxIdentifier, UnsupportedCharset46 character set not supported
CharacterNotInRepertoire, InvalidText21 invalid character(s)
InvalidUna, InvalidDelimiter22 invalid service character(s)
EmptyInterchange, EmptyMessage32 lower level empty
PackageNotSupported47 envelope functionality not supported
anything else18 unspecified error

SyntaxError::for_issue does the same job from a ValidationIssue's stable code, so findings from any validator map correctly — including third-party ones that know nothing about CONTRL. An issue with no stable code, such as a profile-rule finding, becomes 18, which is what the annex provides for a fault it does not name.


Sending it

A CONTRL must travel in an interchange of its own and never inside a group (§5.3). to_interchange_bytes builds that envelope, swapping sender and recipient so the reply goes back the way it came:

# use edifact_rs::{Contrl, from_bytes, validate_envelope};
# let raw = b"UNB+UNOC:3+SENDER+RECEIVER+260101:0900+IC4711'UNH+M1+ORDERS:D:96A:UN'BGM+220'UNT+3+M1'UNZ+1+IC4711'";
# let segments: Vec<_> = from_bytes(raw).collect::<Result<Vec<_>, _>>()?;
# let validated = validate_envelope(&segments)?;
let wire = Contrl::acknowledgement(&validated)
    .to_interchange_string("UNOC", "3", "260101", "0930", "ACK-1")?;

assert!(wire.starts_with("UNB+UNOC:3+RECEIVER+SENDER+260101:0930+ACK-1'"));
assert!(wire.ends_with("UNZ+1+ACK-1'"));
# Ok::<(), edifact_rs::EdifactError>(())

UNT DE 0074 and UNZ DE 0036 are computed from what was actually emitted, so the counts are right by construction.


Parsing an inbound CONTRL

The same layouts serve the read direction. A CONTRL is built entirely from segments in edifact_rs::service, so service::lookup validates one with no directory involved:

use edifact_rs::{DirectoryValidator, ValidationContext, ValidationLayer, from_bytes, service};

let raw = b"UNH+ACK1+CONTRL:4:1:UN'UCI+IC4711+SENDER+RECEIVER+7'UNT+3+ACK1'";
let segments: Vec<_> = from_bytes(raw).collect::<Result<Vec<_>, _>>()?;

// Read the verdict by data element identifier, not by counting to element 3.
assert_eq!(segments[1].value_by_code(&service::UCI, "0083")?, Some("7"));
assert_eq!(segments[1].value_by_code(&service::UCI, "0020")?, Some("IC4711"));

let validator = DirectoryValidator::new(
    "iso-9735-4", service::lookup, |_, _| true, |_, _| None, |_, _| None, None,
);
let report = ValidationContext::builder()
    .with_validator(ValidationLayer::Structure, validator)
    .build()
    .validate_lenient(&segments);
assert!(!report.has_errors());
# Ok::<(), edifact_rs::EdifactError>(())

Scope

This module generates and reads the CONTRL message. It does not implement the two other service messages of the ISO 9735 family — AUTACK (part 6) and KEYMAN (part 9) — which belong to the security parts this crate does not cover.

UNOUNP packages are reported rather than acknowledged: their object is arbitrary binary data this crate does not tokenize, so a subject interchange carrying one raises E044 and maps to code 47, "envelope functionality not supported" — which is the honest answer rather than a silent acknowledgement.


Further reading

  • Validation — producing the report a CONTRL reports on
  • Error Reference — every stable code and what it means
  • Writing — the Writer that renders the result