Skip to content

feat(domain)!: say who is answerable, and under which law - #242

Merged
LKSNDRTMLKV merged 2 commits into
mainfrom
feat/responsible-operator-in-the-passport
Sep 8, 2026
Merged

feat(domain)!: say who is answerable, and under which law#242
LKSNDRTMLKV merged 2 commits into
mainfrom
feat/responsible-operator-in-the-passport

Conversation

@LKSNDRTMLKV

Copy link
Copy Markdown
Member

Closes #233. Built on the OJ read posted on the issue — sources are the original OJ L, 2024/1781, 28.6.2024, the consolidation 02024R1781-20240628, CELEX 32019R1020 and CELEX 32023R0988, all now held locally rather than cited from memory.

What the passport could not say

Annex III(k) asks for the name, contact details and unique operator identifier of the economic operator answerable for the product. None of the three was in the passport:

  • operator_identifier looks like the third and is not — its own doc comment says it is the publisher, frozen at publish, explicitly not who is responsible now;
  • name and role lived on ResponsibleOperator inside TransferChain, which is a separate table and never reaches the passport document or the public view;
  • contact details were modelled nowhere.

Passport::responsible_operator carries all three by value, the same choice facility makes and for the same reason.

Three things the issue did not anticipate

Art. 4 reaches two of our twelve product groups. Art. 4(5) of 2019/1020 limits the article to a closed list of instruments; only construction (305/2011) and toys (2009/48/EC) are on it. So the field cannot be named or documented as "the Art. 4 operator" — for ten of twelve groups that citation would simply be false. ResponsibilityBasis records which limb of point (k) applies, and the catch-all carries its own citation.

OperatorRole was missing one of the four roles Art. 4(2) names. Manufacturer-in-Union, importer-where-the-manufacturer-is-not, authorised representative, and fulfilment service provider where no other is established in the Union — the fallback that stops a product having no answerable party at all. It had no variant, so that case could not be written down. Meanwhile Distributor is in our enum and is not one of the four; the existing enum is a supply-chain taxonomy that overlaps this set without coinciding, and the doc now says so.

Frozen vs current was already answered. Art. 9(1): "The data in the digital product passport shall be accurate, complete and up to date." So point (k) wants the current operator, and operator_identifier is correctly documented as a different, frozen fact. Since a published passport is immutable, "up to date" means a corrected successor when responsibility moves — which the amend route now provides.

A citation defect worth knowing about

Annex III(k) prints "Article 15 of Regulation (EU) 2023/988". Article 15 of that regulation is Cooperation of economic operators with market surveillance authorities; the responsible-person provision is Article 16. Checked in both the original OJ and the consolidation — not a transcription artefact.

Nothing turns on it: Art. 16(1) applies Art. 4(2) and (3) of 2019/1020 wholesale, so both limbs land on the same person performing the same tasks. The variant is named GeneralProductSafety rather than for an article number so it need not choose between the citation as printed and the provision as intended.

Why the module moved

passport is tier 4, transfer is tier 5, and imports only point up the ladder — so the aggregate could not reach a type the satellite owned. This is not a workaround for the gate; it is what the placement rule prescribes. A thing used by two modules belongs at their nearest common parent, and an economic operator is a value object with no aggregate of its own: tier 2, beside manufacturer and facility. The chain records who has been responsible, the passport states who is, and neither owns the concept.

Crate-root re-exports are unchanged, so use dpp_domain::{ResponsibleOperator, OperatorRole} still compiles; only dpp_domain::transfer::operator::* paths are gone.

Contact details are a struct

Four provisions converge on one shape, verbatim: ESPR Art. 27(6) (manufacturer), Art. 29(3) (importer), Art. 4(4) of 2019/1020 and Art. 16(3) of 2023/988 — name, registered trade name or registered trade mark, postal address, electronic address. electronic_address is deliberately not did: a DID document resolves keys, and nothing in it need be a mailbox, so reading it as a contact channel would satisfy the obligation on paper and not in fact.

Not in this PR

ManufacturerInfo is two fields short of Art. 27(6) — no registered trade name, no electronic contact — and 27(6)(a) is an explicit obligation to carry both on the public part of the DPP. Filed as #241 rather than folded in: different provision, different type, and #239 is already touching that struct.

Green

just check green, 1288 tests. Fourteen new tests across the three new modules cover the Art. 4(2) role set, the fallback role, the basis round-trip and its citation, role_fits_basis in both directions, and — the one that matters for a persisted shape — an operator stored before the contact fields existed still reading, as None.

@codacy-production

codacy-production Bot commented Sep 7, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 325 complexity · 106 duplication

Metric Results
Complexity 325
Duplication 106

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@LKSNDRTMLKV

Copy link
Copy Markdown
Member Author

Self-review caught a real defect in this PR, fixed in b447409.

responsible_operator was missing from PASSPORT_WIRE_KEYS, and the completeness gate could not see it. The chain is quiet enough to be worth writing down:

  1. I set responsible_operator: None in sample_passport() and never populated it in fully_populated_passport().
  2. The field is skip_serializing_if = "Option::is_none", so a fully-populated passport did not emit the key.
  3. wire_keys_are_exactly_what_passport_emits compares the const against what that helper emits — so it passed, and PASSPORT_WIRE_KEYS stayed one key short.
  4. passport_every_wire_key_is_classified iterates PASSPORT_WIRE_KEYS, so it passed too.

Net effect: a new envelope field classified nowhere, which filter_by_audience defaults to Public — exactly what that second test says it exists to prevent. just check was green throughout.

Public happens to be the right answer here, and now it is stated rather than defaulted: responsibleOperator is in PUBLICLY_SERVED with the reason, since the point of recording Annex III(k) is that a reader holding the product can reach the answerable party without going out of band. Populating the helper first made the gate go red before I classified it.

The failure message on that test already anticipates this — "or fully_populated_passport does not populate it" — so the gate was correct and I read past it.

One adjacent correction in the same list: manufacturer's reason string read "Annex III(k): name and contact details of the operator". Point (k) is the responsible operator; the manufacturer is Annex III(g) plus Art. 27(6)(a). Corrected in place with a note, since leaving a wrong citation directly above the correct one would be worse than the original.

Annex III(k) asks for the responsible operator's name, contact details and identifier; none of the three was in the passport, and which law makes them responsible is not derivable from the product group.
responsibleOperator was absent from PASSPORT_WIRE_KEYS because the fully-populated helper left it None, so redaction defaulted it to Public and the completeness gate could not see it.
@LKSNDRTMLKV
LKSNDRTMLKV force-pushed the feat/responsible-operator-in-the-passport branch from b447409 to 06fbda7 Compare September 8, 2026 00:26
@LKSNDRTMLKV
LKSNDRTMLKV merged commit ed5300b into main Sep 8, 2026
21 checks passed
@LKSNDRTMLKV
LKSNDRTMLKV deleted the feat/responsible-operator-in-the-passport branch September 8, 2026 00:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The passport carries the operator's identifier but not the name and contact its cited basis requires

1 participant