feat(event-subscriber): NRC event subscriber + rebuildable read projection (closes #7) #59

Merged
not merged 7 commits from feat/7-event-subscriber-projection into main 2026-06-30 13:57:00 +00:00
Contributor

What & why

S-06 wires the upstream event path's destination. A new event-subscriber consumes NRC zaken/zaak/create notifications (the abonnement callback) and projects them into a rebuildable read model; a new projection-api serves it. Builds directly on the OZ→NRC notification wiring from #56 (ADR-0007).

Per a design decision, the projection is notification-only: it stores id + status=INGEDIEND. bsn/naam_placeholder are deferred — the NRC notification doesn't carry the bsn and the subscriber may not read OpenZaak directly (§8.1). Idempotency and "rebuild" both run off a durable processed_notifications log, so a rebuild needs no OpenZaak access. See ADR-0008.

Closes #7

Definition of Done

  • Linked Gitea issue (above).
  • Failing test committed before the implementation (red test(event-subscriber) → green feat(event-subscriber)).
  • Implementation makes the test pass; refactor/hardening commit follows.
  • Conventional Commits referencing the issue (refs #7).
  • CI green — all Gitea Actions jobs passed on PR #59: lint, build, unit (10 unit + 3 acceptance), mutation (100%), and verify-stack (verify-acl + verify-nrc + verify-projection).
  • docker compose up reaches green health — verify-stack brought the full stack up and its health gate passed (~5.5 min job; bring-up well within budget).
  • Docs updated (ADR-0008, demo-script).
  • ADR added (docs/architecture/adr-0008-read-projection-store.md).
  • Demo note in docs/demo-script.md.

Notes for reviewers

Architecture decision to eyeball (ADR-0008): the projection DB is shared by the writer (event-subscriber) and reader (projection-api) as one CQRS read-model bounded context. This nudges §8.5 ("each service owns its schema"); I judged it the textbook read-model split (one derived, rebuildable store) rather than a cross-domain DB reach, and documented the reasoning.

New dependency: EF Core 10 + Npgsql.EntityFrameworkCore.PostgreSQL (first EF use in the repo). dotnet-ef pinned as a local tool; NuGetAuditMode=direct keeps EF's design-time-only tooling transitive (NU1903) out of the shipped, audited graph.

Verification done locally: isolated live end-to-end against real containers + Postgres + EF migrate — 401 without the callback bearer (NRC's probe contract), 204 with it, an INGEDIEND projection row, idempotent replay, and rebuild-from-log. Unit + acceptance + 100% mutation all green.

Not verifiable locally (env, not code): the full OZ→NRC→subscriber path needs egress to selectielijst.openzaak.nl (to publish a zaaktype → create a zaak) and a stable docker compose; the local box is podman with neither. The new verify-projection step in the verify-stack job exercises it in CI. The NRC→subscriber hop reuses the driver/contract already proven by #56.

Follow-ups: populate bsn/naam_placeholder via an ACL zaak-read (§8.1-clean); provision a persistent abonnement (needs a dotted network alias — single-label hosts aren't URL-valid for NRC).

## What & why S-06 wires the upstream event path's destination. A new **event-subscriber** consumes NRC `zaken`/`zaak`/`create` notifications (the abonnement callback) and projects them into a rebuildable read model; a new **projection-api** serves it. Builds directly on the OZ→NRC notification wiring from #56 (ADR-0007). Per a design decision, the projection is **notification-only**: it stores `id` + `status=INGEDIEND`. `bsn`/`naam_placeholder` are deferred — the NRC notification doesn't carry the bsn and the subscriber may not read OpenZaak directly (§8.1). Idempotency and "rebuild" both run off a durable `processed_notifications` log, so a rebuild needs no OpenZaak access. See **ADR-0008**. Closes #7 ## Definition of Done - [x] Linked Gitea issue (above). - [x] Failing test committed before the implementation (red `test(event-subscriber)` → green `feat(event-subscriber)`). - [x] Implementation makes the test pass; refactor/hardening commit follows. - [x] Conventional Commits referencing the issue (`refs #7`). - [x] CI green — all Gitea Actions jobs passed on PR #59: lint, build, unit (10 unit + 3 acceptance), mutation (100%), and verify-stack (verify-acl + verify-nrc + verify-projection). - [x] `docker compose up` reaches green health — verify-stack brought the full stack up and its health gate passed (~5.5 min job; bring-up well within budget). - [x] Docs updated (ADR-0008, demo-script). - [x] ADR added (`docs/architecture/adr-0008-read-projection-store.md`). - [x] Demo note in `docs/demo-script.md`. ## Notes for reviewers **Architecture decision to eyeball (ADR-0008):** the projection DB is shared by the writer (event-subscriber) and reader (projection-api) as one CQRS read-model bounded context. This nudges §8.5 ("each service owns its schema"); I judged it the textbook read-model split (one derived, rebuildable store) rather than a cross-domain DB reach, and documented the reasoning. **New dependency:** EF Core 10 + `Npgsql.EntityFrameworkCore.PostgreSQL` (first EF use in the repo). `dotnet-ef` pinned as a local tool; `NuGetAuditMode=direct` keeps EF's design-time-only tooling transitive (NU1903) out of the shipped, audited graph. **Verification done locally:** isolated live end-to-end against real containers + Postgres + EF migrate — 401 without the callback bearer (NRC's probe contract), 204 with it, an INGEDIEND projection row, idempotent replay, and rebuild-from-log. Unit + acceptance + 100% mutation all green. **Not verifiable locally (env, not code):** the full OZ→NRC→subscriber path needs egress to `selectielijst.openzaak.nl` (to publish a zaaktype → create a zaak) and a stable `docker compose`; the local box is podman with neither. The new `verify-projection` step in the `verify-stack` job exercises it in CI. The NRC→subscriber hop reuses the driver/contract already proven by #56. **Follow-ups:** populate `bsn`/`naam_placeholder` via an ACL zaak-read (§8.1-clean); provision a persistent abonnement (needs a dotted network alias — single-label hosts aren't URL-valid for NRC).
not added 7 commits 2026-06-30 13:24:51 +00:00
Failing unit + acceptance tests for the Event Subscriber's NotificationProjector:
a zaken/zaak/create notification yields one INGEDIEND projection row, duplicate
deliveries collapse to one row, non-zaak/non-create notifications are ignored, and
a rebuild repopulates the projection from the durable notification log (PRD §8.4).

The projector is a no-op stub so the tests compile and fail on the assertions; the
implementation follows in the green commit. The notification log doubles as the
idempotency guard and rebuild source so a rebuild needs no OpenZaak access (§8.1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implement NotificationProjector: a zaken/zaak/create notification records the delivery
in the notification log (atomic record-or-skip for idempotency, §8.6) and upserts an
INGEDIEND projection row keyed by zaak id; other channels/actions are ignored. Rebuild
clears the projection and replays the log — no OpenZaak access needed (§8.1). bsn/naam
are deferred (ADR-0008).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the projection persistence and the two services around it:

- Projection.ReadModel: a shared EF Core (Npgsql) read model owning the projection
  schema — register_projection + the subscriber's processed_notifications log — plus
  EfProjectionStore / EfNotificationLog (atomic record-or-skip on the PK for idempotency)
  and the initial migration. One rebuildable store, written by the subscriber and read
  by projection-api (ADR-0008).
- EventSubscriber.Api: POST /notifications NRC callback (enforces the abonnement bearer,
  401 without it per ADR-0007), POST /admin/rebuild, /health. Migrates on start.
- ProjectionApi.Api: GET /register, GET /register/{id}, /health — the read side.

dotnet-ef pinned as a local tool for migrations; NuGetAuditMode=direct so EF's
design-time-only tooling transitive doesn't flag the shipped build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sharpen the projector tests so Stryker has no survivors (was 75%): assert a replayed
delivery never reaches the store (upsert count, not just row count), that two distinct
zaken get distinct rows (pins the idempotency key), that rebuild clears stale rows, and
a Theory over wrong kanaal/resource/actie combinations (pins the zaken/zaak/create guard).
Add the per-service Stryker config + solution; break threshold 90 (CLAUDE.md §5).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Check the Authorization header before deserializing the notification, and read/parse the
body manually. NRC probes a new abonnement's callback with a request that has neither the
configured auth nor a valid notification body, and refuses to register unless it gets a 401
(not a 400) — ADR-0007. Mirrors the verify harness's sink contract.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add projection-db + the two services to both compose files (host ports 8110/8120), their
Dockerfiles (repo-root context — they share Projection.ReadModel), and a runner-safe
verify-projection check (infra/run-projection-check.sh) that registers the abonnement at the
real subscriber, creates a zaak and asserts projection-api serves an INGEDIEND row. Wire it
into make (verify-projection, verify, WAIT_SVCS) and the CI verify-stack job, and run the
event-subscriber Stryker ratchet in `make mutation` + upload its report.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
docs(arch): ADR-0008 read projection store + demo note for the event path (refs #7)
All checks were successful
CI / lint (pull_request) Successful in 1m0s
CI / build (pull_request) Successful in 45s
CI / unit (pull_request) Successful in 54s
CI / mutation (pull_request) Successful in 2m16s
CI / verify-stack (pull_request) Successful in 5m34s
0d0778036e
ADR-0008 records the read-projection design: one rebuildable store shared by the Event
Subscriber (writer) and projection-api (reader) as one CQRS bounded context (reconciled
with §8.5), idempotency + rebuild from the notification log (no OpenZaak access, §8.1),
the deferred bsn/naam, and the new EF Core + Npgsql dependency. Add a demo-script entry
walking the OZ→NRC→subscriber→projection-api path and wire both into the MkDocs nav.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
not added the type:slicearea:event-subscriberarea:projection labels 2026-06-30 13:25:04 +00:00
not merged commit c3ccffe417 into main 2026-06-30 13:57:00 +00:00
Sign in to join this conversation.