Skip to content

An item-level passport cannot state the unit's serial number #240

Description

@LKSNDRTMLKV

Granularity::Item is modelled — "one passport per physical unit" — and Passport::granularity carries it. Passport has no field for the unit's serial number, so an item-level passport cannot say which item it is.

What fills the gap today, and why it is not the answer

A GS1 Digital Link carrier is /01/{gtin}/21/{serial}, and AI 21 is the serial number. The value put there is derived from the passport's own UUID: dpp_digital_link::short_serial takes the random tail of the UUIDv7 and hex-encodes it to twenty characters, because a raw 36-character UUID exceeds the GS1 20-char cap.

That derivation is deliberate and should stay. It was moved off the leading bytes in 0.11.0 precisely because a UUIDv7 opens with a millisecond timestamp, so the old serial sorted in creation order and its first twelve hex characters decoded to the passport's creation instant — a disclosure through the printed label. The current serial is opaque by design.

But opaque-by-design is the point: it is an identifier for the passport, not for the product. It is not the serial stamped on the unit, it is not the serial in the manufacturer's ERP, and nothing can reconcile the two. An item-level passport that cannot state the manufacturer's own serial cannot be matched back to the physical unit by anyone who has the unit in their hands — which is most of the people the passport exists for.

Why it matters beyond identification

  • Registration. IR (EU) 2026/1778 Art. 8(4)–(5) links an item registration back up to its batch and model. batch_id exists on the passport; the item-level identifier does not, so the most granular level is the one that cannot state its own key.
  • Granularity folds to a maximum. Granularity::most_granular means two acts naming different levels compound rather than conflict, so item level is reachable from any product group whose instrument set includes one act that names it. The field being absent is not scoped to a product group that has not shipped.
  • It is envelope. Additive-only, no lens, and signed content. Adding it later is a field addition; recovering it later is not possible at all, because the serial lives on a physical object.

Suggested shape

/// The manufacturer's own serial for this unit, where the passport is
/// item-level. Distinct from the AI 21 value in the carrier, which is derived
/// from the passport id and identifies the record, not the product.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub serial_number: Option<String>,

Option, so the envelope stays additive and a model- or batch-level passport simply has none. None means "not an item-level record, or not stated" — worth deciding whether Granularity::Item should make it required at publish, or stay advisory. Item-level passports are rare enough today that a lint is probably the honest first step, matching how the other partial checks in this repo behave.

Also worth settling: whether the carrier's AI 21 should switch to this value when it is present. It should probably not — the resolver is GTIN-keyed and ignores the serial entirely, and putting a real serial in a public URL reintroduces exactly the kind of disclosure the 0.11.0 change removed. But it should be decided rather than left implicit.

Why this is filed rather than built

The consuming node reserves a serial_number column that has never been written, because there has never been a field to write from. It is being kept rather than dropped on the basis that this field is coming; that decision should not stand indefinitely on an unfiled intention.

Green test

An item-level passport round-trips a serial; a passport stored before the field existed deserialises with None; and the derived AI 21 carrier value is unchanged by the presence of a real serial.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions