feat(#78): one citizen reference across self-service and the openbaar register (#79)
All checks were successful
All checks were successful
## What & why Before this change the self-service confirmation and the openbaar register showed **different** identifiers, so a citizen could not look their registration back up (#78). Now both surface the same **reference**: - **domain → ACL (write):** the domain `registrationId` is set as the zaak's `identificatie` on `POST /zaken`. - **event-subscriber → ACL (read):** the subscriber reads the zaak's `identificatie` back through the ACL (§8.1 — only the ACL talks to ZGW) via a new `POST /zaken/reference`, and stores it on the projection row **and** the `processed_notifications` replay log. - **BFF + openbaar:** the public view exposes `id/status/reference` (never bsn/naam) and searches by id or reference; the register's "Referentie" column shows the reference. Storing the reference in the replay log keeps ADR-0008's **rebuild-is-log-only** invariant intact — `/admin/rebuild` reproduces the reference without re-reading the ACL. Decision recorded in **ADR-0012**. ## Definition of Done - [x] Linked issue: #78 - [x] Tests written first; red → green per layer - [x] Unit + acceptance green (`make unit`): domain 49, acl 27, bff 20, event-subscriber 19, acceptance 7 - [x] Frontend lint + test green (`nx run-many -t lint test`) - [x] Mutation ≥ break(90): acl 100%, event-subscriber 100%, bff 100%, domain 98.41% (pre-existing FlowableWorkflowClient baseline, untouched) - [x] e2e extended: confirmation reference == register reference - [x] openapi.json + api-client regenerated (drift guard green) - [x] ADR-0012 added; demo-script note appended - [x] `Acl__BaseUrl` wired for the subscriber in compose closes #78 Reviewed-on: #79
This commit was merged in pull request #79.
This commit is contained in:
104
docs/architecture/adr-0012-citizen-reference-correlation.md
Normal file
104
docs/architecture/adr-0012-citizen-reference-correlation.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# ADR-0012: One citizen-facing reference across self-service and the openbaar register
|
||||
|
||||
- **Status:** Accepted
|
||||
- **Date:** 2026-07-14
|
||||
- **Deciders:** Respellion engineering
|
||||
- **Relates to:** #78 (adr-proposal); builds on ADR-0008 (read projection), ADR-0001 (loose coupling), ADR-0009 (external-task worker / zaak creation)
|
||||
|
||||
## Context
|
||||
|
||||
A citizen submits through the self-service portal and is shown a confirmation with a
|
||||
**reference** so they can find their registration back in the public register. But the two
|
||||
sides showed **different identifiers**:
|
||||
|
||||
- The self-service confirmation shows the **domain `registrationId`** — a GUID minted by the
|
||||
domain aggregate (`RegistrationId.New()`) when the registration is created, before any zaak
|
||||
exists.
|
||||
- The openbaar register showed the **zaak id** — the UUID from the NRC `hoofdObject` URL,
|
||||
assigned by OpenZaak when the ACL opens the zaak.
|
||||
|
||||
These never match, so the reference on the confirmation was useless for looking the entry up.
|
||||
The two identifiers live on opposite sides of the ACL boundary and are generated by different
|
||||
systems at different times, so there is no way to reconcile them after the fact without a
|
||||
correlating value carried across the boundary.
|
||||
|
||||
The NRC notification the Event Subscriber consumes carries only the zaak URL plus the fixed
|
||||
`kenmerken` (`bronorganisatie`, `zaaktype`, `vertrouwelijkheidaanduiding`) — **not** the
|
||||
`registrationId`, the bsn, or the `identificatie`. ADR-0008 already recorded that filling any
|
||||
such field means reading the zaak **through the ACL** (§8.1) and deferred it as a follow-up.
|
||||
This is that follow-up, scoped to the one field the citizen actually needs.
|
||||
|
||||
## Decision
|
||||
|
||||
**Use the domain `registrationId` as the zaak's `identificatie`, and surface that single value
|
||||
as the citizen-facing `reference` on both portals. The Event Subscriber enriches the projection
|
||||
with the reference by reading the zaak through the ACL, and stores it in the replay log so
|
||||
rebuild stays log-only.**
|
||||
|
||||
Concretely, following the request path:
|
||||
|
||||
1. **Domain → ACL (write).** When the OpenZaak worker opens a zaak, it passes
|
||||
`registration.Id` to the ACL (`IAclClient.OpenZaakAsync(bsn, reference, …)`). The ACL sets
|
||||
it as the zaak's `identificatie` on `POST /zaken`. OpenZaak's `identificatie` is unique per
|
||||
`bronorganisatie` and ≤ 40 chars — a GUID string fits. The ACL remains the only code that
|
||||
constructs ZGW payloads (§8.1); the domain never sees a ZGW URL.
|
||||
2. **Event Subscriber → ACL (read).** On a notification, the subscriber asks the ACL for the
|
||||
zaak's reference via a new `POST /zaken/reference` endpoint (`{ zaakUrl } → { reference }`),
|
||||
which reads the zaak's `identificatie` through the ACL's OpenZaak gateway. The subscriber
|
||||
still never talks to ZGW itself (§8.1) — it depends only on the ACL, over HTTP.
|
||||
3. **Projection + replay log.** The reference is written both to the `register_projection` row
|
||||
**and** to the `processed_notifications` replay log (a new nullable `reference` column on
|
||||
each). Storing it in the log is what keeps ADR-0008's "**rebuild replays the log, not
|
||||
OpenZaak**" invariant true: `POST /admin/rebuild` reproduces the reference from the log
|
||||
without re-reading the ACL.
|
||||
4. **BFF + openbaar.** The public view (`OpenbaarProjection.PublicView`) exposes
|
||||
`id`, `status`, and `reference` (never bsn/naam), and the openbaar search matches on either
|
||||
`id` or `reference`. The openbaar register's "Referentie" column now renders `reference`.
|
||||
|
||||
The end-to-end guarantee is asserted in the Playwright walking-skeleton: the reference captured
|
||||
from the submit confirmation must appear as a cell in the public register.
|
||||
|
||||
### Why HTTP to the ACL, not the ACL as a library
|
||||
|
||||
ADR-0008 floated "extend the ACL with a zaak-read operation, consumed as a library." We instead
|
||||
call the ACL **over HTTP**, consistent with every other cross-service hop in this system
|
||||
(portals→BFF, domain→ACL). Sharing the ACL as a library would couple the subscriber to the
|
||||
ACL's infrastructure assembly and its ZGW client configuration, defeating the anti-corruption
|
||||
boundary. The HTTP endpoint keeps the ACL the single owner of ZGW access and its config.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive**
|
||||
|
||||
- One reference, end to end: the citizen's confirmation value is exactly what the public
|
||||
register shows and searches by.
|
||||
- §8.1 stays intact — only the ACL reads or writes ZGW; the subscriber depends on the ACL, not
|
||||
OpenZaak.
|
||||
- Rebuild stays log-only (ADR-0008): the reference is replayed from `processed_notifications`,
|
||||
so `/admin/rebuild` needs no ACL/ZGW access.
|
||||
- The column additions are nullable and additive; older rows without a reference are tolerated.
|
||||
|
||||
**Negative / costs**
|
||||
|
||||
- A new coupling: the Event Subscriber now depends on the ACL being reachable
|
||||
(`Acl__BaseUrl`, compose `depends_on: acl`). A registration whose reference read fails will
|
||||
need the notification redelivered (NRC already redelivers; the projection upsert is
|
||||
idempotent).
|
||||
- One extra HTTP hop per notification (subscriber→ACL→OpenZaak) on the projection path. Bounded:
|
||||
one small GET per zaak, off the citizen's request path.
|
||||
- `identificatie` now carries semantic meaning (it equals the `registrationId`). If OpenZaak
|
||||
were ever configured to auto-generate `identificatie`, the correlation would break; the ACL
|
||||
setting it explicitly is now load-bearing.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Carry the `registrationId` in the notification** — rejected: NRC `kenmerken` are fixed and
|
||||
the notification content is not ours to extend; it would also couple the projection to a
|
||||
bespoke notification shape.
|
||||
- **Show the zaak id on the confirmation instead** — rejected: the zaak does not exist yet when
|
||||
the confirmation is returned (the worker opens it asynchronously, ADR-0009), so the domain has
|
||||
no zaak id to show at submit time.
|
||||
- **Store only on the projection row, re-read the ACL on rebuild** — rejected: it would make
|
||||
rebuild depend on the ACL/ZGW, breaking ADR-0008's log-only rebuild invariant.
|
||||
- **Reconcile the two ids in a lookup table** — rejected: adds write-only state and a second
|
||||
source of truth for a value that can simply be the same on both sides.
|
||||
Reference in New Issue
Block a user