Repository navigation
Conversation
…moment Versions are numbered per locale, so "version 3" names a different state in every language. Nothing identifies one moment across locales, which a printed carrier needs: the passport state a product was placed on the market with is one moment, not one per language. Add `publication_releases`: one row per minted version, with a `sequence` that is monotonic per publication, minted inside the same lock as the version number. `publication_versions.release_id` points at it. Existing versions are backfilled, one release each in publish order. `PublicationRelease::versionsAsOf()` resolves the state as of a release: for every locale, the most recent version minted at or before it. That is the read a per-release public route needs and nothing else; this change alters no public behaviour. Releases are immutable like versions (update/delete throw), and `release_id` is sealed on the version.
… out of shared caches `Publisher::redactAll()` redacted only the `is_current` versions before flipping the publication to Redacted. Every superseded version kept its sealed payload readable in the database, so a GDPR Art. 17 erasure held only for as long as nothing ever read history. Now every not-yet-redacted version of the publication is redacted; versions already redacted on their own are left untouched. The public tombstone changes with it. A redacted passport answers `410 Gone` (the state is irreversible), while a withdrawn one stays `200` because it can be reinstated. Both tombstones are now `private, no-store` and `noindex`: the package has no cache purge hook, so a shared cache must never hold a tombstone that a reinstatement would have to displace, and the tombstone must not be indexed regardless of the channel's `indexable` setting. Tests: redactAll nulls superseded payloads and stamps the reason on them; a version redacted individually earlier keeps its own reason; the redacted route is 410 + no-store + noindex; the withdrawn route is 200 + no-store + noindex.
Adds `GET /{prefix}/{uuid}/r/{sequence}/{locale}`: the state of the
passport as of one release, for one locale, resolved through
`PublicationRelease::versionsAsOf()`. This is the URL a printed carrier
can be bound to: it names one moment across every language and keeps
resolving to exactly that state after the passport moves on.
Semantics:
- strict locale, no Accept-Language negotiation; a locale that had no
version yet at that release is 404 there
- 200 with a banner naming the release and whether it is still current,
linking to the live page; the locale switcher stays inside the release
- never indexed (`noindex, noarchive, nofollow`, a `<link rel=canonical>`
and a `Link` header pointing at the live page); no JSON-LD negotiation,
since a historical payload's stamped identity is the publication URL
- a version redacted individually renders the tombstone with 410 and
`no-store`, even while the publication itself stays Published; this
also closes the same gap on the live route
- the ETag now covers release, currency and redaction state, because
the banner and the tombstone change the HTML without changing the
checksum; TEMPLATE_VERSION bumped
- release pages are not counted as views
`show()` and `showRelease()` share one `render()`. The four new strings
live in the Publication package and are present in all 33 locales, in
English where no translation exists yet, so the translation audit passes.
Contributor
Author
|
Context, motivation and suggested review order for this and the related PRs: #683 |
The public `/carrier.svg` encodes the live URL, and nothing records what
was printed. A manual holds a QR code for years; when the passport moves
on there is no durable answer to "which code is in that manual and what
did it point at".
Add `publication_carrier_issuances`: publication, release, the exact
string encoded, format, issued_at, issued_by. Immutable, like versions
and releases. `CarrierIssuer::issue()` mints the row and derives the
target from the per-channel passport base URL, the same base every
other printed link uses.
The target is a new public entry route, `/{prefix}/{uuid}/r/{sequence}`:
it negotiates the locale once from Accept-Language among the locales
that exist in that release and 302s to the strict per-locale URL, the
same way the live carrier's bare `/{uuid}` works. Negotiation is moved
into `PublicationResolver::pickVersion()` so the live and release paths
share it.
Admin: "Issue QR code" per release on the versions page (publish
rights), returning the SVG as a download and recording the issuance.
The page also lists releases (with their locales) and every issuance
with its encoded link. QR rendering is shared via `CarrierSvg` so the
public live carrier and an issued release carrier print identically for
the same target.
midego1
force-pushed
the
feat/publication-carrier-issuance
branch
from
September 4, 2026 09:18
8bc8650 to
393e7ca
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #681 (which stacks on #679 and #680)
Only the last commit (
feat(publication): carrier issuance bound to a release) is new here.Problem
The public
/carrier.svgencodes the live passport URL, and nothing records what was printed. A manual holds a QR code for years. When the passport moves on, there is no durable answer to "which code is in that manual, and what did it point at when it was issued". For the DPP that record is the evidence, not the image.Change
publication_carrier_issuances:publication_id,release_id,target(the exact string encoded, kept verbatim so a later base-URL change cannot rewrite history),format,issued_at,issued_by_id. Immutable like versions and releases:update()/delete()throwImmutableVersionException.CarrierIssuer::issue()mints the row and derives the target from the per-channel passport base URL (general.publication.settings.base_url, falling back toapp.url), the same base every other printed link already uses.Public entry route
GET /{prefix}/{uuid}/r/{sequence}: the URL a release carrier encodes. It negotiates the locale once fromAccept-Language, among the locales that exist in that release, and 302s to the strict per-locale URL from #681. This mirrors how the live carrier's bare/{uuid}works. Negotiation moved intoPublicationResolver::pickVersion()so the live and release paths share one implementation.Admin: an "Issue QR code" action per release on the versions page (publish permission, ACL row added), returning the SVG as a download and recording the issuance. The page now also shows a Release column on versions, a Releases table (with the locales each release contains), and an Issued QR codes table with every issuance and its encoded link.
CarrierSvg: QR rendering extracted fromPublicationCarrierControllerso the live carrier and an issued release carrier print identically for the same target. The live carrier's behaviour is unchanged.Strings: 15 new keys under
passport::app.publications.releases/.carrier, present in all 33 locales.unopim:translations:checkpasses. PHPStan (level 2) clean.Tests
Publication:
<base>/p/{uuid}/r/1, release, format, issuer; update and delete throwVary: Accept-Language; unknown release 404sAccept-Languageis honoured only among locales present in that release, falling back to what existsProductPassport:
image/svg+xml, attachment filename,<svgbody, row recorded with release and issuerNot in this PR
/carrier.svg(s-maxage=86400on a target that can change when the GTIN or base URL changes). Worth a small follow-up with an ETag on the target string.publications.alias_identifieragainst being rewritten on a GTIN correction. Belongs with the GS1 qualifier work.Related
Follows #677, #678, #679, #680, #681. Next: GS1 Digital Link qualifiers (
/01/{gtin}/10/{lot},/01/{gtin}/21/{serial}) resolving to a release through a lot→release mapping behind a contract.