refactor: strip WP-/RB- ticket refs from backend (RD-19)
The backend half of the sweep RD-18 did for the front end. git blame holds the provenance and stays correct when the code moves; the comment names a closed ticket and tells the reader nothing the sentence around it does not. public/letter.css and LetterHtml.golden.html change together, because the renderer inlines the CSS and the golden file snapshots the result. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
bin/
|
bin/
|
||||||
obj/
|
obj/
|
||||||
|
|
||||||
# WP-22: runtime SQLite file (+ WAL sidecars) — ship the migration, not the data.
|
# Runtime SQLite file (+ WAL sidecars) — ship the migration, not the data.
|
||||||
bigregister.db
|
bigregister.db
|
||||||
bigregister.db-shm
|
bigregister.db-shm
|
||||||
bigregister.db-wal
|
bigregister.db-wal
|
||||||
|
|||||||
+3
-3
@@ -1,4 +1,4 @@
|
|||||||
# WP-30: lean deployable image (optional — not used by the dev demo, which keeps the SDK
|
# Lean deployable image (optional — not used by the dev demo, which keeps the SDK
|
||||||
# image in the root docker-compose.yml for `dotnet run` hot-reload). Build from the repo
|
# image in the root docker-compose.yml for `dotnet run` hot-reload). Build from the repo
|
||||||
# root: `docker build -f backend/Dockerfile .`
|
# root: `docker build -f backend/Dockerfile .`
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||||
@@ -9,10 +9,10 @@ RUN dotnet publish backend/src/BigRegister.Api -c Release -o /app
|
|||||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0
|
FROM mcr.microsoft.com/dotnet/aspnet:10.0
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --chown=$APP_UID:$APP_UID --from=build /app .
|
COPY --chown=$APP_UID:$APP_UID --from=build /app .
|
||||||
# LetterHtml.Render (WP-25) walks up from AppContext.BaseDirectory looking for a sibling
|
# LetterHtml.Render walks up from AppContext.BaseDirectory looking for a sibling
|
||||||
# public/letter.css (the FE⇄BE letter contract) — this keeps that lookup working here too.
|
# public/letter.css (the FE⇄BE letter contract) — this keeps that lookup working here too.
|
||||||
COPY --chown=$APP_UID:$APP_UID public ./public
|
COPY --chown=$APP_UID:$APP_UID public ./public
|
||||||
# $APP_UID (uid/gid 1654, "app") is baked into this base image for exactly this purpose —
|
# $APP_UID (uid/gid 1654, "app") is baked into this base image for exactly this purpose —
|
||||||
# non-root, and chown'd above so it can still create/write bigregister.db (WP-22) at /app.
|
# non-root, and chown'd above so it can still create/write bigregister.db at /app.
|
||||||
USER $APP_UID
|
USER $APP_UID
|
||||||
ENTRYPOINT ["dotnet", "BigRegister.Api.dll"]
|
ENTRYPOINT ["dotnet", "BigRegister.Api.dll"]
|
||||||
|
|||||||
+2
-2
@@ -15,7 +15,7 @@ status codes and error envelope are production-shaped.
|
|||||||
covers it, see `docker-compose.yml`) does **not** lose data. Delete the file to
|
covers it, see `docker-compose.yml`) does **not** lose data. Delete the file to
|
||||||
reset demo data back to empty, the same state a fresh clone starts from. This is
|
reset demo data back to empty, the same state a fresh clone starts from. This is
|
||||||
a deliberate, right-sized choice for a POC (SQLite, no external DB service) — see
|
a deliberate, right-sized choice for a POC (SQLite, no external DB service) — see
|
||||||
`docs/project/backlog/WP-22-durable-persistence.md`.
|
the durable-persistence design note in `docs/project/backlog/`.
|
||||||
|
|
||||||
## Run
|
## Run
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ semantics) is introduced as **`/api/v2`** served alongside v1 until clients migr
|
|||||||
- `Diplomas/DiplomaRules.cs` — profession derivation + which policy questions apply.
|
- `Diplomas/DiplomaRules.cs` — profession derivation + which policy questions apply.
|
||||||
- `Registrations/HerregistratieRule.cs` — eligibility + reason + status invariant.
|
- `Registrations/HerregistratieRule.cs` — eligibility + reason + status invariant.
|
||||||
- `Intake/IntakePolicy.cs` — scholing threshold + completeness re-validation on submit
|
- `Intake/IntakePolicy.cs` — scholing threshold + completeness re-validation on submit
|
||||||
(`RejectIncompleteScholing`, WP-69).
|
(`RejectIncompleteScholing`).
|
||||||
- `Submissions/SubmissionRules.cs` — submit rejections + reference generation.
|
- `Submissions/SubmissionRules.cs` — submit rejections + reference generation.
|
||||||
|
|
||||||
## Typed client (NSwag)
|
## Typed client (NSwag)
|
||||||
|
|||||||
+17
-17
@@ -1,4 +1,4 @@
|
|||||||
# OpenZaak integration harness (WP-54)
|
# OpenZaak integration harness
|
||||||
|
|
||||||
A real OpenZaak, for developing/testing the ZGW seam (`backend/src/BigRegister.Api/Zgw/`)
|
A real OpenZaak, for developing/testing the ZGW seam (`backend/src/BigRegister.Api/Zgw/`)
|
||||||
against something that isn't a fixture or a stub `HttpMessageHandler`. Deliberately **not**
|
against something that isn't a fixture or a stub `HttpMessageHandler`. Deliberately **not**
|
||||||
@@ -22,7 +22,7 @@ published), and one zaak (`BIG-2026-000123`) with an initiator rol for the seede
|
|||||||
(`111222333` — the same fixture BSN `OpenZaakZaakSourceTests.cs` uses). It writes what it
|
(`111222333` — the same fixture BSN `OpenZaakZaakSourceTests.cs` uses). It writes what it
|
||||||
seeded to `seeded.env` (gitignored) and prints a summary.
|
seeded to `seeded.env` (gitignored) and prints a summary.
|
||||||
|
|
||||||
**Idempotent (WP-56)** — every resource is looked up by its natural key (the same field(s)
|
**Idempotent** — every resource is looked up by its natural key (the same field(s)
|
||||||
OpenZaak enforces identity on: catalogus by `domein`+`rsin`, zaaktype by `catalogus`+
|
OpenZaak enforces identity on: catalogus by `domein`+`rsin`, zaaktype by `catalogus`+
|
||||||
`identificatie`, statustype by `zaaktype`+`volgnummer`, roltype by `zaaktype`+
|
`identificatie`, statustype by `zaaktype`+`volgnummer`, roltype by `zaaktype`+
|
||||||
`omschrijvingGeneriek`, zaak by `identificatie`) before creating it, so re-running against an
|
`omschrijvingGeneriek`, zaak by `identificatie`) before creating it, so re-running against an
|
||||||
@@ -81,7 +81,7 @@ and is **excluded** from the default `dotnet test` run and from CI (`ci.yml`,
|
|||||||
`scripts/ci-local.sh` both filter `Category!=Integration`) — it only passes with this harness
|
`scripts/ci-local.sh` both filter `Category!=Integration`) — it only passes with this harness
|
||||||
up, so it never runs where the harness doesn't exist.
|
up, so it never runs where the harness doesn't exist.
|
||||||
|
|
||||||
## Notifications-enabled profile (WP-58)
|
## Notifications-enabled profile
|
||||||
|
|
||||||
The base harness above never delivers a real notification (`NOTIFICATIONS_DISABLED: 'true'`,
|
The base harness above never delivers a real notification (`NOTIFICATIONS_DISABLED: 'true'`,
|
||||||
no celery worker) — fine for the read/write ZGW seam, not for proving a live webhook round-trip.
|
no celery worker) — fine for the read/write ZGW seam, not for proving a live webhook round-trip.
|
||||||
@@ -137,7 +137,7 @@ already high going in; `ZGW_DEBUG_HTTP=1` on `api`, see `docker-compose.openzaak
|
|||||||
diagnostics to help nail the cause next time it reproduces). Prints the URLs to check
|
diagnostics to help nail the cause next time it reproduces). Prints the URLs to check
|
||||||
afterward and the teardown commands.
|
afterward and the teardown commands.
|
||||||
|
|
||||||
Two caveats, both non-fatal (WP-60 catches and flags rather than surfacing an error):
|
Two caveats, both non-fatal (the BFF catches and flags rather than surfacing an error):
|
||||||
**only `herregistratie` has a seeded zaaktype** here, so submit that wizard to prove a real
|
**only `herregistratie` has a seeded zaaktype** here, so submit that wizard to prove a real
|
||||||
write; and **no Documenten content is seeded**, so a document upload's ZGW half no-ops (pick
|
write; and **no Documenten content is seeded**, so a document upload's ZGW half no-ops (pick
|
||||||
"per post" in the wizard's document step, or ignore it).
|
"per post" in the wizard's document step, or ignore it).
|
||||||
@@ -148,7 +148,7 @@ write; and **no Documenten content is seeded**, so a document upload's ZGW half
|
|||||||
docker compose -f docker-compose.openzaak.yml down -v
|
docker compose -f docker-compose.openzaak.yml down -v
|
||||||
```
|
```
|
||||||
|
|
||||||
## Production (WP-55)
|
## Production
|
||||||
|
|
||||||
This dev harness stays dev-only: hardcoded `SECRET_KEY`, `POSTGRES_HOST_AUTH_METHOD=trust`,
|
This dev harness stays dev-only: hardcoded `SECRET_KEY`, `POSTGRES_HOST_AUTH_METHOD=trust`,
|
||||||
`IS_HTTPS: 'no'`, a client secret checked into `setup_configuration/data.yaml`. A real
|
`IS_HTTPS: 'no'`, a client secret checked into `setup_configuration/data.yaml`. A real
|
||||||
@@ -188,7 +188,7 @@ app change.
|
|||||||
Django migrations then `setup_configuration` against `setup_configuration/data.yaml`), and
|
Django migrations then `setup_configuration` against `setup_configuration/data.yaml`), and
|
||||||
`web` (the OpenZaak API on `:8000`). Pinned to `openzaak/open-zaak:1.29.1`. No
|
`web` (the OpenZaak API on `:8000`). Pinned to `openzaak/open-zaak:1.29.1`. No
|
||||||
celery/celery-beat/celery-flower/nginx — trimmed for a lean, fast-booting harness; layer
|
celery/celery-beat/celery-flower/nginx — trimmed for a lean, fast-booting harness; layer
|
||||||
`docker-compose.openzaak.notificaties.yml` (WP-58) on top for a real async notification
|
`docker-compose.openzaak.notificaties.yml` on top for a real async notification
|
||||||
delivery round-trip.
|
delivery round-trip.
|
||||||
`NOTIFICATIONS_DISABLED=true` is required, not optional: without it, OpenZaak 500s (and
|
`NOTIFICATIONS_DISABLED=true` is required, not optional: without it, OpenZaak 500s (and
|
||||||
**rolls back the whole create**) on any notified resource — see the compose file's comment.
|
**rolls back the whole create**) on any notified resource — see the compose file's comment.
|
||||||
@@ -197,14 +197,14 @@ app change.
|
|||||||
one `bigregister-test` client with `heeft_alle_autorisaties: false` — this YAML mechanism
|
one `bigregister-test` client with `heeft_alle_autorisaties: false` — this YAML mechanism
|
||||||
(`vng_api_common`'s `ApplicatieConfigurationModel`) has no field for granular scopes at all,
|
(`vng_api_common`'s `ApplicatieConfigurationModel`) has no field for granular scopes at all,
|
||||||
so the client starts with zero Autorisaties; `bootstrap-catalogus.sh` grants the exact ones
|
so the client starts with zero Autorisaties; `bootstrap-catalogus.sh` grants the exact ones
|
||||||
it needs (WP-57).
|
it needs.
|
||||||
- `bootstrap-catalogus.sh` — the business content (catalogus/zaaktype/zaak/…) `setup_configuration`
|
- `bootstrap-catalogus.sh` — the business content (catalogus/zaaktype/zaak/…) `setup_configuration`
|
||||||
has no YAML for; every field value here was checked against OpenZaak's own OpenAPI spec and a
|
has no YAML for; every field value here was checked against OpenZaak's own OpenAPI spec and a
|
||||||
live run of this exact script, not guessed (two OpenZaak quirks it works around: a zaaktype
|
live run of this exact script, not guessed (two OpenZaak quirks it works around: a zaaktype
|
||||||
needs ≥1 resultaattype and 2 statustypen before it can be published, and its
|
needs ≥1 resultaattype and 2 statustypen before it can be published, and its
|
||||||
`selectielijstklasse` and the zaaktype's `selectielijstProcestype` must reference the same
|
`selectielijstklasse` and the zaaktype's `selectielijstProcestype` must reference the same
|
||||||
`procesType` on the public VNG selectielijst API). Idempotent (WP-56) — see "Bring it up" above.
|
`procesType` on the public VNG selectielijst API). Idempotent — see "Bring it up" above.
|
||||||
Also grants `bigregister-test`'s Autorisaties via `manage.py shell` (WP-57, see the script's
|
Also grants `bigregister-test`'s Autorisaties via `manage.py shell` (see the script's
|
||||||
top comment): `ztc` scopes (`catalogi.lezen`/`catalogi.schrijven`, this script's own
|
top comment): `ztc` scopes (`catalogi.lezen`/`catalogi.schrijven`, this script's own
|
||||||
content-creation needs) up front, `zrc` scopes (`zaken.aanmaken`/`zaken.bijwerken`/
|
content-creation needs) up front, `zrc` scopes (`zaken.aanmaken`/`zaken.bijwerken`/
|
||||||
`zaken.lezen`, scoped to the one zaaktype the BFF and this script both use) once that
|
`zaken.lezen`, scoped to the one zaaktype the BFF and this script both use) once that
|
||||||
@@ -213,26 +213,26 @@ app change.
|
|||||||
grant (scoped to a real `informatieobjecttype`, which this script would also need to seed)
|
grant (scoped to a real `informatieobjecttype`, which this script would also need to seed)
|
||||||
when a later WP wires DRC content into this harness.
|
when a later WP wires DRC content into this harness.
|
||||||
- **Not here**: Documenten (DRC) content, or a real Notificaties API (NRC) — add DRC content if a
|
- **Not here**: Documenten (DRC) content, or a real Notificaties API (NRC) — add DRC content if a
|
||||||
later WP needs to prove that round-trip against a live instance too (WP-51 is fixture-tested
|
later change needs to prove that round-trip against a live instance too (fixture-tested
|
||||||
today). A real NRC is a separate application (`open-notificaties`) this harness deliberately
|
today). A real NRC is a separate application (`open-notificaties`) this harness deliberately
|
||||||
doesn't stand up — WP-58's notifications-enabled profile (below) proves live delivery without
|
doesn't stand up — the notifications-enabled profile (below) proves live delivery without
|
||||||
one, since this harness only ever has one subscriber.
|
one, since this harness only ever has one subscriber.
|
||||||
- `docker-compose.openzaak.notificaties.yml` (WP-58) — opt-in overlay: one celery worker for
|
- `docker-compose.openzaak.notificaties.yml` — opt-in overlay: one celery worker for
|
||||||
OpenZaak (async notification delivery needs it) + `NOTIFICATIONS_DISABLED: 'false'`, joined to
|
OpenZaak (async notification delivery needs it) + `NOTIFICATIONS_DISABLED: 'false'`, joined to
|
||||||
the repo root's own compose network so it can reach the `api` container by name (tried
|
the repo root's own compose network so it can reach the `api` container by name (tried
|
||||||
`host.docker.internal:host-gateway` first; this environment's rootless Podman doesn't route
|
`host.docker.internal:host-gateway` first; this environment's rootless Podman doesn't route
|
||||||
container→host-port traffic through it). See "Notifications-enabled profile" below.
|
container→host-port traffic through it). See "Notifications-enabled profile" below.
|
||||||
- `bootstrap-notificaties.sh` (WP-58) — points OpenZaak's `NotificationsConfig` at the BFF's
|
- `bootstrap-notificaties.sh` — points OpenZaak's `NotificationsConfig` at the BFF's
|
||||||
webhook via a `zgw_consumers.Service` (`update_or_create`, idempotent) instead of provisioning
|
webhook via a `zgw_consumers.Service` (`update_or_create`, idempotent) instead of provisioning
|
||||||
a real NRC `abonnement`; preflights that the BFF is reachable with the right secret first
|
a real NRC `abonnement`; preflights that the BFF is reachable with the right secret first
|
||||||
(a misconfigured target here means every write to a notified resource 500s and rolls back).
|
(a misconfigured target here means every write to a notified resource 500s and rolls back).
|
||||||
- `verify-notificatie.sh` (WP-58) — the runnable end-to-end check: PATCHes the seeded zaak, polls
|
- `verify-notificatie.sh` — the runnable end-to-end check: PATCHes the seeded zaak, polls
|
||||||
the BFF's own `/admin/audit` (WP-41) for the resulting `zgw:notificatie`/`allow` row.
|
the BFF's own `/admin/audit` for the resulting `zgw:notificatie`/`allow` row.
|
||||||
- `docker-compose.openzaak.prod.yml` (WP-55) — production overrides layered on top of
|
- `docker-compose.openzaak.prod.yml` — production overrides layered on top of
|
||||||
`docker-compose.openzaak.yml`: real `SECRET_KEY`/DB password/site domain/allowed-hosts from
|
`docker-compose.openzaak.yml`: real `SECRET_KEY`/DB password/site domain/allowed-hosts from
|
||||||
required env vars (fails fast if unset), password DB auth instead of `trust`, `IS_HTTPS: 'yes'`.
|
required env vars (fails fast if unset), password DB auth instead of `trust`, `IS_HTTPS: 'yes'`.
|
||||||
Adds no image/service of its own — see "Production" above for the full flow.
|
Adds no image/service of its own — see "Production" above for the full flow.
|
||||||
- `setup_configuration/data.prod.yaml.template` (WP-55) — the prod counterpart of `data.yaml`
|
- `setup_configuration/data.prod.yaml.template` — the prod counterpart of `data.yaml`
|
||||||
with no secret in it (`${OPENZAAK_CLIENT_SECRET}` etc. as placeholders); `render-prod-secrets.sh`
|
with no secret in it (`${OPENZAAK_CLIENT_SECRET}` etc. as placeholders); `render-prod-secrets.sh`
|
||||||
fills it in to the gitignored `data.prod.yaml`, which the prod compose override mounts over
|
fills it in to the gitignored `data.prod.yaml`, which the prod compose override mounts over
|
||||||
the container's `data.yaml`.
|
the container's `data.yaml`.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# WP-54 (seeding) / WP-56 (idempotency) — seeds business content
|
# Seeds business content
|
||||||
# (catalogus/zaaktype/statustype/roltype/zaak/status/rol) into the OpenZaak harness started
|
# (catalogus/zaaktype/statustype/roltype/zaak/status/rol) into the OpenZaak harness started
|
||||||
# by docker-compose.openzaak.yml. `setup_configuration/data.yaml` only covers infra config
|
# by docker-compose.openzaak.yml. `setup_configuration/data.yaml` only covers infra config
|
||||||
# (JWTSecret + Applicatie) — confirmed by reading the installed `django_setup_configuration`
|
# (JWTSecret + Applicatie) — confirmed by reading the installed `django_setup_configuration`
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
# zaak's `identificatie` + `url` on success; also writes them to seeded.env (repo-ignored) for
|
# zaak's `identificatie` + `url` on success; also writes them to seeded.env (repo-ignored) for
|
||||||
# OpenZaakIntegrationTests.cs to assert against.
|
# OpenZaakIntegrationTests.cs to assert against.
|
||||||
#
|
#
|
||||||
# WP-57: `bigregister-test` starts with ZERO Autorisaties (data.yaml sets
|
# `bigregister-test` starts with ZERO Autorisaties (data.yaml sets
|
||||||
# heeft_alle_autorisaties: false) — the setup_configuration YAML has no field for granular
|
# heeft_alle_autorisaties: false) — the setup_configuration YAML has no field for granular
|
||||||
# scopes at all (confirmed from vng_api_common's own ApplicatieConfigurationModel), so this
|
# scopes at all (confirmed from vng_api_common's own ApplicatieConfigurationModel), so this
|
||||||
# script grants them itself via `manage.py shell` (Django ORM, inside the `web` container) at
|
# script grants them itself via `manage.py shell` (Django ORM, inside the `web` container) at
|
||||||
@@ -71,7 +71,7 @@ oz() {
|
|||||||
echo "$json"
|
echo "$json"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Grant (replace) an Autorisatie for $CLIENT_ID directly via the ORM (see the WP-57 note up
|
# Grant (replace) an Autorisatie for $CLIENT_ID directly via the ORM (see the note up
|
||||||
# top for why this bypasses the REST Autorisaties API). $1 = component, $2 = python list
|
# top for why this bypasses the REST Autorisaties API). $1 = component, $2 = python list
|
||||||
# literal of scopes, $3.. = extra `Autorisatie(...)` kwargs as `name=value` (value already a
|
# literal of scopes, $3.. = extra `Autorisatie(...)` kwargs as `name=value` (value already a
|
||||||
# valid Python literal, e.g. a quoted URL).
|
# valid Python literal, e.g. a quoted URL).
|
||||||
@@ -168,7 +168,7 @@ print(json.dumps({
|
|||||||
echo " created: $zaaktype_url"
|
echo " created: $zaaktype_url"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Granting zrc scopes (zaken.aanmaken, zaken.bijwerken, zaken.lezen, zaken.statussen.toevoegen), scoped to $zaaktype_url — the one zaaktype this harness (and the BFF's Zgw:ZaaktypeUrls config) ever uses. zaken.statussen.toevoegen is needed for WP-66's besluit write: zaken.aanmaken only covers the ONE status set at zaak creation, a later status (the besluit's eindstatus) needs this scope or OpenZaak 403s ('mag je slechts 1 status zetten')..."
|
echo "Granting zrc scopes (zaken.aanmaken, zaken.bijwerken, zaken.lezen, zaken.statussen.toevoegen), scoped to $zaaktype_url — the one zaaktype this harness (and the BFF's Zgw:ZaaktypeUrls config) ever uses. zaken.statussen.toevoegen is needed for the besluit write: zaken.aanmaken only covers the ONE status set at zaak creation, a later status (the besluit's eindstatus) needs this scope or OpenZaak 403s ('mag je slechts 1 status zetten')..."
|
||||||
grant_scopes zrc '["zaken.aanmaken", "zaken.bijwerken", "zaken.lezen", "zaken.statussen.toevoegen"]' \
|
grant_scopes zrc '["zaken.aanmaken", "zaken.bijwerken", "zaken.lezen", "zaken.statussen.toevoegen"]' \
|
||||||
"zaaktype=\"$zaaktype_url\"" \
|
"zaaktype=\"$zaaktype_url\"" \
|
||||||
'max_vertrouwelijkheidaanduiding="openbaar"'
|
'max_vertrouwelijkheidaanduiding="openbaar"'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# WP-58 — points OpenZaak's own NotificationsConfig straight at this repo's BFF webhook
|
# Points OpenZaak's own NotificationsConfig straight at this repo's BFF webhook
|
||||||
# (POST /api/v1/zgw/notificaties, WP-52) instead of standing up a real Notificaties API (NRC)
|
# (POST /api/v1/zgw/notificaties) instead of standing up a real Notificaties API (NRC)
|
||||||
# + abonnement — see docker-compose.openzaak.notificaties.yml's ponytail note for why. Requires
|
# + abonnement — see docker-compose.openzaak.notificaties.yml's ponytail note for why. Requires
|
||||||
# that overlay running (adds the celery worker + flips NOTIFICATIONS_DISABLED) AND the repo
|
# that overlay running (adds the celery worker + flips NOTIFICATIONS_DISABLED) AND the repo
|
||||||
# root's own `docker compose up` running (the overlay joins its `api` container's network —
|
# root's own `docker compose up` running (the overlay joins its `api` container's network —
|
||||||
@@ -41,7 +41,7 @@ from zgw_consumers.models import Service
|
|||||||
service, _ = Service.objects.update_or_create(
|
service, _ = Service.objects.update_or_create(
|
||||||
slug="bff-webhook",
|
slug="bff-webhook",
|
||||||
defaults=dict(
|
defaults=dict(
|
||||||
label="BIG-register BFF webhook (WP-58)",
|
label="BIG-register BFF webhook",
|
||||||
api_type=APITypes.orc,
|
api_type=APITypes.orc,
|
||||||
api_root="$BFF_API_ROOT",
|
api_root="$BFF_API_ROOT",
|
||||||
auth_type=AuthTypes.api_key,
|
auth_type=AuthTypes.api_key,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
#
|
#
|
||||||
# 1. Why container-to-container instead of `http://localhost:8000`: this dev environment's
|
# 1. Why container-to-container instead of `http://localhost:8000`: this dev environment's
|
||||||
# rootless Podman drops container→host-port traffic through `host.docker.internal`
|
# rootless Podman drops container→host-port traffic through `host.docker.internal`
|
||||||
# (confirmed for the WP-58 notifications overlay's celery worker — DNS resolves it, every
|
# (confirmed for the notifications overlay's celery worker — DNS resolves it, every
|
||||||
# TCP connect times out).
|
# TCP connect times out).
|
||||||
#
|
#
|
||||||
# 2. Why the ROOT project's `api` joins INTO this project's network (below), not the other way
|
# 2. Why the ROOT project's `api` joins INTO this project's network (below), not the other way
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# WP-58 — notifications-enabled overlay, layered ON TOP of docker-compose.openzaak.yml
|
# Notifications-enabled overlay, layered ON TOP of docker-compose.openzaak.yml
|
||||||
# (never alone):
|
# (never alone):
|
||||||
#
|
#
|
||||||
# docker compose -f docker-compose.openzaak.yml -f docker-compose.openzaak.notificaties.yml up -d
|
# docker compose -f docker-compose.openzaak.yml -f docker-compose.openzaak.notificaties.yml up -d
|
||||||
#
|
#
|
||||||
# The base file stays the WP-54 fast-iteration default (NOTIFICATIONS_DISABLED=true, no
|
# The base file stays the fast-iteration default (NOTIFICATIONS_DISABLED=true, no
|
||||||
# worker) so nobody testing the read/write seam has to pull/boot this. This overlay flips
|
# worker) so nobody testing the read/write seam has to pull/boot this. This overlay flips
|
||||||
# NOTIFICATIONS_DISABLED off and adds the one celery worker needed to actually deliver a
|
# NOTIFICATIONS_DISABLED off and adds the one celery worker needed to actually deliver a
|
||||||
# notification (see base file's ponytail note).
|
# notification (see base file's ponytail note).
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
# bootstrap-notificaties.sh points OpenZaak's NotificationsConfig straight at the BFF's webhook
|
# bootstrap-notificaties.sh points OpenZaak's NotificationsConfig straight at the BFF's webhook
|
||||||
# instead — same delivery proof (a real write → a real HTTP POST → the BFF's audit trail), far
|
# instead — same delivery proof (a real write → a real HTTP POST → the BFF's audit trail), far
|
||||||
# less harness to stand up and keep alive. Add a real NRC (+ abonnement/kanaal routing) if a
|
# less harness to stand up and keep alive. Add a real NRC (+ abonnement/kanaal routing) if a
|
||||||
# later WP needs more than one subscriber or real kanaal-filtered fan-out.
|
# later change needs more than one subscriber or real kanaal-filtered fan-out.
|
||||||
#
|
#
|
||||||
# No celery-beat here: send_notification is a plain async task (client.post on save), not a
|
# No celery-beat here: send_notification is a plain async task (client.post on save), not a
|
||||||
# scheduled one — beat only matters on a real NRC's polling side, which this harness doesn't have.
|
# scheduled one — beat only matters on a real NRC's polling side, which this harness doesn't have.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# WP-55 — production overrides for docker-compose.openzaak.yml: real secrets, real DB auth,
|
# Production overrides for docker-compose.openzaak.yml: real secrets, real DB auth,
|
||||||
# HTTPS-aware settings. Use ON TOP of the base file, never alone (it has no image/ports of its
|
# HTTPS-aware settings. Use ON TOP of the base file, never alone (it has no image/ports of its
|
||||||
# own to add — see backend/openzaak/README.md for the required env vars and full flow):
|
# own to add — see backend/openzaak/README.md for the required env vars and full flow):
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# WP-54 — a real OpenZaak to develop/test the ZGW seam against, kept OUT of the root
|
# A real OpenZaak to develop/test the ZGW seam against, kept OUT of the root
|
||||||
# docker-compose.yml on purpose (see backend/openzaak/README.md): OpenZaak is a full Django
|
# docker-compose.yml on purpose (see backend/openzaak/README.md): OpenZaak is a full Django
|
||||||
# stack (postgres + redis), heavy compared to this repo's own FE+BFF, and nobody who isn't
|
# stack (postgres + redis), heavy compared to this repo's own FE+BFF, and nobody who isn't
|
||||||
# touching the ZGW slice should have to pull/boot it.
|
# touching the ZGW slice should have to pull/boot it.
|
||||||
#
|
#
|
||||||
# ponytail: trimmed vs. open-zaak's own published compose — no celery/celery-beat/celery-flower
|
# ponytail: trimmed vs. open-zaak's own published compose — no celery/celery-beat/celery-flower
|
||||||
# (async notification delivery, never asserted by the integration test) and no nginx (the test
|
# (async notification delivery, never asserted by the integration test) and no nginx (the test
|
||||||
# hits web's port directly). Add them back only if a later WP needs an actual notification
|
# hits web's port directly). Add them back only if a later change needs an actual notification
|
||||||
# round-trip against this harness (NRC delivery is already covered by fixture tests, WP-52).
|
# round-trip against this harness (NRC delivery is already covered by fixture tests).
|
||||||
services:
|
services:
|
||||||
db:
|
db:
|
||||||
image: postgis/postgis:17-3.5
|
image: postgis/postgis:17-3.5
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# WP-55 — renders setup_configuration/data.prod.yaml.template into the gitignored
|
# Renders setup_configuration/data.prod.yaml.template into the gitignored
|
||||||
# data.prod.yaml docker-compose.openzaak.prod.yml mounts over the container's data.yaml.
|
# data.prod.yaml docker-compose.openzaak.prod.yml mounts over the container's data.yaml.
|
||||||
# Run this once before `docker compose ... up` in a production deploy; re-run whenever the
|
# Run this once before `docker compose ... up` in a production deploy; re-run whenever the
|
||||||
# secrets rotate. Fails fast (no output file) if a required env var is missing — never
|
# secrets rotate. Fails fast (no output file) if a required env var is missing — never
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# Prod counterpart of data.yaml (WP-54's dev-only version, kept as-is for local iteration —
|
# Prod counterpart of data.yaml (the dev-only version, kept as-is for local iteration —
|
||||||
# see docker-compose.openzaak.yml's own comment on why it hardcodes a client secret). This
|
# see docker-compose.openzaak.yml's own comment on why it hardcodes a client secret). This
|
||||||
# template has no secret in it; render-prod-secrets.sh substitutes OPENZAAK_CLIENT_SECRET
|
# template has no secret in it; render-prod-secrets.sh substitutes OPENZAAK_CLIENT_SECRET
|
||||||
# into it to produce the gitignored data.prod.yaml that docker-compose.openzaak.prod.yml
|
# into it to produce the gitignored data.prod.yaml that docker-compose.openzaak.prod.yml
|
||||||
# mounts over the container's data.yaml.
|
# mounts over the container's data.yaml.
|
||||||
#
|
#
|
||||||
# Least-privilege client scopes (WP-57): heeft_alle_autorisaties is false, matching the dev
|
# Least-privilege client scopes: heeft_alle_autorisaties is false, matching the dev
|
||||||
# harness (setup_configuration has no YAML field for granular `autorisaties` — see
|
# harness (setup_configuration has no YAML field for granular `autorisaties` — see
|
||||||
# data.yaml's comment). This template only covers infra config; a real deploy must grant this
|
# data.yaml's comment). This template only covers infra config; a real deploy must grant this
|
||||||
# client's Autorisaties the same way bootstrap-catalogus.sh does for the dev harness — via
|
# client's Autorisaties the same way bootstrap-catalogus.sh does for the dev harness — via
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# documented CLI config mechanism — see docker-compose.openzaak.yml) instead of the Django
|
# documented CLI config mechanism — see docker-compose.openzaak.yml) instead of the Django
|
||||||
# admin. Creates the ONE application the bootstrap script + integration test authenticate as.
|
# admin. Creates the ONE application the bootstrap script + integration test authenticate as.
|
||||||
#
|
#
|
||||||
# heeft_alle_autorisaties is false (WP-57, least privilege) — but
|
# heeft_alle_autorisaties is false (least privilege) — but
|
||||||
# `ApplicatieConfigurationModel` (vng_api_common's setup_configuration step) has no field for
|
# `ApplicatieConfigurationModel` (vng_api_common's setup_configuration step) has no field for
|
||||||
# granular `autorisaties` at all, only this boolean. So this client starts with ZERO scopes;
|
# granular `autorisaties` at all, only this boolean. So this client starts with ZERO scopes;
|
||||||
# bootstrap-catalogus.sh grants the exact ones it needs via `manage.py shell` (Django ORM,
|
# bootstrap-catalogus.sh grants the exact ones it needs via `manage.py shell` (Django ORM,
|
||||||
@@ -12,7 +12,7 @@ sites_config_enable: true
|
|||||||
sites_config:
|
sites_config:
|
||||||
items:
|
items:
|
||||||
- domain: localhost:8000
|
- domain: localhost:8000
|
||||||
name: OpenZaak (WP-54 harness)
|
name: OpenZaak (harness)
|
||||||
|
|
||||||
vng_api_common_credentials_config_enable: true
|
vng_api_common_credentials_config_enable: true
|
||||||
vng_api_common_credentials:
|
vng_api_common_credentials:
|
||||||
@@ -26,5 +26,5 @@ vng_api_common_applicaties:
|
|||||||
- uuid: 5a09b3c9-6a54-4b2b-8f3c-1f9b6b6a3a01
|
- uuid: 5a09b3c9-6a54-4b2b-8f3c-1f9b6b6a3a01
|
||||||
client_ids:
|
client_ids:
|
||||||
- bigregister-test
|
- bigregister-test
|
||||||
label: BIG-register BFF (WP-54 test harness)
|
label: BIG-register BFF (test harness)
|
||||||
heeft_alle_autorisaties: false
|
heeft_alle_autorisaties: false
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# WP-58 — proves the "real write -> real webhook delivery" round-trip end-to-end: PATCHes the
|
# Proves the "real write -> real webhook delivery" round-trip end-to-end: PATCHes the
|
||||||
# zaak bootstrap-catalogus.sh seeded (a notified ZRC resource), then polls the BFF's own audit
|
# zaak bootstrap-catalogus.sh seeded (a notified ZRC resource), then polls the BFF's own audit
|
||||||
# trail (WP-41) for the resulting `zgw:notificatie` row. Requires bootstrap-catalogus.sh and
|
# trail for the resulting `zgw:notificatie` row. Requires bootstrap-catalogus.sh and
|
||||||
# bootstrap-notificaties.sh to have already run.
|
# bootstrap-notificaties.sh to have already run.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||||
@@ -29,7 +29,7 @@ jwt() {
|
|||||||
printf '%s.%s' "$signing_input" "$sig"
|
printf '%s.%s' "$signing_input" "$sig"
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "Triggering a real write: PATCH $ZAAK_URL (bijwerken — WP-57 granted zaken.aanmaken"
|
echo "Triggering a real write: PATCH $ZAAK_URL (bijwerken — the client is granted zaken.aanmaken"
|
||||||
echo "for exactly ONE status, so a second status create 403s; a zaak update is the write this"
|
echo "for exactly ONE status, so a second status create 403s; a zaak update is the write this"
|
||||||
echo "client's narrowed scope can repeat)..."
|
echo "client's narrowed scope can repeat)..."
|
||||||
response=$(curl -sS -X PATCH -H "Authorization: Bearer $(jwt)" -H 'Content-Type: application/json' \
|
response=$(curl -sS -X PATCH -H "Authorization: Bearer $(jwt)" -H 'Content-Type: application/json' \
|
||||||
|
|||||||
@@ -77,11 +77,11 @@ public sealed record DocumentRefDto(string CategoryId, string Channel, string? D
|
|||||||
|
|
||||||
public sealed record ChangeRequestRequest(string Telefoon);
|
public sealed record ChangeRequestRequest(string Telefoon);
|
||||||
|
|
||||||
// Authz/PII-reveal audit row (WP-41) — data-minimised, no PII (see AuthzAuditEntry).
|
// Authz/PII-reveal audit row — data-minimised, no PII (see AuthzAuditEntry).
|
||||||
public sealed record AuthzAuditDto(
|
public sealed record AuthzAuditDto(
|
||||||
string At, string Action, string Resource, string Decision, string Role, string CorrelationId);
|
string At, string Action, string Resource, string Decision, string Role, string CorrelationId);
|
||||||
|
|
||||||
// Feature flags (WP-47): the resolved flag set + the admin toggle body.
|
// Feature flags: the resolved flag set + the admin toggle body.
|
||||||
public sealed record FeatureFlagDto(string Key, string Description, bool Enabled);
|
public sealed record FeatureFlagDto(string Key, string Description, bool Enabled);
|
||||||
public sealed record SetFeatureFlagRequest(bool Enabled);
|
public sealed record SetFeatureFlagRequest(bool Enabled);
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ public sealed record AanvraagSummaryDto(
|
|||||||
string Id, string Type, AanvraagStatusDto Status,
|
string Id, string Type, AanvraagStatusDto Status,
|
||||||
IReadOnlyList<string> DocumentIds,
|
IReadOnlyList<string> DocumentIds,
|
||||||
string CreatedAt, string UpdatedAt, string? SubmittedAt,
|
string CreatedAt, string UpdatedAt, string? SubmittedAt,
|
||||||
string? Owner = null); // populated for the admin cross-owner list (WP-36); the user's own list ignores it
|
string? Owner = null); // populated for the admin cross-owner list; the user's own list ignores it
|
||||||
|
|
||||||
public sealed record AanvraagDetailDto(
|
public sealed record AanvraagDetailDto(
|
||||||
string Id, string Type, AanvraagStatusDto Status,
|
string Id, string Type, AanvraagStatusDto Status,
|
||||||
@@ -118,7 +118,7 @@ public sealed record DraftSyncRequest(
|
|||||||
IReadOnlyList<string>? DocumentIds = null);
|
IReadOnlyList<string>? DocumentIds = null);
|
||||||
|
|
||||||
// Submit carries only the fields the server re-validates per wizard type.
|
// Submit carries only the fields the server re-validates per wizard type.
|
||||||
// AanvullendeScholing/ScholingPunten (WP-69) — intake-typed aanvragen only (gated by
|
// AanvullendeScholing/ScholingPunten — intake-typed aanvragen only (gated by
|
||||||
// IntakePolicy.RejectIncompleteScholing's caller), null for the others.
|
// IntakePolicy.RejectIncompleteScholing's caller), null for the others.
|
||||||
public sealed record AanvraagIndienenRequest(
|
public sealed record AanvraagIndienenRequest(
|
||||||
string? DiplomaHerkomst = null, int? Uren = null,
|
string? DiplomaHerkomst = null, int? Uren = null,
|
||||||
@@ -127,7 +127,7 @@ public sealed record AanvraagIndienenRequest(
|
|||||||
|
|
||||||
public sealed record AanvraagIndienenResponse(string Referentie, AanvraagStatusDto Status);
|
public sealed record AanvraagIndienenResponse(string Referentie, AanvraagStatusDto Status);
|
||||||
|
|
||||||
// --- Beoordeling (WP-65): the behandelportal's case-detail screen. ---
|
// --- Beoordeling: the behandelportal's case-detail screen. ---
|
||||||
|
|
||||||
public sealed record BeoordelingDocumentDto(string DocumentId, string CategoryId, string FileName);
|
public sealed record BeoordelingDocumentDto(string DocumentId, string CategoryId, string FileName);
|
||||||
|
|
||||||
@@ -141,7 +141,7 @@ public sealed record BeoordelingViewDto(
|
|||||||
IReadOnlyList<BeoordelingDocumentDto> Documenten,
|
IReadOnlyList<BeoordelingDocumentDto> Documenten,
|
||||||
BeoordelingDecisionsDto Decisions);
|
BeoordelingDecisionsDto Decisions);
|
||||||
|
|
||||||
/// Recording a decision (WP-65b). `Besluit` is the enum member name as a string — same
|
/// Recording a decision. `Besluit` is the enum member name as a string — same
|
||||||
/// wire convention as `AanvraagStatusDto.Tag` (this backend never ships a raw C# enum,
|
/// wire convention as `AanvraagStatusDto.Tag` (this backend never ships a raw C# enum,
|
||||||
/// it round-trips names via Enum.Parse/.ToString() at the Contracts boundary, no
|
/// it round-trips names via Enum.Parse/.ToString() at the Contracts boundary, no
|
||||||
/// JsonStringEnumConverter configured). The endpoint 400s an unknown name. Toelichting
|
/// JsonStringEnumConverter configured). The endpoint 400s an unknown name. Toelichting
|
||||||
@@ -199,7 +199,7 @@ public sealed record BriefDto(
|
|||||||
// BIG-nummer the case screen ships masked. Status-independent, unlike the action gates.
|
// BIG-nummer the case screen ships masked. Status-independent, unlike the action gates.
|
||||||
public sealed record BriefDecisionsDto(bool CanEdit, bool CanApprove, bool CanReject, bool CanSend, bool CanRevealBigNummer);
|
public sealed record BriefDecisionsDto(bool CanEdit, bool CanApprove, bool CanReject, bool CanSend, bool CanRevealBigNummer);
|
||||||
|
|
||||||
// The brief's screen DTO also carries the org template it renders with (WP-23):
|
// The brief's screen DTO also carries the org template it renders with:
|
||||||
// the sub-org's current PUBLISHED version — or, once sent, the version pinned at
|
// the sub-org's current PUBLISHED version — or, once sent, the version pinned at
|
||||||
// send time (sent letters are immutable; a republish never re-renders them).
|
// send time (sent letters are immutable; a republish never re-renders them).
|
||||||
// The case this letter is about — the zorgverlener + aanvraag the behandelaar is
|
// The case this letter is about — the zorgverlener + aanvraag the behandelaar is
|
||||||
@@ -220,7 +220,7 @@ public sealed record RevealBigNummerResponse(string BigNummer);
|
|||||||
// PRD-0002 §6: coarse, role-derived capabilities for nav/menu-level checks.
|
// PRD-0002 §6: coarse, role-derived capabilities for nav/menu-level checks.
|
||||||
public sealed record MeDto(IReadOnlyList<string> Capabilities);
|
public sealed record MeDto(IReadOnlyList<string> Capabilities);
|
||||||
|
|
||||||
// --- Organization templates (WP-23, Brief v2 PRD §3) ---
|
// --- Organization templates (Brief v2 PRD §3) ---
|
||||||
// The second template axis: appearance/identity per sub-organization (letterhead,
|
// The second template axis: appearance/identity per sub-organization (letterhead,
|
||||||
// footer, signature, margins). Orthogonal to the case-type template (sections +
|
// footer, signature, margins). Orthogonal to the case-type template (sections +
|
||||||
// placeholders); the two only meet at render time.
|
// placeholders); the two only meet at render time.
|
||||||
|
|||||||
@@ -43,15 +43,15 @@ public static class Mappers
|
|||||||
/// is the one place a status has no <see cref="AanvraagStatusTag"/>, so it becomes the wire
|
/// is the one place a status has no <see cref="AanvraagStatusTag"/>, so it becomes the wire
|
||||||
/// convention's magic string here at the boundary rather than living inside the domain type.
|
/// convention's magic string here at the boundary rather than living inside the domain type.
|
||||||
/// Shared by <see cref="ToStatusDto"/> and <c>ZgwZaakMapper</c>, so both status producers
|
/// Shared by <see cref="ToStatusDto"/> and <c>ZgwZaakMapper</c>, so both status producers
|
||||||
/// agree on the projection (WP-68 F3).</summary>
|
/// agree on the projection.</summary>
|
||||||
public static AanvraagStatusDto ToDto(this AanvraagStatus s) => new(
|
public static AanvraagStatusDto ToDto(this AanvraagStatus s) => new(
|
||||||
s.Tag?.ToString() ?? "Concept", s.StepIndex, s.StepCount, s.Referentie, s.Manual, s.Reden);
|
s.Tag?.ToString() ?? "Concept", s.StepIndex, s.StepCount, s.Referentie, s.Manual, s.Reden);
|
||||||
|
|
||||||
// Aanvraag status is COMPUTED ON READ (see the StatusAt extension, Data/AanvraagMapper.cs) —
|
// Aanvraag status is COMPUTED ON READ (see the StatusAt extension, Data/AanvraagMapper.cs) —
|
||||||
// this is now a one-line projection of that onto the wire DTO (WP-68 F3, WP-73).
|
// this is now a one-line projection of that onto the wire DTO.
|
||||||
public static AanvraagStatusDto ToStatusDto(this Aanvraag a, DateTimeOffset now) => a.StatusAt(now).ToDto();
|
public static AanvraagStatusDto ToStatusDto(this Aanvraag a, DateTimeOffset now) => a.StatusAt(now).ToDto();
|
||||||
|
|
||||||
/// <summary>SubmittedAt only exists once Submitted/Decided (WP-73) — null for a Concept,
|
/// <summary>SubmittedAt only exists once Submitted/Decided — null for a Concept,
|
||||||
/// same as the wire DTO's own nullable field.</summary>
|
/// same as the wire DTO's own nullable field.</summary>
|
||||||
private static string? SubmittedAtOf(Aanvraag a) => a switch
|
private static string? SubmittedAtOf(Aanvraag a) => a switch
|
||||||
{
|
{
|
||||||
@@ -61,7 +61,7 @@ public static class Mappers
|
|||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>Draft only exists pre-submission (WP-73) — null once Submitted/Decided (nothing
|
/// <summary>Draft only exists pre-submission — null once Submitted/Decided (nothing
|
||||||
/// reads it past that point; see <c>AanvraagMapper.ApplyTo</c>'s Submitted branch).</summary>
|
/// reads it past that point; see <c>AanvraagMapper.ApplyTo</c>'s Submitted branch).</summary>
|
||||||
private static JsonElement? DraftOf(Aanvraag a) => a is Aanvraag.Concept c ? c.Draft : null;
|
private static JsonElement? DraftOf(Aanvraag a) => a is Aanvraag.Concept c ? c.Draft : null;
|
||||||
|
|
||||||
@@ -69,10 +69,10 @@ public static class Mappers
|
|||||||
a.Id, a.Type, a.ToStatusDto(now), a.DocumentIds,
|
a.Id, a.Type, a.ToStatusDto(now), a.DocumentIds,
|
||||||
a.CreatedAt.ToString("o"), a.UpdatedAt.ToString("o"), SubmittedAtOf(a));
|
a.CreatedAt.ToString("o"), a.UpdatedAt.ToString("o"), SubmittedAtOf(a));
|
||||||
|
|
||||||
/// Admin summary — same shape plus the owner (WP-36; the user-facing list leaves Owner null).
|
/// Admin summary — same shape plus the owner (the user-facing list leaves Owner null).
|
||||||
/// The owner is a BSN, and both consumers of this mapper are cross-owner lists read by
|
/// The owner is a BSN, and both consumers of this mapper are cross-owner lists read by
|
||||||
/// someone who is not the subject (`/admin/cases`, `/werkvoorraad`), so it goes out masked
|
/// someone who is not the subject (`/admin/cases`, `/werkvoorraad`), so it goes out masked
|
||||||
/// (RB-03/BIO-003). Masking here rather than at each endpoint means a third cross-owner
|
/// (BIO-003). Masking here rather than at each endpoint means a third cross-owner
|
||||||
/// list cannot be added that forgets to.
|
/// list cannot be added that forgets to.
|
||||||
public static AanvraagSummaryDto ToAdminSummaryDto(this Aanvraag a, DateTimeOffset now) =>
|
public static AanvraagSummaryDto ToAdminSummaryDto(this Aanvraag a, DateTimeOffset now) =>
|
||||||
a.ToSummaryDto(now) with { Owner = Pii.MaskTail(a.Owner, 3) };
|
a.ToSummaryDto(now) with { Owner = Pii.MaskTail(a.Owner, 3) };
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ namespace BigRegister.Api.Data;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The two-way seam between <see cref="AanvraagEntity"/> (the EF-mapped persistence row —
|
/// The two-way seam between <see cref="AanvraagEntity"/> (the EF-mapped persistence row —
|
||||||
/// mutable, no invariants of its own, exactly the shape SQLite needs) and <see cref="Aanvraag"/>
|
/// mutable, no invariants of its own, exactly the shape SQLite needs) and <see cref="Aanvraag"/>
|
||||||
/// (the closed Concept/Submitted/Decided domain union, WP-73). <see cref="ToDomain"/> is the
|
/// (the closed Concept/Submitted/Decided domain union). <see cref="ToDomain"/> is the
|
||||||
/// read half: it reconstructs whichever variant a row's stored fields describe, going through
|
/// read half: it reconstructs whichever variant a row's stored fields describe, going through
|
||||||
/// that variant's own constructor/required members, so a row that doesn't actually describe a
|
/// that variant's own constructor/required members, so a row that doesn't actually describe a
|
||||||
/// legal aanvraag throws here rather than downstream. <see cref="ApplyTo"/>/<see cref="ToEntity"/>
|
/// legal aanvraag throws here rather than downstream. <see cref="ApplyTo"/>/<see cref="ToEntity"/>
|
||||||
@@ -36,7 +36,7 @@ public static class AanvraagMapper
|
|||||||
var submittedAt = row.SubmittedAt
|
var submittedAt = row.SubmittedAt
|
||||||
?? throw new InvalidOperationException($"Submitted aanvraag {row.Id} has no SubmittedAt.");
|
?? throw new InvalidOperationException($"Submitted aanvraag {row.Id} has no SubmittedAt.");
|
||||||
|
|
||||||
// Reden wins over BesluitStatus — matches the pre-WP-73 StatusAt's own priority. In
|
// Reden wins over BesluitStatus — matches StatusAt's own established priority. In
|
||||||
// practice a row never carries both (BeoordelingRules.CanDecide already refuses a besluit
|
// practice a row never carries both (BeoordelingRules.CanDecide already refuses a besluit
|
||||||
// once Reden's auto-reject makes the projected status Afgewezen), but if it somehow did,
|
// once Reden's auto-reject makes the projected status Afgewezen), but if it somehow did,
|
||||||
// the auto-reject at submission time is authoritative.
|
// the auto-reject at submission time is authoritative.
|
||||||
@@ -133,7 +133,7 @@ public static class AanvraagMapper
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case Aanvraag.Submitted s:
|
case Aanvraag.Submitted s:
|
||||||
// Submitted ⇒ !Draft (WP-73's Draft decision) — nothing reads a submitted aanvraag's
|
// Submitted ⇒ !Draft (the Draft-clearing decision) — nothing reads a submitted aanvraag's
|
||||||
// draft (registratie/application/draft-sync.ts only ever resumes a still-Concept
|
// draft (registratie/application/draft-sync.ts only ever resumes a still-Concept
|
||||||
// wizard), so this is now actually true rather than the aspirational doc-comment it
|
// wizard), so this is now actually true rather than the aspirational doc-comment it
|
||||||
// used to be.
|
// used to be.
|
||||||
@@ -186,11 +186,11 @@ public static class AanvraagMapper
|
|||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>The status at a point in time (WP-68 F3, WP-73) — pattern matching over the
|
/// <summary>The status at a point in time — pattern matching over the
|
||||||
/// closed <see cref="Aanvraag"/> union, replacing the null-forgiving derefs the old flat
|
/// closed <see cref="Aanvraag"/> union, replacing the null-forgiving derefs the old flat
|
||||||
/// mutable row needed (Referentie/SubmittedAt are simply non-nullable on Submitted/Decided
|
/// mutable row needed (Referentie/SubmittedAt are simply non-nullable on Submitted/Decided
|
||||||
/// now, so there's nothing left to force). A recorded decision wins over the auto-approve
|
/// now, so there's nothing left to force). A recorded decision wins over the auto-approve
|
||||||
/// computation, matching the pre-WP-73 priority.</summary>
|
/// computation, matching the established priority.</summary>
|
||||||
public static AanvraagStatus StatusAt(this Aanvraag a, DateTimeOffset now) => a switch
|
public static AanvraagStatus StatusAt(this Aanvraag a, DateTimeOffset now) => a switch
|
||||||
{
|
{
|
||||||
Aanvraag.Concept c => AanvraagStatus.Concept(c.StepIndex, c.StepCount),
|
Aanvraag.Concept c => AanvraagStatus.Concept(c.StepIndex, c.StepCount),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ namespace BigRegister.Api.Data;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// EF Core/SQLite persistence for the three stores that used to be static
|
/// EF Core/SQLite persistence for the three stores that used to be static
|
||||||
/// in-memory dictionaries (WP-22): <see cref="AanvraagEntity"/>, <see cref="StoredDocument"/>
|
/// in-memory dictionaries: <see cref="AanvraagEntity"/>, <see cref="StoredDocument"/>
|
||||||
/// + <see cref="AuditEntry"/>, and <see cref="BriefEntity"/>. Opaque nested shapes
|
/// + <see cref="AuditEntry"/>, and <see cref="BriefEntity"/>. Opaque nested shapes
|
||||||
/// (a wizard's draft snapshot, a brief's sections/placeholders/status) are stored as
|
/// (a wizard's draft snapshot, a brief's sections/placeholders/status) are stored as
|
||||||
/// JSON text columns rather than redesigned into relational tables — the backend
|
/// JSON text columns rather than redesigned into relational tables — the backend
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ using BigRegister.Domain.Submissions;
|
|||||||
namespace BigRegister.Api.Data;
|
namespace BigRegister.Api.Data;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The EF-mapped persistence row for an application (aanvraag) — WP-73 demoted this to
|
/// The EF-mapped persistence row for an application (aanvraag) — demoted to
|
||||||
/// exactly that: a flat, mutable bag with no invariants of its own (SQLite needs precisely
|
/// exactly that: a flat, mutable bag with no invariants of its own (SQLite needs precisely
|
||||||
/// this shape), never read or written directly outside this file. Everywhere else, production
|
/// this shape), never read or written directly outside this file. Everywhere else, production
|
||||||
/// code reads and writes <see cref="Aanvraag"/> (the closed Concept/Submitted/Decided domain
|
/// code reads and writes <see cref="Aanvraag"/> (the closed Concept/Submitted/Decided domain
|
||||||
@@ -32,21 +32,21 @@ public sealed class AanvraagEntity
|
|||||||
public DateTimeOffset UpdatedAt { get; set; }
|
public DateTimeOffset UpdatedAt { get; set; }
|
||||||
public DateTimeOffset? SubmittedAt { get; set; }
|
public DateTimeOffset? SubmittedAt { get; set; }
|
||||||
|
|
||||||
/// <summary>The OpenZaak zaak's URL, set once CreateZaak (WP-50) registers one — null under
|
/// <summary>The OpenZaak zaak's URL, set once CreateZaak registers one — null under
|
||||||
/// the local source. Persisted so later steps (WP-51's document→zaak link) can find it
|
/// the local source. Persisted so later steps (the document→zaak link) can find it
|
||||||
/// without a network round-trip; IZaakSource.CreateZaak itself doesn't write here (the
|
/// without a network round-trip; IZaakSource.CreateZaak itself doesn't write here (the
|
||||||
/// endpoint does, via <see cref="ApplicationStore.SetZaakUrl"/>) to keep the seam's write
|
/// endpoint does, via <see cref="ApplicationStore.SetZaakUrl"/>) to keep the seam's write
|
||||||
/// surface at "return data", not "reach into another store".</summary>
|
/// surface at "return data", not "reach into another store".</summary>
|
||||||
public string? ZaakUrl { get; set; }
|
public string? ZaakUrl { get; set; }
|
||||||
|
|
||||||
/// <summary>WP-60: non-null means the ZGW side of this submit (or its document link) did not
|
/// <summary>Non-null means the ZGW side of this submit (or its document link) did not
|
||||||
/// complete — the local aanvraag is authoritative and is NOT rolled back (that risks an
|
/// complete — the local aanvraag is authoritative and is NOT rolled back (that risks an
|
||||||
/// orphan zaak if the failure landed after the zaak POST succeeded). The zaak, if it exists,
|
/// orphan zaak if the failure landed after the zaak POST succeeded). The zaak, if it exists,
|
||||||
/// is re-findable by <c>identificatie == Referentie</c>. Cleared by a future repair path;
|
/// is re-findable by <c>identificatie == Referentie</c>. Cleared by a future repair path;
|
||||||
/// none exists yet (see openzaak-integration.md's "Write resilience" section).</summary>
|
/// none exists yet (see openzaak-integration.md's "Write resilience" section).</summary>
|
||||||
public string? ZgwError { get; set; }
|
public string? ZgwError { get; set; }
|
||||||
|
|
||||||
/// <summary>WP-65b: a behandelaar's recorded decision, if any. Non-null wins over the
|
/// <summary>A behandelaar's recorded decision, if any. Non-null wins over the
|
||||||
/// auto-approve computation in <see cref="BigRegister.Api.Contracts.Mappers.ToStatusDto"/> —
|
/// auto-approve computation in <see cref="BigRegister.Api.Contracts.Mappers.ToStatusDto"/> —
|
||||||
/// "a recorded decision wins". Mutable across <see cref="AanvraagStatusTag.MeerInfoGevraagd"/>
|
/// "a recorded decision wins". Mutable across <see cref="AanvraagStatusTag.MeerInfoGevraagd"/>
|
||||||
/// (a behandelaar may decide again later); frozen once Goedgekeurd/Afgewezen (terminal, per
|
/// (a behandelaar may decide again later); frozen once Goedgekeurd/Afgewezen (terminal, per
|
||||||
@@ -59,7 +59,7 @@ public sealed class AanvraagEntity
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// EF Core/SQLite-backed application store (WP-22 — was a static Dictionary),
|
/// EF Core/SQLite-backed application store (was a static Dictionary),
|
||||||
/// mirrors <see cref="DocumentStore"/>. ponytail: one global lock — SQLite
|
/// mirrors <see cref="DocumentStore"/>. ponytail: one global lock — SQLite
|
||||||
/// tolerates only one writer at a time anyway, and this was already a single
|
/// tolerates only one writer at a time anyway, and this was already a single
|
||||||
/// coarse gate before the DB existed.
|
/// coarse gate before the DB existed.
|
||||||
@@ -72,7 +72,7 @@ public static class ApplicationStore
|
|||||||
private static readonly object _gate = new();
|
private static readonly object _gate = new();
|
||||||
|
|
||||||
/// Create a Concept for <paramref name="owner"/> — UNLESS one of this
|
/// Create a Concept for <paramref name="owner"/> — UNLESS one of this
|
||||||
/// <paramref name="type"/> already exists unsubmitted. WP-35: at most one Concept per
|
/// <paramref name="type"/> already exists unsubmitted. At most one Concept per
|
||||||
/// type is a server-enforced invariant (the FE's draft-sync only guards it best-effort;
|
/// type is a server-enforced invariant (the FE's draft-sync only guards it best-effort;
|
||||||
/// this stays procedural here — it's an AGGREGATE-SET rule over every (Owner, Type), not
|
/// this stays procedural here — it's an AGGREGATE-SET rule over every (Owner, Type), not
|
||||||
/// something a single Aanvraag value's own shape could ever encode, and there is no unique
|
/// something a single Aanvraag value's own shape could ever encode, and there is no unique
|
||||||
@@ -121,7 +121,7 @@ public static class ApplicationStore
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cross-owner single read (WP-65b) — the behandelaar decision endpoint's counterpart of
|
/// Cross-owner single read — the behandelaar decision endpoint's counterpart of
|
||||||
/// <see cref="Get"/>, same "any owner" shape as <see cref="DeleteAny"/>.
|
/// <see cref="Get"/>, same "any owner" shape as <see cref="DeleteAny"/>.
|
||||||
public static Aanvraag? GetAny(string id)
|
public static Aanvraag? GetAny(string id)
|
||||||
{
|
{
|
||||||
@@ -132,7 +132,7 @@ public static class ApplicationStore
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cross-owner lookup by Referentie — real bug fix (WP-66): the behandelaar besluit
|
/// Cross-owner lookup by Referentie — a real bug fix: the behandelaar besluit
|
||||||
/// endpoint receives the FE-facing case id from <c>IZaakSource.ListCases</c>, which under
|
/// endpoint receives the FE-facing case id from <c>IZaakSource.ListCases</c>, which under
|
||||||
/// <c>OpenZaakZaakSource</c> is the ZGW zaak's own uuid, NOT this store's primary key (only
|
/// <c>OpenZaakZaakSource</c> is the ZGW zaak's own uuid, NOT this store's primary key (only
|
||||||
/// <c>LocalZaakSource</c>'s id happens to already be the Aanvraag.Id — every besluit 404'd
|
/// <c>LocalZaakSource</c>'s id happens to already be the Aanvraag.Id — every besluit 404'd
|
||||||
@@ -147,7 +147,7 @@ public static class ApplicationStore
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Admin: every case across all owners (WP-36). The per-owner List is the norm; this
|
/// Admin: every case across all owners. The per-owner List is the norm; this
|
||||||
/// is the deliberate cross-owner read behind the admin-only /admin/cases endpoint.
|
/// is the deliberate cross-owner read behind the admin-only /admin/cases endpoint.
|
||||||
public static IReadOnlyList<Aanvraag> ListAll()
|
public static IReadOnlyList<Aanvraag> ListAll()
|
||||||
{
|
{
|
||||||
@@ -163,7 +163,7 @@ public static class ApplicationStore
|
|||||||
/// Draft sync: idempotent upsert of the wizard snapshot. Only a Concept is mutable — the
|
/// Draft sync: idempotent upsert of the wizard snapshot. Only a Concept is mutable — the
|
||||||
/// domain reconstruction below is what enforces "0 <= StepIndex <= StepCount"
|
/// domain reconstruction below is what enforces "0 <= StepIndex <= StepCount"
|
||||||
/// (<see cref="Aanvraag.Concept"/>'s own constructor throws on an out-of-range pair instead
|
/// (<see cref="Aanvraag.Concept"/>'s own constructor throws on an out-of-range pair instead
|
||||||
/// of this silently writing one onto the row, the way the pre-WP-73 code did).
|
/// of this silently writing one onto the row, the way the earlier code did).
|
||||||
public static bool SyncDraft(string id, string owner, JsonElement draft, int stepIndex, int stepCount, IReadOnlyList<string>? documentIds)
|
public static bool SyncDraft(string id, string owner, JsonElement draft, int stepIndex, int stepCount, IReadOnlyList<string>? documentIds)
|
||||||
{
|
{
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
@@ -207,7 +207,7 @@ public static class ApplicationStore
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Admin: delete ANY case regardless of owner or submitted state (WP-36). The
|
/// Admin: delete ANY case regardless of owner or submitted state. The
|
||||||
/// user-facing Delete refuses a submitted aanvraag and is owner-scoped; an admin
|
/// user-facing Delete refuses a submitted aanvraag and is owner-scoped; an admin
|
||||||
/// managing the register may remove any case. Cascades to the case's documents
|
/// managing the register may remove any case. Cascades to the case's documents
|
||||||
/// using its own owner. Returns false only when the id doesn't exist.
|
/// using its own owner. Returns false only when the id doesn't exist.
|
||||||
@@ -231,7 +231,7 @@ public static class ApplicationStore
|
|||||||
|
|
||||||
/// Submit transition. reject != null → Afgewezen; else accepted (In behandeling,
|
/// Submit transition. reject != null → Afgewezen; else accepted (In behandeling,
|
||||||
/// auto-advancing to Goedgekeurd after the window when autoApprovable). Returns null
|
/// auto-advancing to Goedgekeurd after the window when autoApprovable). Returns null
|
||||||
/// if the aanvraag is gone or already submitted (idempotency guard). WP-73: the returned
|
/// if the aanvraag is gone or already submitted (idempotency guard). The returned
|
||||||
/// <see cref="Aanvraag.Submitted"/> is constructed with a non-null Referentie/SubmittedAt by
|
/// <see cref="Aanvraag.Submitted"/> is constructed with a non-null Referentie/SubmittedAt by
|
||||||
/// its own required members — there is no longer a null-forgiving deref anywhere down the
|
/// its own required members — there is no longer a null-forgiving deref anywhere down the
|
||||||
/// line reading them back (<c>StatusAt</c>, <c>IZaakSource.CreateZaak</c>). Submitting also
|
/// line reading them back (<c>StatusAt</c>, <c>IZaakSource.CreateZaak</c>). Submitting also
|
||||||
@@ -267,7 +267,7 @@ public static class ApplicationStore
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Persist the zaak URL CreateZaak (WP-50) registered for this aanvraag. No-op if
|
/// <summary>Persist the zaak URL CreateZaak registered for this aanvraag. No-op if
|
||||||
/// the aanvraag is gone (shouldn't happen — this runs right after Submit found it).</summary>
|
/// the aanvraag is gone (shouldn't happen — this runs right after Submit found it).</summary>
|
||||||
public static void SetZaakUrl(string id, string zaakUrl)
|
public static void SetZaakUrl(string id, string zaakUrl)
|
||||||
{
|
{
|
||||||
@@ -297,13 +297,13 @@ public static class ApplicationStore
|
|||||||
|
|
||||||
public enum RecordBesluitOutcome { Ok, NotFound, Conflict }
|
public enum RecordBesluitOutcome { Ok, NotFound, Conflict }
|
||||||
|
|
||||||
/// <summary>Record a behandelaar's decision (WP-65b) — cross-owner like
|
/// <summary>Record a behandelaar's decision — cross-owner like
|
||||||
/// <see cref="DeleteAny"/>, since a behandelaar decides on any citizen's case.
|
/// <see cref="DeleteAny"/>, since a behandelaar decides on any citizen's case.
|
||||||
/// WP-68 (F2): the transition-legality check (<see cref="BeoordelingRules.CanDecide"/>)
|
/// The transition-legality check (<see cref="BeoordelingRules.CanDecide"/>)
|
||||||
/// now runs INSIDE this lock, against a status read fresh under the lock, rather than in
|
/// now runs INSIDE this lock, against a status read fresh under the lock, rather than in
|
||||||
/// the endpoint beforehand — two concurrent besluiten used to both pass the endpoint's
|
/// the endpoint beforehand — two concurrent besluiten used to both pass the endpoint's
|
||||||
/// check before either wrote, letting the second silently overwrite a terminal decision.
|
/// check before either wrote, letting the second silently overwrite a terminal decision.
|
||||||
/// WP-73: <see cref="Aanvraag.Decided.Afgewezen"/>/<see cref="Aanvraag.Decided.MeerInfoGevraagd"/>
|
/// <see cref="Aanvraag.Decided.Afgewezen"/>/<see cref="Aanvraag.Decided.MeerInfoGevraagd"/>
|
||||||
/// require a non-null Toelichting by their own shape — the endpoint already 400s a missing
|
/// require a non-null Toelichting by their own shape — the endpoint already 400s a missing
|
||||||
/// one (<c>BeoordelingRules.RequiresToelichting</c>), and this is the defense-in-depth
|
/// one (<c>BeoordelingRules.RequiresToelichting</c>), and this is the defense-in-depth
|
||||||
/// backstop for any other caller (this method is public, and e.g.
|
/// backstop for any other caller (this method is public, and e.g.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
namespace BigRegister.Api.Data;
|
namespace BigRegister.Api.Data;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A persisted, DATA-MINIMISED authorization/PII-reveal audit entry (WP-41, PRD-0002 §8):
|
/// A persisted, DATA-MINIMISED authorization/PII-reveal audit entry (PRD-0002 §8):
|
||||||
/// who (acting role, not identity), what action, on which resource ref, allow or deny, and
|
/// who (acting role, not identity), what action, on which resource ref, allow or deny, and
|
||||||
/// the correlation id — **never** a name, BSN, or the value that was (or wasn't) revealed.
|
/// the correlation id — **never** a name, BSN, or the value that was (or wasn't) revealed.
|
||||||
/// Id is EF Core's auto-increment key (not positional), mirroring <see cref="AuditEntry"/>.
|
/// Id is EF Core's auto-increment key (not positional), mirroring <see cref="AuditEntry"/>.
|
||||||
@@ -37,7 +37,7 @@ public static class AuthzAuditStore
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Newest first. Ordered client-side: SQLite can't ORDER BY a DateTimeOffset (WP-36).
|
/// Newest first. Ordered client-side: SQLite can't ORDER BY a DateTimeOffset.
|
||||||
public static IReadOnlyList<AuthzAuditEntry> List()
|
public static IReadOnlyList<AuthzAuditEntry> List()
|
||||||
{
|
{
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ namespace BigRegister.Api.Data;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The letter (brief) — one demo brief per owner, created from a template on first
|
/// The letter (brief) — one demo brief per owner, created from a template on first
|
||||||
/// read. EF Core/SQLite-backed (WP-22 — was a static Dictionary), mirrors
|
/// read. EF Core/SQLite-backed (was a static Dictionary), mirrors
|
||||||
/// <see cref="ApplicationStore"/>. The status machine and its guards live here (the
|
/// <see cref="ApplicationStore"/>. The status machine and its guards live here (the
|
||||||
/// server is authoritative for transitions); the FE mirrors them in its pure reducer
|
/// server is authoritative for transitions); the FE mirrors them in its pure reducer
|
||||||
/// for UX. Rich-text content is stored opaquely as DTOs — the stub does not
|
/// for UX. Rich-text content is stored opaquely as DTOs — the stub does not
|
||||||
@@ -23,11 +23,11 @@ public sealed class BriefEntity
|
|||||||
public required IReadOnlyList<PlaceholderDefDto> Placeholders { get; init; }
|
public required IReadOnlyList<PlaceholderDefDto> Placeholders { get; init; }
|
||||||
public List<LetterSectionDto> Sections { get; set; } = new();
|
public List<LetterSectionDto> Sections { get; set; } = new();
|
||||||
public BriefStatusDto Status { get; set; } = new("draft");
|
public BriefStatusDto Status { get; set; } = new("draft");
|
||||||
/// Which sub-organization's org template themes this letter (WP-23).
|
/// Which sub-organization's org template themes this letter.
|
||||||
public string SubOrgId { get; set; } = OrgTemplateSeed.Registers;
|
public string SubOrgId { get; set; } = OrgTemplateSeed.Registers;
|
||||||
/// Pinned at send: sent letters are immutable, a republish never re-themes them.
|
/// Pinned at send: sent letters are immutable, a republish never re-themes them.
|
||||||
public int? SentOrgTemplateVersion { get; set; }
|
public int? SentOrgTemplateVersion { get; set; }
|
||||||
/// The composed HTML archived at send (WP-25) — from here on the preview endpoint
|
/// The composed HTML archived at send — from here on the preview endpoint
|
||||||
/// serves this verbatim, so a later org-template republish never re-renders it.
|
/// serves this verbatim, so a later org-template republish never re-renders it.
|
||||||
public string? ArchivedHtml { get; set; }
|
public string? ArchivedHtml { get; set; }
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ public static class BriefStore
|
|||||||
|
|
||||||
private static readonly object _gate = new();
|
private static readonly object _gate = new();
|
||||||
|
|
||||||
/// Pure query (RB-23/CQ-007): no write. `GET /brief` 404s when this returns null —
|
/// Pure query (CQ-007): no write. `GET /brief` 404s when this returns null —
|
||||||
/// the owner's first-ever draft is created only through the explicit `ResetAndCreate`
|
/// the owner's first-ever draft is created only through the explicit `ResetAndCreate`
|
||||||
/// command (`POST /brief/reset`), never as a side effect of a read.
|
/// command (`POST /brief/reset`), never as a side effect of a read.
|
||||||
public static BriefEntity? Get(string owner)
|
public static BriefEntity? Get(string owner)
|
||||||
@@ -110,10 +110,10 @@ public static class BriefStore
|
|||||||
var outcome = BriefRules.CanSend(e.Status);
|
var outcome = BriefRules.CanSend(e.Status);
|
||||||
if (outcome != Outcome.Ok) return (outcome, null);
|
if (outcome != Outcome.Ok) return (outcome, null);
|
||||||
e.Status = new BriefStatusDto("sent", SentAt: at);
|
e.Status = new BriefStatusDto("sent", SentAt: at);
|
||||||
// Pin the org-template version the letter was sent with (WP-23): from here on
|
// Pin the org-template version the letter was sent with: from here on
|
||||||
// its appearance is frozen — republishing the template touches unsent briefs only.
|
// its appearance is frozen — republishing the template touches unsent briefs only.
|
||||||
e.SentOrgTemplateVersion = OrgTemplateStore.PublishedVersionOf(e.SubOrgId);
|
e.SentOrgTemplateVersion = OrgTemplateStore.PublishedVersionOf(e.SubOrgId);
|
||||||
// Archive the composed HTML at this exact instant (WP-25): the preview endpoint
|
// Archive the composed HTML at this exact instant: the preview endpoint
|
||||||
// serves this verbatim once sent, so a later republish never re-renders it.
|
// serves this verbatim once sent, so a later republish never re-renders it.
|
||||||
var template = OrgTemplateStore.TemplateForBrief(e.SubOrgId, e.SentOrgTemplateVersion);
|
var template = OrgTemplateStore.TemplateForBrief(e.SubOrgId, e.SentOrgTemplateVersion);
|
||||||
e.ArchivedHtml = LetterHtml.Render(e, template, at, watermark: false);
|
e.ArchivedHtml = LetterHtml.Render(e, template, at, watermark: false);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace BigRegister.Api.Data;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Factory for short-lived <see cref="AppDbContext"/> instances. The three stores
|
/// Factory for short-lived <see cref="AppDbContext"/> instances. The three stores
|
||||||
/// (ApplicationStore/DocumentStore/BriefStore) are static classes — that shape
|
/// (ApplicationStore/DocumentStore/BriefStore) are static classes — that shape
|
||||||
/// predates WP-22 and this WP keeps it — so they can't take a constructor-injected
|
/// predates this persistence layer, which keeps that shape — so they can't take a constructor-injected
|
||||||
/// DbContext; each store method opens one here, uses it, and disposes it under its
|
/// DbContext; each store method opens one here, uses it, and disposes it under its
|
||||||
/// own lock instead.
|
/// own lock instead.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ namespace BigRegister.Api.Data;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stored document: metadata + bytes. The demo persists bytes in the SQLite file
|
/// Stored document: metadata + bytes. The demo persists bytes in the SQLite file
|
||||||
/// (WP-22) purely so a re-opened wizard can preview/download what was uploaded — a
|
/// purely so a re-opened wizard can preview/download what was uploaded — a
|
||||||
/// real backend persists them to blob storage keyed by DocumentId. Bytes are never
|
/// real backend persists them to blob storage keyed by DocumentId. Bytes are never
|
||||||
/// serialized into a JSON response; only the dedicated content endpoint streams them.
|
/// serialized into a JSON response; only the dedicated content endpoint streams them.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -14,7 +14,7 @@ public sealed record StoredDocument(
|
|||||||
{
|
{
|
||||||
public bool Linked { get; set; }
|
public bool Linked { get; set; }
|
||||||
|
|
||||||
/// <summary>The OpenZaak DRC enkelvoudiginformatieobject's URL, set once Upload (WP-51)
|
/// <summary>The OpenZaak DRC enkelvoudiginformatieobject's URL, set once Upload
|
||||||
/// registers one — null under the local source. Persisted so the later zaak-link step can
|
/// registers one — null under the local source. Persisted so the later zaak-link step can
|
||||||
/// find it without re-uploading; not part of the positional constructor, same reasoning as
|
/// find it without re-uploading; not part of the positional constructor, same reasoning as
|
||||||
/// <see cref="Linked"/> (every existing `new StoredDocument(...)` call site keeps working).</summary>
|
/// <see cref="Linked"/> (every existing `new StoredDocument(...)` call site keeps working).</summary>
|
||||||
@@ -30,7 +30,7 @@ public sealed record AuditEntry(DateTimeOffset At, string Action, string Documen
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// EF Core/SQLite-backed document store + audit log (WP-22 — was a static
|
/// EF Core/SQLite-backed document store + audit log (was a static
|
||||||
/// Dictionary). ponytail: one global lock, same as before — SQLite tolerates only
|
/// Dictionary). ponytail: one global lock, same as before — SQLite tolerates only
|
||||||
/// one writer at a time anyway, and this process already serialized all access
|
/// one writer at a time anyway, and this process already serialized all access
|
||||||
/// through a single gate, so it now doubles as a coarse single-writer guard for
|
/// through a single gate, so it now doubles as a coarse single-writer guard for
|
||||||
@@ -85,7 +85,7 @@ public static class DocumentStore
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Documents by DocumentId (WP-65's beoordeling detail reads an aanvraag's already-
|
/// <summary>Documents by DocumentId (the beoordeling detail reads an aanvraag's already-
|
||||||
/// linked documents) — the DocumentId-keyed counterpart of <see cref="ByLocalIds"/>, which is
|
/// linked documents) — the DocumentId-keyed counterpart of <see cref="ByLocalIds"/>, which is
|
||||||
/// keyed by the wizard's own LocalId instead.</summary>
|
/// keyed by the wizard's own LocalId instead.</summary>
|
||||||
public static IReadOnlyList<StoredDocument> ByIds(IEnumerable<string> documentIds)
|
public static IReadOnlyList<StoredDocument> ByIds(IEnumerable<string> documentIds)
|
||||||
@@ -114,7 +114,7 @@ public static class DocumentStore
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Persist the DRC url an OpenZaak upload (WP-51) registered for a document.</summary>
|
/// <summary>Persist the DRC url an OpenZaak upload registered for a document.</summary>
|
||||||
public static void SetDrcUrl(string documentId, string drcUrl)
|
public static void SetDrcUrl(string documentId, string drcUrl)
|
||||||
{
|
{
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
@@ -181,7 +181,7 @@ public static class DocumentStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Append one metadata-only audit row. <paramref name="actor"/> must arrive
|
/// <summary>Append one metadata-only audit row. <paramref name="actor"/> must arrive
|
||||||
/// **already redacted** (RB-04/BIO-005) — the two citizen call sites pass
|
/// **already redacted** (BIO-005) — the two citizen call sites pass
|
||||||
/// <see cref="Pii.MaskTail"/> of the owner BSN, `delete-admin` passes the literal
|
/// <see cref="Pii.MaskTail"/> of the owner BSN, `delete-admin` passes the literal
|
||||||
/// `"admin"`. The unmasked BSN lives only in <see cref="StoredDocument.Owner"/>, which is
|
/// `"admin"`. The unmasked BSN lives only in <see cref="StoredDocument.Owner"/>, which is
|
||||||
/// the authorization key and stays untouched. Masking here instead would have to guess
|
/// the authorization key and stays untouched. Masking here instead would have to guess
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ public sealed class FeatureFlagEntity
|
|||||||
public sealed record ResolvedFlag(string Key, string Description, bool Enabled);
|
public sealed record ResolvedFlag(string Key, string Description, bool Enabled);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Runtime feature-flag state (WP-47). SQLite-backed like <see cref="OrgTemplateStore"/>, same
|
/// Runtime feature-flag state. SQLite-backed like <see cref="OrgTemplateStore"/>, same
|
||||||
/// single-gate idiom. The CATALOG (which flags exist + their defaults) is code
|
/// single-gate idiom. The CATALOG (which flags exist + their defaults) is code
|
||||||
/// (<see cref="FeatureFlags"/>); this store only holds the admin's on/off overrides. An unknown
|
/// (<see cref="FeatureFlags"/>); this store only holds the admin's on/off overrides. An unknown
|
||||||
/// key is never writable/enabled — the code catalog is the authority.
|
/// key is never writable/enabled — the code catalog is the authority.
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ using BigRegister.Domain.Authorization;
|
|||||||
namespace BigRegister.Api.Data;
|
namespace BigRegister.Api.Data;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The documents seam (WP-51), sibling of <see cref="IZaakSource"/>: uploads always land
|
/// The documents seam, sibling of <see cref="IZaakSource"/>: uploads always land
|
||||||
/// locally first (<see cref="DocumentStore"/> stays the record of truth for preview/download/
|
/// locally first (<see cref="DocumentStore"/> stays the record of truth for preview/download/
|
||||||
/// audit regardless of config, exactly like <c>ApplicationStore.Submit</c> for aanvragen,
|
/// audit regardless of config, exactly like <c>ApplicationStore.Submit</c> for aanvragen)
|
||||||
/// WP-50) — this interface is only the OpenZaak integration side-effect, selected the same way
|
/// — this interface is only the OpenZaak integration side-effect, selected the same way
|
||||||
/// (<c>Zgw:Enabled</c>). Default binding is <see cref="LocalDocumentSource"/> (offline);
|
/// (<c>Zgw:Enabled</c>). Default binding is <see cref="LocalDocumentSource"/> (offline);
|
||||||
/// <c>OpenZaakDocumentSource</c> also registers each upload as a DRC
|
/// <c>OpenZaakDocumentSource</c> also registers each upload as a DRC
|
||||||
/// enkelvoudiginformatieobject and links it to a zaak once one exists.
|
/// enkelvoudiginformatieobject and links it to a zaak once one exists.
|
||||||
@@ -16,17 +16,17 @@ public interface IDocumentSource
|
|||||||
{
|
{
|
||||||
/// <summary>Store an uploaded file (already validated by <c>DocumentRules</c>) and return the
|
/// <summary>Store an uploaded file (already validated by <c>DocumentRules</c>) and return the
|
||||||
/// existing <see cref="UploadResponse"/> DTO unchanged, whichever source is active.
|
/// existing <see cref="UploadResponse"/> DTO unchanged, whichever source is active.
|
||||||
/// <paramref name="caller"/> (WP-53) is both the document's owner (<c>DocumentStore</c>'s
|
/// <paramref name="caller"/> is both the document's owner (<c>DocumentStore</c>'s
|
||||||
/// ownership field) and, under the OpenZaak source, the identity minted into the ZGW JWT.</summary>
|
/// ownership field) and, under the OpenZaak source, the identity minted into the ZGW JWT.</summary>
|
||||||
UploadResponse Upload(
|
UploadResponse Upload(
|
||||||
string localId, string categoryId, string wizardId, string fileName, string contentType,
|
string localId, string categoryId, string wizardId, string fileName, string contentType,
|
||||||
byte[] content, ZorgverlenerCaller caller);
|
byte[] content, ZorgverlenerCaller caller);
|
||||||
|
|
||||||
/// <summary>Finalise a set of already-uploaded documents against a just-submitted aanvraag
|
/// <summary>Finalise a set of already-uploaded documents against a just-submitted aanvraag:
|
||||||
/// (WP-50/51): local behaviour is exactly today's <c>DocumentStore.Link</c>; the OpenZaak
|
/// local behaviour is exactly today's <c>DocumentStore.Link</c>; the OpenZaak
|
||||||
/// source additionally links each document (that has a DRC url) to the zaak, once
|
/// source additionally links each document (that has a DRC url) to the zaak, once
|
||||||
/// <paramref name="zaakUrl"/> is known (null under the local <see cref="IZaakSource"/>, in
|
/// <paramref name="zaakUrl"/> is known (null under the local <see cref="IZaakSource"/>, in
|
||||||
/// which case there is nothing extra to link) — minted with <paramref name="caller"/>'s
|
/// which case there is nothing extra to link) — minted with <paramref name="caller"/>'s
|
||||||
/// identity (WP-53).</summary>
|
/// identity.</summary>
|
||||||
void LinkToZaak(IReadOnlyList<string> documentIds, string? zaakUrl, CallerIdentity caller);
|
void LinkToZaak(IReadOnlyList<string> documentIds, string? zaakUrl, CallerIdentity caller);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,25 +5,25 @@ using BigRegister.Domain.Authorization;
|
|||||||
namespace BigRegister.Api.Data;
|
namespace BigRegister.Api.Data;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The cases (zaken) READ seam (WP-49). A "zaak" in ZGW terms is an <see cref="Aanvraag"/>
|
/// The cases (zaken) READ seam. A "zaak" in ZGW terms is an <see cref="Aanvraag"/>
|
||||||
/// here; this interface is the one injection point that lets a real ZGW backend (OpenZaak)
|
/// here; this interface is the one injection point that lets a real ZGW backend (OpenZaak)
|
||||||
/// replace the local SQLite store <em>behind the same <see cref="AanvraagSummaryDto"/>
|
/// replace the local SQLite store <em>behind the same <see cref="AanvraagSummaryDto"/>
|
||||||
/// contract</em> — so the frontend never changes (BFF-lite anti-corruption, ADR-0001).
|
/// contract</em> — so the frontend never changes (BFF-lite anti-corruption, ADR-0001).
|
||||||
///
|
///
|
||||||
/// Default binding is <see cref="LocalZaakSource"/> (offline). Setting <c>Zgw:Enabled=true</c>
|
/// Default binding is <see cref="LocalZaakSource"/> (offline). Setting <c>Zgw:Enabled=true</c>
|
||||||
/// swaps in <c>OpenZaakZaakSource</c>. Slice 1 (WP-49) was read-only; <see cref="CreateZaak"/>
|
/// swaps in <c>OpenZaakZaakSource</c>. The first slice was read-only; <see cref="CreateZaak"/>
|
||||||
/// (WP-50) is the first write. The interface returns the wire DTO (not the domain
|
/// is the first write. The interface returns the wire DTO (not the domain
|
||||||
/// <see cref="Aanvraag"/>) precisely so each source owns its own mapping — the OpenZaak
|
/// <see cref="Aanvraag"/>) precisely so each source owns its own mapping — the OpenZaak
|
||||||
/// source maps a ZGW Zaak into this shape, the local source maps the stored aanvraag.
|
/// source maps a ZGW Zaak into this shape, the local source maps the stored aanvraag.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IZaakSource
|
public interface IZaakSource
|
||||||
{
|
{
|
||||||
/// <summary>Every case across every owner, newest-first (the admin cross-owner list,
|
/// <summary>Every case across every owner, newest-first (the admin cross-owner list) —
|
||||||
/// WP-36) — cases:manage only, deliberately NOT citizen-scoped.</summary>
|
/// cases:manage only, deliberately NOT citizen-scoped.</summary>
|
||||||
IReadOnlyList<AanvraagSummaryDto> ListCases(DateTimeOffset now);
|
IReadOnlyList<AanvraagSummaryDto> ListCases(DateTimeOffset now);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Only <paramref name="caller"/>'s own cases (WP-53) — the citizen-scoped counterpart of
|
/// Only <paramref name="caller"/>'s own cases — the citizen-scoped counterpart of
|
||||||
/// <see cref="ListCases"/>, backing the citizen's own dashboard. The local source filters
|
/// <see cref="ListCases"/>, backing the citizen's own dashboard. The local source filters
|
||||||
/// <c>ApplicationStore</c> by owner (unchanged behaviour); the OpenZaak source adds ZGW's
|
/// <c>ApplicationStore</c> by owner (unchanged behaviour); the OpenZaak source adds ZGW's
|
||||||
/// <c>rol__betrokkeneIdentificatie__natuurlijkPersoon__inpBsn</c> query filter so a citizen
|
/// <c>rol__betrokkeneIdentificatie__natuurlijkPersoon__inpBsn</c> query filter so a citizen
|
||||||
@@ -32,9 +32,9 @@ public interface IZaakSource
|
|||||||
IReadOnlyList<AanvraagSummaryDto> ListMyCases(ZorgverlenerCaller caller, DateTimeOffset now);
|
IReadOnlyList<AanvraagSummaryDto> ListMyCases(ZorgverlenerCaller caller, DateTimeOffset now);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Register a just-submitted <paramref name="aanvraag"/> as a zaak (WP-50). The aanvraag is
|
/// Register a just-submitted <paramref name="aanvraag"/> as a zaak. The aanvraag is
|
||||||
/// already persisted locally (<c>ApplicationStore.Submit</c> already ran, hence the
|
/// already persisted locally (<c>ApplicationStore.Submit</c> already ran, hence the
|
||||||
/// <see cref="Aanvraag.Submitted"/> parameter type — WP-73: a freshly submitted aanvraag
|
/// <see cref="Aanvraag.Submitted"/> parameter type — a freshly submitted aanvraag
|
||||||
/// always has a Referentie, so neither implementation needs a null-forgiving deref for it
|
/// always has a Referentie, so neither implementation needs a null-forgiving deref for it
|
||||||
/// any more) — this is the integration side-effect, and (Referentie, Status) is what the
|
/// any more) — this is the integration side-effect, and (Referentie, Status) is what the
|
||||||
/// submit endpoint hands back to the FE (ADR-0001: route the create through the existing
|
/// submit endpoint hands back to the FE (ADR-0001: route the create through the existing
|
||||||
@@ -42,19 +42,19 @@ public interface IZaakSource
|
|||||||
/// the already-computed local reference/status (ZaakUrl null — nothing to persist); the
|
/// the already-computed local reference/status (ZaakUrl null — nothing to persist); the
|
||||||
/// OpenZaak source creates a Zaak (+ status + rol) and maps the result back into the same
|
/// OpenZaak source creates a Zaak (+ status + rol) and maps the result back into the same
|
||||||
/// shape, returning the zaak's URL so the endpoint can persist it
|
/// shape, returning the zaak's URL so the endpoint can persist it
|
||||||
/// (<see cref="ApplicationStore.SetZaakUrl"/>, WP-51 needs it to later link documents to this
|
/// (<see cref="ApplicationStore.SetZaakUrl"/>; linking documents to this
|
||||||
/// zaak). <paramref name="caller"/> (WP-53) is the acting citizen — the ZGW JWT's audit
|
/// zaak later needs it). <paramref name="caller"/> is the acting citizen — the ZGW JWT's audit
|
||||||
/// claims reflect them, not a static config identity.
|
/// claims reflect them, not a static config identity.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
(string Referentie, AanvraagStatusDto Status, string? ZaakUrl) CreateZaak(Aanvraag.Submitted aanvraag, DateTimeOffset now, CallerIdentity caller);
|
(string Referentie, AanvraagStatusDto Status, string? ZaakUrl) CreateZaak(Aanvraag.Submitted aanvraag, DateTimeOffset now, CallerIdentity caller);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extend a behandelaar's already-locally-recorded decision (WP-65b's
|
/// Extend a behandelaar's already-locally-recorded decision (
|
||||||
/// <c>ApplicationStore.RecordBesluit</c> already ran) with a ZGW-side status transition
|
/// <c>ApplicationStore.RecordBesluit</c> already ran) with a ZGW-side status transition —
|
||||||
/// (WP-66) — the write counterpart to <see cref="CreateZaak"/>'s initial status. The local
|
/// the write counterpart to <see cref="CreateZaak"/>'s initial status. The local
|
||||||
/// source is a no-op (the decision IS the record of truth there, unchanged from before this
|
/// source is a no-op (the decision IS the record of truth there, unchanged from before this
|
||||||
/// seam existed); the OpenZaak source POSTs a new Statussen entry to
|
/// seam existed); the OpenZaak source POSTs a new Statussen entry to
|
||||||
/// <paramref name="aanvraag"/>'s zaak. <paramref name="caller"/> (WP-53/62) is the acting
|
/// <paramref name="aanvraag"/>'s zaak. <paramref name="caller"/> is the acting
|
||||||
/// medewerker.
|
/// medewerker.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void RecordBesluit(Aanvraag aanvraag, Besluit besluit, string? toelichting, DateTimeOffset now, CallerIdentity caller);
|
void RecordBesluit(Aanvraag aanvraag, Besluit besluit, string? toelichting, DateTimeOffset now, CallerIdentity caller);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ namespace BigRegister.Api.Data;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The default <see cref="IDocumentSource"/> — uploads go only to the local SQLite
|
/// The default <see cref="IDocumentSource"/> — uploads go only to the local SQLite
|
||||||
/// <see cref="DocumentStore"/>, exactly as before this seam existed (WP-51). Zero behaviour
|
/// <see cref="DocumentStore"/>, exactly as before this seam existed. Zero behaviour
|
||||||
/// change: this is the same <c>DocumentStore.Add</c>/<c>DocumentStore.Link</c> the upload/
|
/// change: this is the same <c>DocumentStore.Add</c>/<c>DocumentStore.Link</c> the upload/
|
||||||
/// submit endpoints used to call inline.
|
/// submit endpoints used to call inline.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace BigRegister.Api.Data;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The default <see cref="IZaakSource"/> — the cases come from the local SQLite
|
/// The default <see cref="IZaakSource"/> — the cases come from the local SQLite
|
||||||
/// <see cref="ApplicationStore"/>, exactly as before the seam existed (WP-49). Zero
|
/// <see cref="ApplicationStore"/>, exactly as before the seam existed. Zero
|
||||||
/// behaviour change: this is the same <c>ListAll().ToAdminSummaryDto(now)</c> the
|
/// behaviour change: this is the same <c>ListAll().ToAdminSummaryDto(now)</c> the
|
||||||
/// <c>/admin/cases</c> endpoint used to call inline.
|
/// <c>/admin/cases</c> endpoint used to call inline.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -15,7 +15,7 @@ public sealed class LocalZaakSource : IZaakSource
|
|||||||
public IReadOnlyList<AanvraagSummaryDto> ListCases(DateTimeOffset now) =>
|
public IReadOnlyList<AanvraagSummaryDto> ListCases(DateTimeOffset now) =>
|
||||||
ApplicationStore.ListAll().Select(a => a.ToAdminSummaryDto(now)).ToList();
|
ApplicationStore.ListAll().Select(a => a.ToAdminSummaryDto(now)).ToList();
|
||||||
|
|
||||||
/// <summary>Citizen-scoped (WP-53) — exactly what <c>GET /aanvragen</c> used to compute
|
/// <summary>Citizen-scoped — exactly what <c>GET /aanvragen</c> used to compute
|
||||||
/// inline before it was routed through this seam.</summary>
|
/// inline before it was routed through this seam.</summary>
|
||||||
public IReadOnlyList<AanvraagSummaryDto> ListMyCases(ZorgverlenerCaller caller, DateTimeOffset now) =>
|
public IReadOnlyList<AanvraagSummaryDto> ListMyCases(ZorgverlenerCaller caller, DateTimeOffset now) =>
|
||||||
ApplicationStore.List(caller.Bsn)
|
ApplicationStore.List(caller.Bsn)
|
||||||
@@ -23,11 +23,11 @@ public sealed class LocalZaakSource : IZaakSource
|
|||||||
.Select(a => a.ToSummaryDto(now)).ToList();
|
.Select(a => a.ToSummaryDto(now)).ToList();
|
||||||
|
|
||||||
/// <summary>No external zaak to create — the aanvraag's local submit already IS the record
|
/// <summary>No external zaak to create — the aanvraag's local submit already IS the record
|
||||||
/// of truth, exactly as before this seam existed (WP-50). Zero behaviour change.</summary>
|
/// of truth, exactly as before this seam existed. Zero behaviour change.</summary>
|
||||||
public (string Referentie, AanvraagStatusDto Status, string? ZaakUrl) CreateZaak(Aanvraag.Submitted aanvraag, DateTimeOffset now, CallerIdentity caller) =>
|
public (string Referentie, AanvraagStatusDto Status, string? ZaakUrl) CreateZaak(Aanvraag.Submitted aanvraag, DateTimeOffset now, CallerIdentity caller) =>
|
||||||
(aanvraag.Referentie, aanvraag.ToStatusDto(now), null);
|
(aanvraag.Referentie, aanvraag.ToStatusDto(now), null);
|
||||||
|
|
||||||
/// <summary>No external zaak to update — the recorded decision already IS the record of
|
/// <summary>No external zaak to update — the recorded decision already IS the record of
|
||||||
/// truth locally (WP-66). Zero behaviour change.</summary>
|
/// truth locally. Zero behaviour change.</summary>
|
||||||
public void RecordBesluit(Aanvraag aanvraag, Besluit besluit, string? toelichting, DateTimeOffset now, CallerIdentity caller) { }
|
public void RecordBesluit(Aanvraag aanvraag, Besluit besluit, string? toelichting, DateTimeOffset now, CallerIdentity caller) { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ using BigRegister.Api.Contracts;
|
|||||||
namespace BigRegister.Api.Data;
|
namespace BigRegister.Api.Data;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Organization template per sub-organization (WP-23, Brief v2 PRD §3): one row per
|
/// Organization template per sub-organization (Brief v2 PRD §3): one row per
|
||||||
/// sub-org. `Draft` is the work-in-progress payload (Version 0), `History` the
|
/// sub-org. `Draft` is the work-in-progress payload (Version 0), `History` the
|
||||||
/// append-only list of published snapshots, `PublishedVersion` points into it.
|
/// append-only list of published snapshots, `PublishedVersion` points into it.
|
||||||
/// Rollback copies an old snapshot back into the draft — it never rewrites history.
|
/// Rollback copies an old snapshot back into the draft — it never rewrites history.
|
||||||
/// Mirrors <see cref="BriefStore"/>: static class, short-lived context per call,
|
/// Mirrors <see cref="BriefStore"/>: static class, short-lived context per call,
|
||||||
/// nested DTO shapes stored as JSON text columns (WP-22 posture).
|
/// nested DTO shapes stored as JSON text columns.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class OrgTemplateEntity
|
public sealed class OrgTemplateEntity
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ using System.Text.Json;
|
|||||||
namespace BigRegister.Domain.Applications;
|
namespace BigRegister.Domain.Applications;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The aanvraag lifecycle as a closed union (WP-73): <see cref="Concept"/> (the pre-submission
|
/// The aanvraag lifecycle as a closed union: <see cref="Concept"/> (the pre-submission
|
||||||
/// wizard draft) → <see cref="Submitted"/> (awaiting a behandelaar's decision, or already
|
/// wizard draft) → <see cref="Submitted"/> (awaiting a behandelaar's decision, or already
|
||||||
/// auto-rejected at submission time — see <see cref="Submitted.Reden"/>) → <see cref="Decided"/>
|
/// auto-rejected at submission time — see <see cref="Submitted.Reden"/>) → <see cref="Decided"/>
|
||||||
/// (a behandelaar's outcome recorded). Each variant carries only the fields that make sense for
|
/// (a behandelaar's outcome recorded). Each variant carries only the fields that make sense for
|
||||||
@@ -31,11 +31,11 @@ public abstract record Aanvraag
|
|||||||
public required DateTimeOffset CreatedAt { get; init; }
|
public required DateTimeOffset CreatedAt { get; init; }
|
||||||
public required DateTimeOffset UpdatedAt { get; init; }
|
public required DateTimeOffset UpdatedAt { get; init; }
|
||||||
|
|
||||||
/// <summary>The OpenZaak zaak's URL, set once CreateZaak (WP-50) registers one — null under
|
/// <summary>The OpenZaak zaak's URL, set once CreateZaak registers one — null under
|
||||||
/// the local source, or before a zaak has been registered at all.</summary>
|
/// the local source, or before a zaak has been registered at all.</summary>
|
||||||
public string? ZaakUrl { get; init; }
|
public string? ZaakUrl { get; init; }
|
||||||
|
|
||||||
/// <summary>WP-60: non-null means the ZGW side of this aanvraag's last write did not
|
/// <summary>Non-null means the ZGW side of this aanvraag's last write did not
|
||||||
/// complete — see <c>Api.Data.ApplicationStore.SetZgwError</c>.</summary>
|
/// complete — see <c>Api.Data.ApplicationStore.SetZgwError</c>.</summary>
|
||||||
public string? ZgwError { get; init; }
|
public string? ZgwError { get; init; }
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ public abstract record Aanvraag
|
|||||||
public string? Reden { get; init; }
|
public string? Reden { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>A behandelaar's decision (WP-65b/68) — closed by besluit: only
|
/// <summary>A behandelaar's decision — closed by besluit: only
|
||||||
/// <see cref="Afgewezen"/>/<see cref="MeerInfoGevraagd"/> require a toelichting
|
/// <see cref="Afgewezen"/>/<see cref="MeerInfoGevraagd"/> require a toelichting
|
||||||
/// (<c>BeoordelingRules.RequiresToelichting</c>'s rule, now also a type, not just an endpoint
|
/// (<c>BeoordelingRules.RequiresToelichting</c>'s rule, now also a type, not just an endpoint
|
||||||
/// check) — omitting it is a compile error, not merely a 400 the type happens to also let
|
/// check) — omitting it is a compile error, not merely a 400 the type happens to also let
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
namespace BigRegister.Domain.Applications;
|
namespace BigRegister.Domain.Applications;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The post-submission aanvraag status lifecycle (ADR-0002, WP-63): Ingediend → In
|
/// The post-submission aanvraag status lifecycle (ADR-0002): Ingediend → In
|
||||||
/// behandeling → (Meer info gevraagd ⇄) → Goedgekeurd/Afgewezen. Concept (pre-submission,
|
/// behandeling → (Meer info gevraagd ⇄) → Goedgekeurd/Afgewezen. Concept (pre-submission,
|
||||||
/// the wizard draft) is deliberately NOT a member here — see <see cref="AanvraagStatus.Tag"/>,
|
/// the wizard draft) is deliberately NOT a member here — see <see cref="AanvraagStatus.Tag"/>,
|
||||||
/// which is null exactly when the aanvraag hasn't been submitted yet, instead of a sixth
|
/// which is null exactly when the aanvraag hasn't been submitted yet, instead of a sixth
|
||||||
/// "magic string" tag with no enum member to match it (WP-68 F3).
|
/// "magic string" tag with no enum member to match it.
|
||||||
/// <see cref="Ingediend"/> is reserved: no endpoint sets it yet (there is no state between
|
/// <see cref="Ingediend"/> is reserved: no endpoint sets it yet (there is no state between
|
||||||
/// "just submitted" and "in behandeling" in this POC) — kept because the FE's status union
|
/// "just submitted" and "in behandeling" in this POC) — kept because the FE's status union
|
||||||
/// and $localize catalogue already declare it, and removing it would ripple into both.
|
/// and $localize catalogue already declare it, and removing it would ripple into both.
|
||||||
@@ -13,13 +13,13 @@ namespace BigRegister.Domain.Applications;
|
|||||||
public enum AanvraagStatusTag { Ingediend, InBehandeling, MeerInfoGevraagd, Goedgekeurd, Afgewezen }
|
public enum AanvraagStatusTag { Ingediend, InBehandeling, MeerInfoGevraagd, Goedgekeurd, Afgewezen }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A behandelaar's recorded decision (WP-65b) — the three actions the beoordeling screen
|
/// A behandelaar's recorded decision — the three actions the beoordeling screen
|
||||||
/// offers, each advancing an aanvraag's <see cref="AanvraagStatus"/>.
|
/// offers, each advancing an aanvraag's <see cref="AanvraagStatus"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum Besluit { Goedkeuren, Afwijzen, MeerInfoOpvragen }
|
public enum Besluit { Goedkeuren, Afwijzen, MeerInfoOpvragen }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The domain projection of an aanvraag's status at a point in time (WP-68 F3) — the type
|
/// The domain projection of an aanvraag's status at a point in time — the type
|
||||||
/// <c>Aanvraag.StatusAt(now)</c> returns, replacing the logic that used to live directly in
|
/// <c>Aanvraag.StatusAt(now)</c> returns, replacing the logic that used to live directly in
|
||||||
/// <c>Contracts.Mappers.ToStatusDto</c>. Constructible only via the factories below, so a
|
/// <c>Contracts.Mappers.ToStatusDto</c>. Constructible only via the factories below, so a
|
||||||
/// caller can never build e.g. a Referentie-less Goedgekeurd. <see cref="Tag"/> is null only
|
/// caller can never build e.g. a Referentie-less Goedgekeurd. <see cref="Tag"/> is null only
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public enum BriefAction { Approve, Reject, Send }
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class Authz
|
public static class Authz
|
||||||
{
|
{
|
||||||
// WP-53: role now comes from the per-request CallerIdentity the identity middleware
|
// Role now comes from the per-request CallerIdentity the identity middleware
|
||||||
// resolved (StubIdentityProvider reads the same X-Role header this used to read directly) —
|
// resolved (StubIdentityProvider reads the same X-Role header this used to read directly) —
|
||||||
// one source of "who", so a real IIdentityProvider swap carries this over unchanged.
|
// one source of "who", so a real IIdentityProvider swap carries this over unchanged.
|
||||||
public static Principal ResolvePrincipal(HttpContext ctx) => new(ctx.Caller().Role);
|
public static Principal ResolvePrincipal(HttpContext ctx) => new(ctx.Caller().Role);
|
||||||
@@ -49,7 +49,7 @@ public static class Authz
|
|||||||
/// BriefStore.Review enforces before its status guard; kept separate from
|
/// BriefStore.Review enforces before its status guard; kept separate from
|
||||||
/// Decisions() below so enforcement ORDER (Forbidden before Conflict) matches
|
/// Decisions() below so enforcement ORDER (Forbidden before Conflict) matches
|
||||||
/// today's behavior exactly. The explicit Approver condition keeps the new Admin
|
/// today's behavior exactly. The explicit Approver condition keeps the new Admin
|
||||||
/// role out of the review flow (WP-23) — SoD alone would have let it through.
|
/// role out of the review flow — SoD alone would have let it through.
|
||||||
public static bool CanActOn(BriefAction action, Principal principal, string drafterId) => action switch
|
public static bool CanActOn(BriefAction action, Principal principal, string drafterId) => action switch
|
||||||
{
|
{
|
||||||
BriefAction.Approve or BriefAction.Reject =>
|
BriefAction.Approve or BriefAction.Reject =>
|
||||||
@@ -58,7 +58,7 @@ public static class Authz
|
|||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Org-template management (WP-23): admin-only, resource-independent — templates
|
/// Org-template management: admin-only, resource-independent — templates
|
||||||
/// have no per-resource state to weigh, so role IS the whole decision here.
|
/// have no per-resource state to weigh, so role IS the whole decision here.
|
||||||
public static bool CanManageOrgTemplates(Principal principal) => principal.Role == PrincipalRole.Admin;
|
public static bool CanManageOrgTemplates(Principal principal) => principal.Role == PrincipalRole.Admin;
|
||||||
|
|
||||||
@@ -67,18 +67,18 @@ public static class Authz
|
|||||||
/// the maintenance editor consumes; the actual edit lands as a reviewed PR, not a write here.
|
/// the maintenance editor consumes; the actual edit lands as a reviewed PR, not a write here.
|
||||||
public static bool CanEditStamdata(Principal principal) => principal.Role == PrincipalRole.Admin;
|
public static bool CanEditStamdata(Principal principal) => principal.Role == PrincipalRole.Admin;
|
||||||
|
|
||||||
/// Case management (WP-36): admin-only, resource-independent — same shape as
|
/// Case management: admin-only, resource-independent — same shape as
|
||||||
/// org-template / stamdata (role IS the decision). Gates the cross-owner /admin/cases
|
/// org-template / stamdata (role IS the decision). Gates the cross-owner /admin/cases
|
||||||
/// list + admin delete.
|
/// list + admin delete.
|
||||||
public static bool CanManageCases(Principal principal) => principal.Role == PrincipalRole.Admin;
|
public static bool CanManageCases(Principal principal) => principal.Role == PrincipalRole.Admin;
|
||||||
|
|
||||||
/// Feature-flag management (WP-47): admin-only, resource-independent — role IS the decision.
|
/// Feature-flag management: admin-only, resource-independent — role IS the decision.
|
||||||
public static bool CanManageFeatureFlags(Principal principal) => principal.Role == PrincipalRole.Admin;
|
public static bool CanManageFeatureFlags(Principal principal) => principal.Role == PrincipalRole.Admin;
|
||||||
|
|
||||||
// --- Medewerker (backoffice) capabilities (WP-62, ADR-0002 §3) ------------------------------
|
// --- Medewerker (backoffice) capabilities (ADR-0002 §3) ------------------------------
|
||||||
|
|
||||||
/// May this caller assess/decide an aanvraag (the behandelportal's werkvoorraad + beoordeling,
|
/// May this caller assess/decide an aanvraag (the behandelportal's werkvoorraad + beoordeling)?
|
||||||
/// WP-64/65)? Rol-based, deliberately NOT derived from PrincipalRole — a zorgverlener is false
|
/// Rol-based, deliberately NOT derived from PrincipalRole — a zorgverlener is false
|
||||||
/// regardless of X-Role, because the capability belongs to the medewerker actor kind, not to
|
/// regardless of X-Role, because the capability belongs to the medewerker actor kind, not to
|
||||||
/// the dev role stand-in. Shipped to a frontend only as a decision flag, never as a rollen
|
/// the dev role stand-in. Shipped to a frontend only as a decision flag, never as a rollen
|
||||||
/// matrix (ADR-0001).
|
/// matrix (ADR-0001).
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
namespace BigRegister.Domain.Authorization;
|
namespace BigRegister.Domain.Authorization;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The two actor kinds a request can come from (WP-62, ADR-0002 §3): a <see cref="ZorgverlenerCaller"/>
|
/// The two actor kinds a request can come from (ADR-0002 §3): a <see cref="ZorgverlenerCaller"/>
|
||||||
/// (citizen, WP-53 — subject BSN) or a <see cref="MedewerkerCaller"/> (backoffice employee — no BSN,
|
/// (citizen — subject BSN) or a <see cref="MedewerkerCaller"/> (backoffice employee — no BSN,
|
||||||
/// has rollen). Resolved once per request by <see cref="IIdentityProvider"/> and stashed on
|
/// has rollen). Resolved once per request by <see cref="IIdentityProvider"/> and stashed on
|
||||||
/// <see cref="HttpContext.Items"/> by the identity-resolution middleware (<c>Program.cs</c>, right
|
/// <see cref="HttpContext.Items"/> by the identity-resolution middleware (<c>Program.cs</c>, right
|
||||||
/// after the correlation-id middleware). Everything that used to hardcode <c>DocumentStore.DemoOwner</c>
|
/// after the correlation-id middleware). Everything that used to hardcode <c>DocumentStore.DemoOwner</c>
|
||||||
@@ -33,7 +33,7 @@ public sealed record MedewerkerCaller(
|
|||||||
|
|
||||||
/// <summary>Backoffice functions a medewerker holds (ADR-0002 §4: admin/auditor/institution-rep
|
/// <summary>Backoffice functions a medewerker holds (ADR-0002 §4: admin/auditor/institution-rep
|
||||||
/// slot in here as extra rollen, never as new CallerIdentity variants). Deliberately one member —
|
/// slot in here as extra rollen, never as new CallerIdentity variants). Deliberately one member —
|
||||||
/// WP-65 adds the next one when a capability actually needs it.</summary>
|
/// A later capability adds the next one when it actually needs it.</summary>
|
||||||
public enum MedewerkerRol { Behandelaar }
|
public enum MedewerkerRol { Behandelaar }
|
||||||
|
|
||||||
public static class CallerIdentityHttpContextExtensions
|
public static class CallerIdentityHttpContextExtensions
|
||||||
@@ -51,10 +51,10 @@ public static class CallerIdentityHttpContextExtensions
|
|||||||
: throw new InvalidOperationException(
|
: throw new InvalidOperationException(
|
||||||
"No CallerIdentity resolved for this request — the identity middleware didn't run.");
|
"No CallerIdentity resolved for this request — the identity middleware didn't run.");
|
||||||
|
|
||||||
/// <summary>The citizen-scoped narrowing (WP-62): every SSP endpoint that scopes data by owner
|
/// <summary>The citizen-scoped narrowing: every SSP endpoint that scopes data by owner
|
||||||
/// needs a BSN, which only a zorgverlener has. Throws rather than silently degrading — no
|
/// needs a BSN, which only a zorgverlener has. Throws rather than silently degrading — no
|
||||||
/// medewerker reaches these endpoints today (the behandelportal calls its own endpoints,
|
/// medewerker reaches these endpoints today (the behandelportal calls its own endpoints),
|
||||||
/// WP-64+), so this is a loud "wrong actor kind" bug detector, not a user-facing path.</summary>
|
/// so this is a loud "wrong actor kind" bug detector, not a user-facing path.</summary>
|
||||||
public static ZorgverlenerCaller Zorgverlener(this HttpContext ctx) =>
|
public static ZorgverlenerCaller Zorgverlener(this HttpContext ctx) =>
|
||||||
ctx.Caller() as ZorgverlenerCaller
|
ctx.Caller() as ZorgverlenerCaller
|
||||||
?? throw new InvalidOperationException(
|
?? throw new InvalidOperationException(
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
namespace BigRegister.Domain.Authorization;
|
namespace BigRegister.Domain.Authorization;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolves the acting <see cref="CallerIdentity"/> for a request (WP-53) — one of the two actor
|
/// Resolves the acting <see cref="CallerIdentity"/> for a request — one of the two actor
|
||||||
/// kinds (WP-62, ADR-0002 §3): a zorgverlener (real DigiD claims in production) or a medewerker
|
/// kinds (ADR-0002 §3): a zorgverlener (real DigiD claims in production) or a medewerker
|
||||||
/// (real employee SSO/eHerkenning claims in production). <see cref="StubIdentityProvider"/> is
|
/// (real employee SSO/eHerkenning claims in production). <see cref="StubIdentityProvider"/> is
|
||||||
/// the only implementation today, and is registered only in Development (<c>Program.cs</c>,
|
/// the only implementation today, and is registered only in Development (<c>Program.cs</c>,
|
||||||
/// RB-09/BIO-002).
|
/// BIO-002).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IIdentityProvider
|
public interface IIdentityProvider
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,18 +3,18 @@ using BigRegister.Api.Data;
|
|||||||
namespace BigRegister.Domain.Authorization;
|
namespace BigRegister.Domain.Authorization;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dev stub (WP-53, extended WP-62) — NOT a security boundary, same caveat as
|
/// Dev stub — NOT a security boundary, same caveat as
|
||||||
/// <see cref="Authz.ResolvePrincipal"/> (which this provider now backs). Role comes from the
|
/// <see cref="Authz.ResolvePrincipal"/> (which this provider now backs). Role comes from the
|
||||||
/// existing client-asserted X-Role header (mirrors the FE's <c>?role=</c> toggle) and applies to
|
/// existing client-asserted X-Role header (mirrors the FE's <c>?role=</c> toggle) and applies to
|
||||||
/// either actor kind. Presence of X-Medewerker selects a <see cref="MedewerkerCaller"/> (id +
|
/// either actor kind. Presence of X-Medewerker selects a <see cref="MedewerkerCaller"/> (id +
|
||||||
/// rollen from X-Rollen) and takes precedence over X-Subject; absent — every request today —
|
/// rollen from X-Rollen) and takes precedence over X-Subject; absent — every request today —
|
||||||
/// falls through to the WP-53 <see cref="ZorgverlenerCaller"/> path unchanged: subject BSN from
|
/// falls through to the <see cref="ZorgverlenerCaller"/> path unchanged: subject BSN from
|
||||||
/// X-Subject, defaulting to the single seeded citizen (<see cref="DocumentStore.DemoOwner"/>).
|
/// X-Subject, defaulting to the single seeded citizen (<see cref="DocumentStore.DemoOwner"/>).
|
||||||
/// A real system builds this from verified DigiD claims (zorgverlener) / employee SSO claims
|
/// A real system builds this from verified DigiD claims (zorgverlener) / employee SSO claims
|
||||||
/// (medewerker); every consumer of <see cref="CallerIdentity"/> carries over unchanged once that
|
/// (medewerker); every consumer of <see cref="CallerIdentity"/> carries over unchanged once that
|
||||||
/// swap happens.
|
/// swap happens.
|
||||||
///
|
///
|
||||||
/// Registered only in Development (<c>Program.cs</c>, RB-09/BIO-002) — it always invents a
|
/// Registered only in Development (<c>Program.cs</c>, BIO-002) — it always invents a
|
||||||
/// caller for a request with no credential, which is a deliberate developer convenience, not
|
/// caller for a request with no credential, which is a deliberate developer convenience, not
|
||||||
/// something a production build may do. Its own return type stays non-nullable: unlike
|
/// something a production build may do. Its own return type stays non-nullable: unlike
|
||||||
/// <see cref="IIdentityProvider.Resolve"/>, this stub never has "no identity" to report.
|
/// <see cref="IIdentityProvider.Resolve"/>, this stub never has "no identity" to report.
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ using BigRegister.Domain.Applications;
|
|||||||
namespace BigRegister.Domain.Beoordeling;
|
namespace BigRegister.Domain.Beoordeling;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// SERVER-OWNED rules for the behandelportal's case-treatment decision (WP-65). Used from
|
/// SERVER-OWNED rules for the behandelportal's case-treatment decision. Used from
|
||||||
/// both the beoordeling read side (<see cref="CanDecide"/> backs the `canBesluiten` decision
|
/// both the beoordeling read side (<see cref="CanDecide"/> backs the `canBesluiten` decision
|
||||||
/// flag) and the besluit write side (the SAME `CanDecide` gates the mutation, and — since
|
/// flag) and the besluit write side (the SAME `CanDecide` gates the mutation, and
|
||||||
/// WP-68 F2 — runs inside the write lock, so the two can never drift and a concurrent besluit
|
/// runs inside the write lock, so the two can never drift and a concurrent besluit
|
||||||
/// can't race past the check).
|
/// can't race past the check).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class BeoordelingRules
|
public static class BeoordelingRules
|
||||||
@@ -18,7 +18,7 @@ public static class BeoordelingRules
|
|||||||
current is AanvraagStatusTag.Ingediend or AanvraagStatusTag.InBehandeling
|
current is AanvraagStatusTag.Ingediend or AanvraagStatusTag.InBehandeling
|
||||||
or AanvraagStatusTag.MeerInfoGevraagd;
|
or AanvraagStatusTag.MeerInfoGevraagd;
|
||||||
|
|
||||||
/// <summary>WP-68 F6: moved here from an inline check in the besluit endpoint. The
|
/// <summary>Moved here from an inline check in the besluit endpoint. The
|
||||||
/// toelichting (behandelaar's explanation) is required for every besluit except an
|
/// toelichting (behandelaar's explanation) is required for every besluit except an
|
||||||
/// approval — Afwijzen/MeerInfoOpvragen must justify why (becomes the published status's
|
/// approval — Afwijzen/MeerInfoOpvragen must justify why (becomes the published status's
|
||||||
/// Reden).</summary>
|
/// Reden).</summary>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ public static class DocumentRules
|
|||||||
new DocumentCategory("nascholing", "Nascholingscertificaten",
|
new DocumentCategory("nascholing", "Nascholingscertificaten",
|
||||||
"Upload uw nascholingscertificaten (optioneel).", false, PdfImage, 10, true, true),
|
"Upload uw nascholingscertificaten (optioneel).", false, PdfImage, 10, true, true),
|
||||||
},
|
},
|
||||||
// WP-23: the admin's org-template logo rides the same upload machinery as the
|
// The admin's org-template logo rides the same upload machinery as the
|
||||||
// wizard documents — one category under its own "wizard" id.
|
// wizard documents — one category under its own "wizard" id.
|
||||||
"org-template" => new[]
|
"org-template" => new[]
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ namespace BigRegister.Domain.Intake;
|
|||||||
/// scholing question is required. The frontend receives this value
|
/// scholing question is required. The frontend receives this value
|
||||||
/// (<c>GET /intake/policy</c>) and applies it for instant UX feedback
|
/// (<c>GET /intake/policy</c>) and applies it for instant UX feedback
|
||||||
/// (<c>intake.machine.ts</c>'s <c>lageUren</c>); <see cref="RejectIncompleteScholing"/> is the
|
/// (<c>intake.machine.ts</c>'s <c>lageUren</c>); <see cref="RejectIncompleteScholing"/> is the
|
||||||
/// backend re-validating it as the authority on submit (WP-69) —
|
/// backend re-validating it as the authority on submit —
|
||||||
/// <c>POST /aanvragen/{id}/submit</c> (intake-typed aanvragen only) calls it before
|
/// <c>POST /aanvragen/{id}/submit</c> (intake-typed aanvragen only) calls it before
|
||||||
/// writing anything, and a violation 400s (<c>ProblemDetails</c>), never silently accepts
|
/// writing anything, and a violation 400s (<c>ProblemDetails</c>), never silently accepts
|
||||||
/// an incomplete answer.
|
/// an incomplete answer.
|
||||||
@@ -15,10 +15,10 @@ public static class IntakePolicy
|
|||||||
public const int ScholingThreshold = 1000;
|
public const int ScholingThreshold = 1000;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Completeness rule for the scholing question (WP-69) — not merit: below
|
/// Completeness rule for the scholing question — not merit: below
|
||||||
/// <see cref="ScholingThreshold"/> an answer must be present, but "nee" is a legal answer
|
/// <see cref="ScholingThreshold"/> an answer must be present, but "nee" is a legal answer
|
||||||
/// that still submits (turning "few uren + no scholing" into a rejection is out of scope,
|
/// that still submits (turning "few uren + no scholing" into a rejection is out of scope).
|
||||||
/// see the WP). Three-valued, so two parameters (uren, punten) couldn't express it:
|
/// Three-valued, so two parameters (uren, punten) couldn't express it:
|
||||||
/// <list type="bullet">
|
/// <list type="bullet">
|
||||||
/// <item>below threshold and no answer at all ⇒ incomplete;</item>
|
/// <item>below threshold and no answer at all ⇒ incomplete;</item>
|
||||||
/// <item>answered <c>true</c> (scholing gevolgd) ⇒ punten required and non-negative
|
/// <item>answered <c>true</c> (scholing gevolgd) ⇒ punten required and non-negative
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using BigRegister.Domain.Authorization;
|
|||||||
namespace BigRegister.Domain.Letters;
|
namespace BigRegister.Domain.Letters;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// SERVER-OWNED brief state-transition and authorization rules (RB-30, TE-008). Each
|
/// SERVER-OWNED brief state-transition and authorization rules (TE-008). Each
|
||||||
/// method is a pure decision over (status tag, actor role, entity completeness) —
|
/// method is a pure decision over (status tag, actor role, entity completeness) —
|
||||||
/// extracted out of <see cref="BriefStore"/>'s lock-held, DB-opening methods so the
|
/// extracted out of <see cref="BriefStore"/>'s lock-held, DB-opening methods so the
|
||||||
/// decision can be unit-tested without a booted host or a real SQLite file. Callers
|
/// decision can be unit-tested without a booted host or a real SQLite file. Callers
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ using BigRegister.Api.Data;
|
|||||||
namespace BigRegister.Domain.Letters;
|
namespace BigRegister.Domain.Letters;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Server-rendered letter HTML (WP-25) — the archived, "what is sent" artifact.
|
/// Server-rendered letter HTML — the archived, "what is sent" artifact.
|
||||||
/// Mirrors the FE letter canvas' class vocabulary exactly (<c>public/letter.css</c>,
|
/// Mirrors the FE letter canvas' class vocabulary exactly (<c>public/letter.css</c>,
|
||||||
/// the FE⇄BE contract; LetterHtmlTests' class-parity test is the fence against drift).
|
/// the FE⇄BE contract; LetterHtmlTests' class-parity test is the fence against drift).
|
||||||
///
|
///
|
||||||
@@ -151,7 +151,8 @@ public static class LetterHtml
|
|||||||
private static string EncLines(string s) => Enc(s).Replace("\n", "<br>");
|
private static string EncLines(string s) => Enc(s).Replace("\n", "<br>");
|
||||||
|
|
||||||
// Walks up from the running assembly's own directory (NOT the process cwd, which
|
// Walks up from the running assembly's own directory (NOT the process cwd, which
|
||||||
// varies by how `dotnet run`/docker/tests invoke it — see docs/project/backlog/WP-25) until
|
// varies by how `dotnet run`/docker/tests invoke it — see the letter-preview-html design
|
||||||
|
// note in docs/project/backlog/) until
|
||||||
// it finds `public/letter.css`. docker-compose.yml bind-mounts `./public` under the
|
// it finds `public/letter.css`. docker-compose.yml bind-mounts `./public` under the
|
||||||
// api container's `/src` for exactly this walk to resolve there too.
|
// api container's `/src` for exactly this walk to resolve there too.
|
||||||
private static string FindLetterCss()
|
private static string FindLetterCss()
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ public enum StatusTag
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Status as a closed union: each variant carries exactly the data that makes sense for it
|
/// Status as a closed union: each variant carries exactly the data that makes sense for it.
|
||||||
/// (WP-73). Only <see cref="Geregistreerd"/> carries a herregistratie deadline; only
|
/// Only <see cref="Geregistreerd"/> carries a herregistratie deadline; only
|
||||||
/// <see cref="Geschorst"/> and <see cref="Doorgehaald"/> carry a reden — and there it is
|
/// <see cref="Geschorst"/> and <see cref="Doorgehaald"/> carry a reden — and there it is
|
||||||
/// required, not nullable (the old flat record left <c>Reden</c> nullable on every tag,
|
/// required, not nullable (the old flat record left <c>Reden</c> nullable on every tag,
|
||||||
/// diverging from the frontend union, which has always required it on those two variants —
|
/// diverging from the frontend union, which has always required it on those two variants —
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ public static class SubmissionRules
|
|||||||
|
|
||||||
// RULE: a contact change needs a well-formed Dutch phone number (10 digits, leading
|
// RULE: a contact change needs a well-formed Dutch phone number (10 digits, leading
|
||||||
// 0, formatting stripped). The BRP address is authoritative and cannot be changed
|
// 0, formatting stripped). The BRP address is authoritative and cannot be changed
|
||||||
// here (WP-34), so only the phone is submitted. The server re-validates format
|
// here, so only the phone is submitted. The server re-validates format
|
||||||
// authoritatively (the FE check is UX-only) — and must strip the SAME formatting the
|
// authoritatively (the FE check is UX-only) — and must strip the SAME formatting the
|
||||||
// FE's parseTelefoonnummer does (whitespace/dashes/parens, a leading +31 → 0; WP-75),
|
// FE's parseTelefoonnummer does (whitespace/dashes/parens, a leading +31 → 0),
|
||||||
// or the two sides disagree on what's a valid number.
|
// or the two sides disagree on what's a valid number.
|
||||||
public static string? RejectPhoneChange(string telefoon)
|
public static string? RejectPhoneChange(string telefoon)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -39,20 +39,20 @@ const string SpaCors = "spa";
|
|||||||
builder.Services.AddCors(o => o.AddPolicy(SpaCors, p =>
|
builder.Services.AddCors(o => o.AddPolicy(SpaCors, p =>
|
||||||
p.WithOrigins("http://localhost:4200").AllowAnyHeader().AllowAnyMethod()));
|
p.WithOrigins("http://localhost:4200").AllowAnyHeader().AllowAnyMethod()));
|
||||||
|
|
||||||
// WP-22: the three stores (Applications/Documents/Briefs — Data/*.cs) are static
|
// The three stores (Applications/Documents/Briefs — Data/*.cs) are static
|
||||||
// classes that open their own short-lived AppDbContext per call (see Db.Create),
|
// classes that open their own short-lived AppDbContext per call (see Db.Create),
|
||||||
// not DI-injected, so there's no builder.Services.AddDbContext here. Configuring
|
// not DI-injected, so there's no builder.Services.AddDbContext here. Configuring
|
||||||
// the connection string still goes through IConfiguration so tests/deployments can
|
// the connection string still goes through IConfiguration so tests/deployments can
|
||||||
// override it (ConnectionStrings:AppDb) without touching this file.
|
// override it (ConnectionStrings:AppDb) without touching this file.
|
||||||
Db.ConnectionString = builder.Configuration.GetConnectionString("AppDb") ?? Db.ConnectionString;
|
Db.ConnectionString = builder.Configuration.GetConnectionString("AppDb") ?? Db.ConnectionString;
|
||||||
|
|
||||||
// WP-53 (extended WP-62): the per-request acting caller — resolved once (middleware, below)
|
// The per-request acting caller — resolved once (middleware, below)
|
||||||
// into HttpContext.Items, consumed by Authz.ResolvePrincipal, ZgwTokenProvider.Mint(caller), and
|
// into HttpContext.Items, consumed by Authz.ResolvePrincipal, ZgwTokenProvider.Mint(caller), and
|
||||||
// every store call site that used to hardcode DocumentStore.DemoOwner. Stub today (X-Role/
|
// every store call site that used to hardcode DocumentStore.DemoOwner. Stub today (X-Role/
|
||||||
// X-Subject for a zorgverlener, X-Medewerker/X-Rollen for a medewerker); a real
|
// X-Subject for a zorgverlener, X-Medewerker/X-Rollen for a medewerker); a real
|
||||||
// DigiD/employee-SSO provider swaps in without touching a consumer.
|
// DigiD/employee-SSO provider swaps in without touching a consumer.
|
||||||
//
|
//
|
||||||
// RB-09/BIO-002: StubIdentityProvider invents a citizen identity for any request with no
|
// BIO-002: StubIdentityProvider invents a citizen identity for any request with no
|
||||||
// credential at all — a production behandelportal build sends no X-Medewerker header, so it
|
// credential at all — a production behandelportal build sends no X-Medewerker header, so it
|
||||||
// used to authenticate every request as the seeded citizen (open on that citizen's own rights,
|
// used to authenticate every request as the seeded citizen (open on that citizen's own rights,
|
||||||
// including CanRevealBigNummer). Registering the stub only in Development, and failing to
|
// including CanRevealBigNummer). Registering the stub only in Development, and failing to
|
||||||
@@ -65,10 +65,10 @@ if (builder.Environment.IsDevelopment())
|
|||||||
else if (builder.Environment.IsProduction())
|
else if (builder.Environment.IsProduction())
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
"No IIdentityProvider is registered for a Production environment. StubIdentityProvider " +
|
"No IIdentityProvider is registered for a Production environment. StubIdentityProvider " +
|
||||||
"is Development-only (RB-09/BIO-002); there is no real DigiD/employee-SSO provider in " +
|
"is Development-only (BIO-002); there is no real DigiD/employee-SSO provider in " +
|
||||||
"this POC yet. Register one before deploying to Production.");
|
"this POC yet. Register one before deploying to Production.");
|
||||||
|
|
||||||
// WP-49: the cases (zaken) READ path goes through IZaakSource so a real ZGW backend
|
// The cases (zaken) READ path goes through IZaakSource so a real ZGW backend
|
||||||
// (OpenZaak) can replace the local SQLite store behind the same DTO contract — the FE never
|
// (OpenZaak) can replace the local SQLite store behind the same DTO contract — the FE never
|
||||||
// changes (ADR-0001). Default = LocalZaakSource (offline). Zgw:Enabled=true swaps in the
|
// changes (ADR-0001). Default = LocalZaakSource (offline). Zgw:Enabled=true swaps in the
|
||||||
// OpenZaak client (needs the base URLs + credentials in the Zgw config section).
|
// OpenZaak client (needs the base URLs + credentials in the Zgw config section).
|
||||||
@@ -77,11 +77,11 @@ if (zgw.Enabled)
|
|||||||
{
|
{
|
||||||
builder.Services.AddSingleton(zgw);
|
builder.Services.AddSingleton(zgw);
|
||||||
builder.Services.AddSingleton<ZgwTokenProvider>();
|
builder.Services.AddSingleton<ZgwTokenProvider>();
|
||||||
// WP-60: a bounded client timeout matters once ZgwHttpClient retries — without one, the
|
// A bounded client timeout matters once ZgwHttpClient retries — without one, the
|
||||||
// sources' sync-over-async call (no CancellationToken threaded through) could block a
|
// sources' sync-over-async call (no CancellationToken threaded through) could block a
|
||||||
// thread-pool thread for HttpClient's 100s default times 3 attempts.
|
// thread-pool thread for HttpClient's 100s default times 3 attempts.
|
||||||
var zaakClientBuilder = builder.Services.AddHttpClient<IZaakSource, OpenZaakZaakSource>(c => c.Timeout = TimeSpan.FromSeconds(15));
|
var zaakClientBuilder = builder.Services.AddHttpClient<IZaakSource, OpenZaakZaakSource>(c => c.Timeout = TimeSpan.FromSeconds(15));
|
||||||
// WP-51: the documents (Documenten API / DRC) seam — same pattern as IZaakSource above.
|
// The documents (Documenten API / DRC) seam — same pattern as IZaakSource above.
|
||||||
var documentClientBuilder = builder.Services.AddHttpClient<IDocumentSource, OpenZaakDocumentSource>(c => c.Timeout = TimeSpan.FromSeconds(15));
|
var documentClientBuilder = builder.Services.AddHttpClient<IDocumentSource, OpenZaakDocumentSource>(c => c.Timeout = TimeSpan.FromSeconds(15));
|
||||||
|
|
||||||
// Opt-in diagnostic for the still-unexplained per-container flake (see
|
// Opt-in diagnostic for the still-unexplained per-container flake (see
|
||||||
@@ -101,10 +101,10 @@ else
|
|||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Migrate on every startup, seed nothing (WP-22): unlike SeedData's read-only
|
// Migrate on every startup, seed nothing: unlike SeedData's read-only
|
||||||
// reference fixtures (registration/diplomas/notes — untouched by this WP, still
|
// reference fixtures (registration/diplomas/notes — untouched by this change, still
|
||||||
// static in-memory), Applications/Documents/Briefs never had seed data — they
|
// static in-memory), Applications/Documents/Briefs never had seed data — they
|
||||||
// started empty and accumulated through normal use before this WP too. A fresh
|
// started empty and accumulated through normal use before this change too. A fresh
|
||||||
// SQLite file just starts empty again, same as the old in-memory dictionaries did.
|
// SQLite file just starts empty again, same as the old in-memory dictionaries did.
|
||||||
using (var db = Db.Create())
|
using (var db = Db.Create())
|
||||||
db.Database.Migrate();
|
db.Database.Migrate();
|
||||||
@@ -124,9 +124,9 @@ app.Use(async (ctx, next) =>
|
|||||||
await next(ctx);
|
await next(ctx);
|
||||||
});
|
});
|
||||||
|
|
||||||
// WP-53: resolve the acting citizen once per request, right after correlation — everything
|
// Resolve the acting citizen once per request, right after correlation — everything
|
||||||
// downstream (Authz.ResolvePrincipal, the endpoints below) reads it via ctx.Caller() instead of
|
// downstream (Authz.ResolvePrincipal, the endpoints below) reads it via ctx.Caller() instead of
|
||||||
// re-deriving "who" itself. RB-09/BIO-002: a null resolution is "no identity", not "the seeded
|
// re-deriving "who" itself. BIO-002: a null resolution is "no identity", not "the seeded
|
||||||
// citizen" — this is the one place that turns it into a response (401) rather than letting it
|
// citizen" — this is the one place that turns it into a response (401) rather than letting it
|
||||||
// flow downstream as a silent identity substitution.
|
// flow downstream as a silent identity substitution.
|
||||||
var identityProvider = app.Services.GetRequiredService<IIdentityProvider>();
|
var identityProvider = app.Services.GetRequiredService<IIdentityProvider>();
|
||||||
@@ -142,7 +142,7 @@ app.Use(async (ctx, next) =>
|
|||||||
await next(ctx);
|
await next(ctx);
|
||||||
});
|
});
|
||||||
|
|
||||||
// RB-15/BIO-015: the OpenAPI document + its UI are a genuine attack-surface reduction to
|
// BIO-015: the OpenAPI document + its UI are a genuine attack-surface reduction to
|
||||||
// gate — they enumerate every route, request/response shape and (via SwaggerUI's "Try it
|
// gate — they enumerate every route, request/response shape and (via SwaggerUI's "Try it
|
||||||
// out") let a caller fire requests straight from the browser. Development-only, like the
|
// out") let a caller fire requests straight from the browser. Development-only, like the
|
||||||
// dev-role/scenario-toggle hatches this POC already keeps out of production builds
|
// dev-role/scenario-toggle hatches this POC already keeps out of production builds
|
||||||
@@ -150,8 +150,8 @@ app.Use(async (ctx, next) =>
|
|||||||
// Development). `dotnet swagger tofile` (npm run gen:api) is unaffected: Swashbuckle's CLI
|
// Development). `dotnet swagger tofile` (npm run gen:api) is unaffected: Swashbuckle's CLI
|
||||||
// resolves ISwaggerProvider straight out of the DI container to build swagger.json — it
|
// resolves ISwaggerProvider straight out of the DI container to build swagger.json — it
|
||||||
// never sends an HTTP request through this pipeline, so it never touches this middleware at
|
// never sends an HTTP request through this pipeline, so it never touches this middleware at
|
||||||
// all, gated or not. Verified empirically (see rb-15.md) rather than assumed, per RB-09's
|
// all, gated or not. Verified empirically (see rb-15.md) rather than assumed — a past
|
||||||
// note that this exact file has already broken that tool once.
|
// regression already broke that tool once in this exact file.
|
||||||
if (app.Environment.IsDevelopment())
|
if (app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
app.UseSwagger();
|
app.UseSwagger();
|
||||||
@@ -213,7 +213,7 @@ api.MapGet("/stamdata/{table}", (string table, string? peildatum, HttpContext ct
|
|||||||
var t = StamdataCatalog.Find(table);
|
var t = StamdataCatalog.Find(table);
|
||||||
if (t is null) return Results.NotFound();
|
if (t is null) return Results.NotFound();
|
||||||
DateOnly? peildatumWaarde = null;
|
DateOnly? peildatumWaarde = null;
|
||||||
// RB-16/BIO-019: DateOnly.Parse threw FormatException on unparseable input, surfacing as
|
// BIO-019: DateOnly.Parse threw FormatException on unparseable input, surfacing as
|
||||||
// an unhandled 500 (and, in Development, an exception detail leaked to the caller) — an
|
// an unhandled 500 (and, in Development, an exception detail leaked to the caller) — an
|
||||||
// admin-gated but still user-supplied string needs the same 400 path every other bad-input
|
// admin-gated but still user-supplied string needs the same 400 path every other bad-input
|
||||||
// check in this file uses, not a crash.
|
// check in this file uses, not a crash.
|
||||||
@@ -250,7 +250,7 @@ api.MapGet("/uploads/categories", (string wizardId, string? diplomaHerkomst, str
|
|||||||
|
|
||||||
// Serve stored bytes so a re-opened wizard can preview/download an upload. Inline
|
// Serve stored bytes so a re-opened wizard can preview/download an upload. Inline
|
||||||
// for pdf/image (browser renders it), attachment otherwise (download).
|
// for pdf/image (browser renders it), attachment otherwise (download).
|
||||||
// Scoped like DELETE on the same resource (RB-01/BIO-004): the owning citizen, or a
|
// Scoped like DELETE on the same resource (BIO-004): the owning citizen, or a
|
||||||
// behandelaar reading an aanvraag's linked documents. A foreign id 404s rather than
|
// behandelaar reading an aanvraag's linked documents. A foreign id 404s rather than
|
||||||
// 403s, so the endpoint never confirms that a document id exists.
|
// 403s, so the endpoint never confirms that a document id exists.
|
||||||
api.MapGet("/uploads/{documentId}/content", (string documentId, HttpContext ctx) =>
|
api.MapGet("/uploads/{documentId}/content", (string documentId, HttpContext ctx) =>
|
||||||
@@ -272,7 +272,7 @@ api.MapGet("/uploads/{documentId}/content", (string documentId, HttpContext ctx)
|
|||||||
api.MapGet("/uploads/status", (string? localIds, HttpContext ctx) =>
|
api.MapGet("/uploads/status", (string? localIds, HttpContext ctx) =>
|
||||||
{
|
{
|
||||||
var ids = (localIds ?? "").Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
var ids = (localIds ?? "").Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
// Owner-scoped (RB-01/BIO-004): someone else's localId reads back as "unknown", the
|
// Owner-scoped (BIO-004): someone else's localId reads back as "unknown", the
|
||||||
// same answer an id that never existed gets.
|
// same answer an id that never existed gets.
|
||||||
var found = DocumentStore.ByLocalIds(ids, ctx.Zorgverlener().Bsn).ToDictionary(d => d.LocalId);
|
var found = DocumentStore.ByLocalIds(ids, ctx.Zorgverlener().Bsn).ToDictionary(d => d.LocalId);
|
||||||
var results = ids.Select(id => found.TryGetValue(id, out var d)
|
var results = ids.Select(id => found.TryGetValue(id, out var d)
|
||||||
@@ -301,7 +301,7 @@ api.MapPost("/uploads", async (HttpRequest request, HttpContext ctx, IDocumentSo
|
|||||||
|
|
||||||
using var ms = new MemoryStream();
|
using var ms = new MemoryStream();
|
||||||
await file.CopyToAsync(ms);
|
await file.CopyToAsync(ms);
|
||||||
// WP-51: route through IDocumentSource — LocalDocumentSource is the same DocumentStore.Add
|
// Route through IDocumentSource — LocalDocumentSource is the same DocumentStore.Add
|
||||||
// call this used to make inline; OpenZaakDocumentSource (Zgw:Enabled=true) also registers
|
// call this used to make inline; OpenZaakDocumentSource (Zgw:Enabled=true) also registers
|
||||||
// the file as a DRC enkelvoudiginformatieobject. Response DTO unchanged either way.
|
// the file as a DRC enkelvoudiginformatieobject. Response DTO unchanged either way.
|
||||||
var response = documents.Upload(localId, categoryId, wizardId, file.FileName, file.ContentType, ms.ToArray(), ctx.Zorgverlener());
|
var response = documents.Upload(localId, categoryId, wizardId, file.FileName, file.ContentType, ms.ToArray(), ctx.Zorgverlener());
|
||||||
@@ -325,8 +325,8 @@ api.MapDelete("/uploads/{documentId}", (string documentId, HttpContext ctx) =>
|
|||||||
|
|
||||||
// Admin delete: bypasses ownership, unlinks, and flags the submission for review. Gated
|
// Admin delete: bypasses ownership, unlinks, and flags the submission for review. Gated
|
||||||
// by the same CasesAdmin wrapper (cases:manage) the other admin-cases endpoints use
|
// by the same CasesAdmin wrapper (cases:manage) the other admin-cases endpoints use
|
||||||
// (RB-08/BIO-003) — it used to be gated by a standalone X-Admin header, outside Authz and
|
// (BIO-003) — it used to be gated by a standalone X-Admin header, outside Authz and
|
||||||
// unaudited; CasesAdmin gives it the missing AuthzAuditStore row for free (RB-07).
|
// unaudited; CasesAdmin gives it the missing AuthzAuditStore row for free.
|
||||||
api.MapDelete("/admin/uploads/{documentId}", (string documentId, HttpContext ctx) => CasesAdmin(ctx, () =>
|
api.MapDelete("/admin/uploads/{documentId}", (string documentId, HttpContext ctx) => CasesAdmin(ctx, () =>
|
||||||
DocumentStore.AdminDelete(documentId, "admin") ? Results.NoContent() : Results.NotFound()))
|
DocumentStore.AdminDelete(documentId, "admin") ? Results.NoContent() : Results.NotFound()))
|
||||||
.Gate("CasesAdmin")
|
.Gate("CasesAdmin")
|
||||||
@@ -338,7 +338,7 @@ api.MapDelete("/admin/uploads/{documentId}", (string documentId, HttpContext ctx
|
|||||||
|
|
||||||
// --- reads ---
|
// --- reads ---
|
||||||
|
|
||||||
// WP-53: routed through IZaakSource (like /admin/cases already was) rather than calling
|
// Routed through IZaakSource (like /admin/cases already was) rather than calling
|
||||||
// ApplicationStore directly — under Zgw:Enabled=true a citizen's own dashboard list comes from
|
// ApplicationStore directly — under Zgw:Enabled=true a citizen's own dashboard list comes from
|
||||||
// OpenZaak (BSN-filtered) too, closing the last "reads a static store directly" gap
|
// OpenZaak (BSN-filtered) too, closing the last "reads a static store directly" gap
|
||||||
// openzaak-integration.md's ACL caveat used to flag for this endpoint.
|
// openzaak-integration.md's ACL caveat used to flag for this endpoint.
|
||||||
@@ -356,7 +356,7 @@ api.MapGet("/aanvragen/{id}", (string id, HttpContext ctx) =>
|
|||||||
|
|
||||||
api.MapPost("/aanvragen", (CreateAanvraagRequest req, HttpContext ctx) =>
|
api.MapPost("/aanvragen", (CreateAanvraagRequest req, HttpContext ctx) =>
|
||||||
{
|
{
|
||||||
// Feature flag (WP-47): self-service registration can be closed by an admin.
|
// Feature flag: self-service registration can be closed by an admin.
|
||||||
if (req.Type == "registratie" && !FeatureFlagStore.IsEnabled(FeatureFlags.InschrijvingOpen))
|
if (req.Type == "registratie" && !FeatureFlagStore.IsEnabled(FeatureFlags.InschrijvingOpen))
|
||||||
return Results.Problem(detail: "Inschrijving is momenteel gesloten.", statusCode: StatusCodes.Status403Forbidden);
|
return Results.Problem(detail: "Inschrijving is momenteel gesloten.", statusCode: StatusCodes.Status403Forbidden);
|
||||||
var a = ApplicationStore.CreateConcept(req.Type, ctx.Zorgverlener().Bsn);
|
var a = ApplicationStore.CreateConcept(req.Type, ctx.Zorgverlener().Bsn);
|
||||||
@@ -417,7 +417,7 @@ api.MapPost("/aanvragen/{id}/submit", (string id, AanvraagIndienenRequest req, H
|
|||||||
_ /* herregistratie | intake */ => (SubmissionRules.RejectZeroUren(req.Uren ?? 0), true),
|
_ /* herregistratie | intake */ => (SubmissionRules.RejectZeroUren(req.Uren ?? 0), true),
|
||||||
};
|
};
|
||||||
|
|
||||||
// WP-69: intake-only (herregistratie has no scholing question) — guarded by `reject is
|
// Intake-only (herregistratie has no scholing question) — guarded by `reject is
|
||||||
// null` so a { uren: 0 } submission is still decided on merit (RejectZeroUren) and
|
// null` so a { uren: 0 } submission is still decided on merit (RejectZeroUren) and
|
||||||
// completeness is moot; placed before the document-ownership check and
|
// completeness is moot; placed before the document-ownership check and
|
||||||
// ApplicationStore.Submit so a rejected submit leaves the aanvraag a Concept (retryable).
|
// ApplicationStore.Submit so a rejected submit leaves the aanvraag a Concept (retryable).
|
||||||
@@ -442,13 +442,13 @@ api.MapPost("/aanvragen/{id}/submit", (string id, AanvraagIndienenRequest req, H
|
|||||||
"aanvraag submit id={Id} type={Type} outcome={Outcome} auto={Auto} reference={Reference}",
|
"aanvraag submit id={Id} type={Type} outcome={Outcome} auto={Auto} reference={Reference}",
|
||||||
id, existing.Type, reject is null ? "accepted" : "rejected", autoApprovable, submitted.Referentie);
|
id, existing.Type, reject is null ? "accepted" : "rejected", autoApprovable, submitted.Referentie);
|
||||||
|
|
||||||
// WP-50: route the create through the IZaakSource seam — LocalZaakSource is a passthrough
|
// Route the create through the IZaakSource seam — LocalZaakSource is a passthrough
|
||||||
// of what was computed above; OpenZaakZaakSource (Zgw:Enabled=true) also registers a zaak
|
// of what was computed above; OpenZaakZaakSource (Zgw:Enabled=true) also registers a zaak
|
||||||
// in OpenZaak and maps its result back into this same response shape (ADR-0001/ADR-0005:
|
// in OpenZaak and maps its result back into this same response shape (ADR-0001/ADR-0005:
|
||||||
// zero FE contract change either way). WP-53: the caller is threaded through so the minted
|
// zero FE contract change either way). The caller is threaded through so the minted
|
||||||
// ZGW JWT's user_id/user_representation reflect the acting citizen, not a static config value.
|
// ZGW JWT's user_id/user_representation reflect the acting citizen, not a static config value.
|
||||||
//
|
//
|
||||||
// WP-60: the local submit above already committed — it is never rolled back on a ZGW
|
// The local submit above already committed — it is never rolled back on a ZGW
|
||||||
// failure (an orphan zaak from a rolled-back-then-retried submit is worse than a flagged
|
// failure (an orphan zaak from a rolled-back-then-retried submit is worse than a flagged
|
||||||
// one, see openzaak-integration.md's "Write resilience" section). Each ZGW half is caught
|
// one, see openzaak-integration.md's "Write resilience" section). Each ZGW half is caught
|
||||||
// separately so a create-zaak failure doesn't also skip the (still-local) document link.
|
// separately so a create-zaak failure doesn't also skip the (still-local) document link.
|
||||||
@@ -465,7 +465,7 @@ api.MapPost("/aanvragen/{id}/submit", (string id, AanvraagIndienenRequest req, H
|
|||||||
RecordZgwDivergence(ctx, id, referentie, ex);
|
RecordZgwDivergence(ctx, id, referentie, ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// WP-51: link the submitted documents to the zaak — LocalDocumentSource is exactly the
|
// Link the submitted documents to the zaak — LocalDocumentSource is exactly the
|
||||||
// DocumentStore.Link call this used to make inline; OpenZaakDocumentSource additionally
|
// DocumentStore.Link call this used to make inline; OpenZaakDocumentSource additionally
|
||||||
// POSTs a zaakinformatieobject per document, now that the zaak (zaakUrl) exists.
|
// POSTs a zaakinformatieobject per document, now that the zaak (zaakUrl) exists.
|
||||||
if (documentIds is not null)
|
if (documentIds is not null)
|
||||||
@@ -487,7 +487,7 @@ api.MapPost("/aanvragen/{id}/submit", (string id, AanvraagIndienenRequest req, H
|
|||||||
.ProducesProblem(StatusCodes.Status409Conflict)
|
.ProducesProblem(StatusCodes.Status409Conflict)
|
||||||
.Produces(StatusCodes.Status404NotFound);
|
.Produces(StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
// --- Admin cases (WP-36): cross-owner list + admin delete, gated by `cases:manage`. ---
|
// --- Admin cases: cross-owner list + admin delete, gated by `cases:manage`. ---
|
||||||
|
|
||||||
// --- reads ---
|
// --- reads ---
|
||||||
|
|
||||||
@@ -497,7 +497,7 @@ api.MapGet("/admin/cases", (HttpContext ctx, IZaakSource zaken) => CasesAdmin(ct
|
|||||||
.Produces<List<AanvraagSummaryDto>>()
|
.Produces<List<AanvraagSummaryDto>>()
|
||||||
.ProducesProblem(StatusCodes.Status403Forbidden);
|
.ProducesProblem(StatusCodes.Status403Forbidden);
|
||||||
|
|
||||||
// Queryable authz/PII-reveal audit trail (WP-41) — data-minimised, no PII. Admin-gated
|
// Queryable authz/PII-reveal audit trail — data-minimised, no PII. Admin-gated
|
||||||
// via the existing CasesAdmin (cases:manage); a dedicated audit:read cap is a later refinement.
|
// via the existing CasesAdmin (cases:manage); a dedicated audit:read cap is a later refinement.
|
||||||
api.MapGet("/admin/audit", (HttpContext ctx) => CasesAdmin(ctx, () =>
|
api.MapGet("/admin/audit", (HttpContext ctx) => CasesAdmin(ctx, () =>
|
||||||
Results.Ok(AuthzAuditStore.List()
|
Results.Ok(AuthzAuditStore.List()
|
||||||
@@ -522,9 +522,9 @@ api.MapDelete("/admin/cases/{id}", (string id, HttpContext ctx) => CasesAdmin(ct
|
|||||||
.Produces(StatusCodes.Status404NotFound)
|
.Produces(StatusCodes.Status404NotFound)
|
||||||
.ProducesProblem(StatusCodes.Status403Forbidden);
|
.ProducesProblem(StatusCodes.Status403Forbidden);
|
||||||
|
|
||||||
// --- Werkvoorraad (WP-64): the behandelportal's queue of aanvragen needing treatment. ---
|
// --- Werkvoorraad: the behandelportal's queue of aanvragen needing treatment. ---
|
||||||
// Cross-owner like /admin/cases, but gated by the medewerker capability (`CanBeoordelen`,
|
// Cross-owner like /admin/cases, but gated by the medewerker capability (`CanBeoordelen`)
|
||||||
// WP-62) rather than the admin role, and pre-filtered to the two "still open" status tags —
|
// rather than the admin role, and pre-filtered to the two "still open" status tags —
|
||||||
// a behandelaar never needs to see a Concept (not their business yet) or a terminal case.
|
// a behandelaar never needs to see a Concept (not their business yet) or a terminal case.
|
||||||
api.MapGet("/werkvoorraad", (HttpContext ctx, IZaakSource zaken) => Beoordelen(ctx, "werkvoorraad", () =>
|
api.MapGet("/werkvoorraad", (HttpContext ctx, IZaakSource zaken) => Beoordelen(ctx, "werkvoorraad", () =>
|
||||||
Results.Ok(zaken.ListCases(DateTimeOffset.UtcNow)
|
Results.Ok(zaken.ListCases(DateTimeOffset.UtcNow)
|
||||||
@@ -534,9 +534,9 @@ api.MapGet("/werkvoorraad", (HttpContext ctx, IZaakSource zaken) => Beoordelen(c
|
|||||||
.Produces<List<AanvraagSummaryDto>>()
|
.Produces<List<AanvraagSummaryDto>>()
|
||||||
.ProducesProblem(StatusCodes.Status403Forbidden);
|
.ProducesProblem(StatusCodes.Status403Forbidden);
|
||||||
|
|
||||||
// --- Beoordeling (WP-65): one aanvraag's case-treatment detail — read side only (recording
|
// --- Beoordeling: one aanvraag's case-treatment detail — read side only (recording
|
||||||
// a decision is WP-65's second half). Reads through IZaakSource.ListCases (no new seam method:
|
// a decision is the second half). Reads through IZaakSource.ListCases (no new seam method:
|
||||||
// adding one now would force an OpenZaak get-by-id + mapper, which is WP-66's surface) — O(n)
|
// adding one now would force an OpenZaak get-by-id + mapper, which is a later change's surface) — O(n)
|
||||||
// over a POC-sized table. A Concept isn't a case a behandelaar can treat yet, so it 404s here
|
// over a POC-sized table. A Concept isn't a case a behandelaar can treat yet, so it 404s here
|
||||||
// same as an unknown id (only /aanvragen/{id}, citizen-scoped, shows a Concept).
|
// same as an unknown id (only /aanvragen/{id}, citizen-scoped, shows a Concept).
|
||||||
api.MapGet("/beoordeling/{id}", (string id, HttpContext ctx, IZaakSource zaken) =>
|
api.MapGet("/beoordeling/{id}", (string id, HttpContext ctx, IZaakSource zaken) =>
|
||||||
@@ -546,11 +546,11 @@ api.MapGet("/beoordeling/{id}", (string id, HttpContext ctx, IZaakSource zaken)
|
|||||||
if (c is null || c.Status.Tag == "Concept") return Results.NotFound();
|
if (c is null || c.Status.Tag == "Concept") return Results.NotFound();
|
||||||
var docs = DocumentStore.ByIds(c.DocumentIds)
|
var docs = DocumentStore.ByIds(c.DocumentIds)
|
||||||
.Select(d => new BeoordelingDocumentDto(d.DocumentId, d.CategoryId, d.FileName)).ToList();
|
.Select(d => new BeoordelingDocumentDto(d.DocumentId, d.CategoryId, d.FileName)).ToList();
|
||||||
// Belt and braces: ToAdminSummaryDto already masks the local source (RB-03) and
|
// Belt and braces: ToAdminSummaryDto already masks the local source and
|
||||||
// MaskTail is idempotent, but IZaakSource has a second implementation whose Owner
|
// MaskTail is idempotent, but IZaakSource has a second implementation whose Owner
|
||||||
// is mapped from OpenZaak, so this stays as the guarantee for this response.
|
// is mapped from OpenZaak, so this stays as the guarantee for this response.
|
||||||
var masked = c with { Owner = Pii.MaskTail(c.Owner!, 3) };
|
var masked = c with { Owner = Pii.MaskTail(c.Owner!, 3) };
|
||||||
// WP-68 (F3): non-throwing — c.Status.Tag crosses the IZaakSource wire boundary, so an
|
// Non-throwing — c.Status.Tag crosses the IZaakSource wire boundary, so an
|
||||||
// unrecognised tag degrades to "cannot decide" instead of a 500.
|
// unrecognised tag degrades to "cannot decide" instead of a 500.
|
||||||
var canBesluiten = Enum.TryParse<AanvraagStatusTag>(c.Status.Tag, out var tag) && BeoordelingRules.CanDecide(tag);
|
var canBesluiten = Enum.TryParse<AanvraagStatusTag>(c.Status.Tag, out var tag) && BeoordelingRules.CanDecide(tag);
|
||||||
var decisions = new BeoordelingDecisionsDto(canBesluiten);
|
var decisions = new BeoordelingDecisionsDto(canBesluiten);
|
||||||
@@ -561,13 +561,13 @@ api.MapGet("/beoordeling/{id}", (string id, HttpContext ctx, IZaakSource zaken)
|
|||||||
.ProducesProblem(StatusCodes.Status403Forbidden)
|
.ProducesProblem(StatusCodes.Status403Forbidden)
|
||||||
.Produces(StatusCodes.Status404NotFound);
|
.Produces(StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
// --- Besluit (WP-65b/66): record a behandelaar's decision, advancing the WP-63 status
|
// --- Besluit: record a behandelaar's decision, advancing the status
|
||||||
// lifecycle. The local write runs against ApplicationStore directly (not the IZaakSource
|
// lifecycle. The local write runs against ApplicationStore directly (not the IZaakSource
|
||||||
// seam) — same reasoning as the GET above. The transition-legality check
|
// seam) — same reasoning as the GET above. The transition-legality check
|
||||||
// (BeoordelingRules.CanDecide) is the SAME function the GET's canBesluiten flag uses,
|
// (BeoordelingRules.CanDecide) is the SAME function the GET's canBesluiten flag uses,
|
||||||
// so the two can never drift — and (WP-68 F2) it now runs inside ApplicationStore.RecordBesluit's
|
// so the two can never drift — and it now runs inside ApplicationStore.RecordBesluit's
|
||||||
// write lock rather than here, so two concurrent besluiten can't both pass it before either
|
// write lock rather than here, so two concurrent besluiten can't both pass it before either
|
||||||
// writes. WP-66: once the local decision has committed, IZaakSource also gets a chance to
|
// writes. Once the local decision has committed, IZaakSource also gets a chance to
|
||||||
// advance the ZGW-side zaak status — LocalZaakSource no-ops, OpenZaakZaakSource POSTs a new
|
// advance the ZGW-side zaak status — LocalZaakSource no-ops, OpenZaakZaakSource POSTs a new
|
||||||
// Statussen entry (see its RecordBesluit).
|
// Statussen entry (see its RecordBesluit).
|
||||||
api.MapPost("/beoordeling/{id}/besluit", (string id, RecordBesluitRequest req, HttpContext ctx, IZaakSource zaken) =>
|
api.MapPost("/beoordeling/{id}/besluit", (string id, RecordBesluitRequest req, HttpContext ctx, IZaakSource zaken) =>
|
||||||
@@ -575,12 +575,12 @@ api.MapPost("/beoordeling/{id}/besluit", (string id, RecordBesluitRequest req, H
|
|||||||
{
|
{
|
||||||
if (!Enum.TryParse<Besluit>(req.Besluit, out var besluit))
|
if (!Enum.TryParse<Besluit>(req.Besluit, out var besluit))
|
||||||
return Results.Problem(detail: $"Onbekend besluit '{req.Besluit}'.", statusCode: StatusCodes.Status400BadRequest);
|
return Results.Problem(detail: $"Onbekend besluit '{req.Besluit}'.", statusCode: StatusCodes.Status400BadRequest);
|
||||||
// WP-68 (F6): moved to BeoordelingRules.RequiresToelichting — same rule, now unit-testable.
|
// Moved to BeoordelingRules.RequiresToelichting — same rule, now unit-testable.
|
||||||
if (BeoordelingRules.RequiresToelichting(besluit) && string.IsNullOrWhiteSpace(req.Toelichting))
|
if (BeoordelingRules.RequiresToelichting(besluit) && string.IsNullOrWhiteSpace(req.Toelichting))
|
||||||
return Results.Problem(detail: "Toelichting is verplicht bij dit besluit.", statusCode: StatusCodes.Status400BadRequest);
|
return Results.Problem(detail: "Toelichting is verplicht bij dit besluit.", statusCode: StatusCodes.Status400BadRequest);
|
||||||
|
|
||||||
var now = DateTimeOffset.UtcNow;
|
var now = DateTimeOffset.UtcNow;
|
||||||
// Real bug fix (WP-66): `id` is the FE-facing case id from IZaakSource.ListCases — under
|
// Real bug fix: `id` is the FE-facing case id from IZaakSource.ListCases — under
|
||||||
// OpenZaakZaakSource that's the ZGW zaak's own uuid, not this store's primary key (a
|
// OpenZaakZaakSource that's the ZGW zaak's own uuid, not this store's primary key (a
|
||||||
// ListCases lookup, not ApplicationStore.GetAny(id), same seam the GET sibling above
|
// ListCases lookup, not ApplicationStore.GetAny(id), same seam the GET sibling above
|
||||||
// uses), so resolve the case first and go to the local Aanvraag via its Referentie
|
// uses), so resolve the case first and go to the local Aanvraag via its Referentie
|
||||||
@@ -597,12 +597,12 @@ api.MapPost("/beoordeling/{id}/besluit", (string id, RecordBesluitRequest req, H
|
|||||||
statusCode: StatusCodes.Status409Conflict);
|
statusCode: StatusCodes.Status409Conflict);
|
||||||
|
|
||||||
app.Logger.LogInformation("aanvraag besluit id={Id} besluit={Besluit}", a.Id, besluit);
|
app.Logger.LogInformation("aanvraag besluit id={Id} besluit={Besluit}", a.Id, besluit);
|
||||||
// RB-07/BIO-007: the gate above records that a behandelaar was allowed to act; this
|
// BIO-007: the gate above records that a behandelaar was allowed to act; this
|
||||||
// records what they decided. Without it /beheer/audit cannot answer "who rejected this
|
// records what they decided. Without it /beheer/audit cannot answer "who rejected this
|
||||||
// aanvraag", which is the question the trail exists for.
|
// aanvraag", which is the question the trail exists for.
|
||||||
AuditAuthz(ctx, "aanvraag:besluit", $"aanvraag/{a.Id}/{besluit}", true, Authz.ResolvePrincipal(ctx));
|
AuditAuthz(ctx, "aanvraag:besluit", $"aanvraag/{a.Id}/{besluit}", true, Authz.ResolvePrincipal(ctx));
|
||||||
|
|
||||||
// WP-60: the local decision above already committed — a ZGW failure here is caught and
|
// The local decision above already committed — a ZGW failure here is caught and
|
||||||
// flagged rather than allowed to diverge silently, same handling as submit's create-zaak
|
// flagged rather than allowed to diverge silently, same handling as submit's create-zaak
|
||||||
// and document-link writes.
|
// and document-link writes.
|
||||||
try
|
try
|
||||||
@@ -611,7 +611,7 @@ api.MapPost("/beoordeling/{id}/besluit", (string id, RecordBesluitRequest req, H
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// WP-73: Aanvraag.Decided's Referentie is required/non-null — no `?? a.Id` fallback needed.
|
// Aanvraag.Decided's Referentie is required/non-null — no `?? a.Id` fallback needed.
|
||||||
RecordZgwDivergence(ctx, a.Id, updated!.Referentie, ex);
|
RecordZgwDivergence(ctx, a.Id, updated!.Referentie, ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -625,7 +625,7 @@ api.MapPost("/beoordeling/{id}/besluit", (string id, RecordBesluitRequest req, H
|
|||||||
.Produces(StatusCodes.Status404NotFound);
|
.Produces(StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
// OpenZaak's Notificaties API (NRC) calls this on every zaak event once an `abonnement` is
|
// OpenZaak's Notificaties API (NRC) calls this on every zaak event once an `abonnement` is
|
||||||
// provisioned (WP-52, out-of-band — see openzaak-integration.md, no app code subscribes it).
|
// provisioned (out-of-band — see openzaak-integration.md, no app code subscribes it).
|
||||||
// The caller is NRC, not a user: no Principal, so this audits via AuthzAuditStore directly
|
// The caller is NRC, not a user: no Principal, so this audits via AuthzAuditStore directly
|
||||||
// rather than the Principal-shaped AuditAuthz helper below. A plain shared secret (not a
|
// rather than the Principal-shaped AuditAuthz helper below. A plain shared secret (not a
|
||||||
// JWT — that's only for this BFF's OUTBOUND ZGW calls) compared in fixed time; an unconfigured
|
// JWT — that's only for this BFF's OUTBOUND ZGW calls) compared in fixed time; an unconfigured
|
||||||
@@ -656,7 +656,7 @@ api.MapPost("/zgw/notificaties", (HttpContext ctx, NotificatieDto body) =>
|
|||||||
|
|
||||||
// PRD-0002 §6: coarse, role-derived capabilities for nav/menu-level checks (NOT
|
// PRD-0002 §6: coarse, role-derived capabilities for nav/menu-level checks (NOT
|
||||||
// tied to a specific brief's live status — see BriefDecisionsDto for that).
|
// tied to a specific brief's live status — see BriefDecisionsDto for that).
|
||||||
// WP-64: `aanvraag:beoordelen` is caller-kind-derived (CanBeoordelen), not role-derived like
|
// `aanvraag:beoordelen` is caller-kind-derived (CanBeoordelen), not role-derived like
|
||||||
// the rest of RoleCapabilities — appended here rather than folded into that switch, since it
|
// the rest of RoleCapabilities — appended here rather than folded into that switch, since it
|
||||||
// depends on CallerIdentity (medewerker rollen), not the dev X-Role stand-in.
|
// depends on CallerIdentity (medewerker rollen), not the dev X-Role stand-in.
|
||||||
api.MapGet("/me", (HttpContext ctx) =>
|
api.MapGet("/me", (HttpContext ctx) =>
|
||||||
@@ -667,7 +667,7 @@ api.MapGet("/me", (HttpContext ctx) =>
|
|||||||
})
|
})
|
||||||
.Produces<MeDto>();
|
.Produces<MeDto>();
|
||||||
|
|
||||||
// Feature flags (WP-47). GET is readable by any principal (it drives FE gating); the toggle is
|
// Feature flags. GET is readable by any principal (it drives FE gating); the toggle is
|
||||||
// admin-only. Catalog is code; state is the runtime override in SQLite.
|
// admin-only. Catalog is code; state is the runtime override in SQLite.
|
||||||
api.MapGet("/flags", () =>
|
api.MapGet("/flags", () =>
|
||||||
Results.Ok(FeatureFlagStore.All().Select(f => new FeatureFlagDto(f.Key, f.Description, f.Enabled)).ToList()))
|
Results.Ok(FeatureFlagStore.All().Select(f => new FeatureFlagDto(f.Key, f.Description, f.Enabled)).ToList()))
|
||||||
@@ -690,7 +690,7 @@ api.MapPut("/admin/flags/{key}", (string key, SetFeatureFlagRequest req, HttpCon
|
|||||||
|
|
||||||
api.MapGet("/brief", (HttpContext ctx) =>
|
api.MapGet("/brief", (HttpContext ctx) =>
|
||||||
{
|
{
|
||||||
// RB-23/CQ-007: a read that used to allocate a row on first call. The owner's first
|
// CQ-007: a read that used to allocate a row on first call. The owner's first
|
||||||
// draft now comes only from the explicit POST /brief/reset (BriefStore.ResetAndCreate)
|
// draft now comes only from the explicit POST /brief/reset (BriefStore.ResetAndCreate)
|
||||||
// — this GET is a pure query and 404s when there is nothing to read yet.
|
// — this GET is a pure query and 404s when there is nothing to read yet.
|
||||||
var e = BriefStore.Get(ctx.Zorgverlener().Bsn);
|
var e = BriefStore.Get(ctx.Zorgverlener().Bsn);
|
||||||
@@ -700,13 +700,13 @@ api.MapGet("/brief", (HttpContext ctx) =>
|
|||||||
.Produces<BriefViewDto>()
|
.Produces<BriefViewDto>()
|
||||||
.Produces(StatusCodes.Status404NotFound);
|
.Produces(StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
// Server-rendered HTML preview (WP-25): "what you compose is what is sent" — the
|
// Server-rendered HTML preview: "what you compose is what is sent" — the
|
||||||
// same LetterHtml.Render a sent brief archived. Hand-written on the FE (fetch →
|
// same LetterHtml.Render a sent brief archived. Hand-written on the FE (fetch →
|
||||||
// blob → new tab), so excluded from the OpenAPI doc, same seam as uploads. Sent
|
// blob → new tab), so excluded from the OpenAPI doc, same seam as uploads. Sent
|
||||||
// letters serve their frozen archive; anything else renders live with a watermark.
|
// letters serve their frozen archive; anything else renders live with a watermark.
|
||||||
api.MapGet("/brief/preview", (HttpContext ctx) =>
|
api.MapGet("/brief/preview", (HttpContext ctx) =>
|
||||||
{
|
{
|
||||||
// RB-23: BriefStore.GetOrCreate is gone (split into Get + ResetAndCreate). This GET
|
// BriefStore.GetOrCreate is gone (split into Get + ResetAndCreate). This GET
|
||||||
// must not create a brief as a side effect either, so it 404s under the same
|
// must not create a brief as a side effect either, so it 404s under the same
|
||||||
// precondition as GET /brief — in the running app the FE only reaches this endpoint
|
// precondition as GET /brief — in the running app the FE only reaches this endpoint
|
||||||
// from the brief page, which has already loaded (and, if needed, reset) a brief.
|
// from the brief page, which has already loaded (and, if needed, reset) a brief.
|
||||||
@@ -785,7 +785,7 @@ api.MapPost("/brief/reveal-bignummer", (HttpContext ctx) =>
|
|||||||
var canReveal = Authz.CanRevealBigNummer(principal);
|
var canReveal = Authz.CanRevealBigNummer(principal);
|
||||||
var steppedUp = ctx.Request.Headers["X-Step-Up"] == "true";
|
var steppedUp = ctx.Request.Headers["X-Step-Up"] == "true";
|
||||||
var allowed = canReveal && steppedUp;
|
var allowed = canReveal && steppedUp;
|
||||||
// RB-02/BIO-008: the resource ref is the brief, not the subject — a BSN concatenated
|
// BIO-008: the resource ref is the brief, not the subject — a BSN concatenated
|
||||||
// here lands in a persisted, admin-visible column the "no PII" guarantee covers. One
|
// here lands in a persisted, admin-visible column the "no PII" guarantee covers. One
|
||||||
// brief exists per owner, so the id added nothing the acting principal did not imply.
|
// brief exists per owner, so the id added nothing the acting principal did not imply.
|
||||||
AuditAuthz(ctx, "brief:reveal-bignummer", "brief", allowed, principal);
|
AuditAuthz(ctx, "brief:reveal-bignummer", "brief", allowed, principal);
|
||||||
@@ -810,7 +810,7 @@ api.MapPost("/brief/reset", (HttpContext ctx) =>
|
|||||||
.WithName("briefReset")
|
.WithName("briefReset")
|
||||||
.Produces<BriefViewDto>();
|
.Produces<BriefViewDto>();
|
||||||
|
|
||||||
// --- Organization templates (WP-23): the second template axis — appearance and
|
// --- Organization templates: the second template axis — appearance and
|
||||||
// identity per sub-organization. Admin-only (X-Role: admin, the same dev-stub seam
|
// identity per sub-organization. Admin-only (X-Role: admin, the same dev-stub seam
|
||||||
// as drafter/approver); the same Authz check gates every endpoint and feeds the
|
// as drafter/approver); the same Authz check gates every endpoint and feeds the
|
||||||
// `orgtemplate:edit` capability on /me, so emit and enforce cannot drift. ---
|
// `orgtemplate:edit` capability on /me, so emit and enforce cannot drift. ---
|
||||||
@@ -886,7 +886,7 @@ app.Run();
|
|||||||
// One gate for every org-template endpoint — the enforce twin of the
|
// One gate for every org-template endpoint — the enforce twin of the
|
||||||
// `orgtemplate:edit` capability RoleCapabilities emits (single Authz source).
|
// `orgtemplate:edit` capability RoleCapabilities emits (single Authz source).
|
||||||
//
|
//
|
||||||
// RB-07/BIO-007: every gate below audits the real decision, allow *and* deny. Auditing
|
// BIO-007: every gate below audits the real decision, allow *and* deny. Auditing
|
||||||
// only denials left /beheer/audit able to answer "who was turned away" but not "who
|
// only denials left /beheer/audit able to answer "who was turned away" but not "who
|
||||||
// changed this", which for a register whose integrity is the product is the wrong half
|
// changed this", which for a register whose integrity is the product is the wrong half
|
||||||
// (PRD-0002 §8 lists approvals alongside denials). The allow row is written by the gate,
|
// (PRD-0002 §8 lists approvals alongside denials). The allow row is written by the gate,
|
||||||
@@ -914,7 +914,7 @@ IResult StamdataAdmin(HttpContext ctx, Func<IResult> action)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// One gate for every admin-cases endpoint — the enforce twin of the `cases:manage`
|
// One gate for every admin-cases endpoint — the enforce twin of the `cases:manage`
|
||||||
// capability RoleCapabilities emits (single Authz source, WP-36). A denial is audited.
|
// capability RoleCapabilities emits (single Authz source). A denial is audited.
|
||||||
IResult CasesAdmin(HttpContext ctx, Func<IResult> action)
|
IResult CasesAdmin(HttpContext ctx, Func<IResult> action)
|
||||||
{
|
{
|
||||||
var principal = Authz.ResolvePrincipal(ctx);
|
var principal = Authz.ResolvePrincipal(ctx);
|
||||||
@@ -925,8 +925,8 @@ IResult CasesAdmin(HttpContext ctx, Func<IResult> action)
|
|||||||
statusCode: StatusCodes.Status403Forbidden);
|
statusCode: StatusCodes.Status403Forbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
// One gate for every behandelaar endpoint (werkvoorraad, WP-64; beoordeling detail, WP-65) —
|
// One gate for every behandelaar endpoint (werkvoorraad; beoordeling detail) —
|
||||||
// the enforce twin of `CanBeoordelen` (WP-62). Unlike the other *Admin gates above, this
|
// the enforce twin of `CanBeoordelen`. Unlike the other *Admin gates above, this
|
||||||
// checks the CallerIdentity directly (medewerker rollen), not a role-only Principal — a
|
// checks the CallerIdentity directly (medewerker rollen), not a role-only Principal — a
|
||||||
// zorgverlener with X-Role=admin still gets denied. `resource` feeds the denial's audit row.
|
// zorgverlener with X-Role=admin still gets denied. `resource` feeds the denial's audit row.
|
||||||
IResult Beoordelen(HttpContext ctx, string resource, Func<IResult> action)
|
IResult Beoordelen(HttpContext ctx, string resource, Func<IResult> action)
|
||||||
@@ -938,7 +938,7 @@ IResult Beoordelen(HttpContext ctx, string resource, Func<IResult> action)
|
|||||||
statusCode: StatusCodes.Status403Forbidden);
|
statusCode: StatusCodes.Status403Forbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
// One gate for the feature-flag toggle — the enforce twin of `flags:manage` (WP-47). Takes a
|
// One gate for the feature-flag toggle — the enforce twin of `flags:manage`. Takes a
|
||||||
// per-call `resource` like Beoordelen does, because the toggle endpoint writes no log line of
|
// per-call `resource` like Beoordelen does, because the toggle endpoint writes no log line of
|
||||||
// its own (BIO-007): a bare "feature-flags" row would say a flag changed without saying which,
|
// its own (BIO-007): a bare "feature-flags" row would say a flag changed without saying which,
|
||||||
// and this is the surface CQ-004/ADR-C-009 hinge on.
|
// and this is the surface CQ-004/ADR-C-009 hinge on.
|
||||||
@@ -963,11 +963,11 @@ void AuditAuthz(HttpContext ctx, string action, string resource, bool allowed, P
|
|||||||
app.Logger.LogInformation(
|
app.Logger.LogInformation(
|
||||||
"authz action={Action} resource={Resource} decision={Decision} role={Role} correlationId={Cid}",
|
"authz action={Action} resource={Resource} decision={Decision} role={Role} correlationId={Cid}",
|
||||||
action, resource, allowed ? "allow" : "deny", principal.Role, cid);
|
action, resource, allowed ? "allow" : "deny", principal.Role, cid);
|
||||||
// Persist the queryable, data-minimised trail (WP-41) alongside the log line.
|
// Persist the queryable, data-minimised trail alongside the log line.
|
||||||
AuthzAuditStore.Record(action, resource, allowed, principal.Role.ToString(), cid);
|
AuthzAuditStore.Record(action, resource, allowed, principal.Role.ToString(), cid);
|
||||||
}
|
}
|
||||||
|
|
||||||
// WP-60: the local write already committed — this records that its ZGW counterpart didn't,
|
// The local write already committed — this records that its ZGW counterpart didn't,
|
||||||
// rather than letting the two sides diverge silently (openzaak-integration.md's "Write
|
// rather than letting the two sides diverge silently (openzaak-integration.md's "Write
|
||||||
// resilience" section). Same audit trail AuditAuthz writes to (/beheer/audit), so a
|
// resilience" section). Same audit trail AuditAuthz writes to (/beheer/audit), so a
|
||||||
// divergence is visible next to every other decision, not a separate mechanism.
|
// divergence is visible next to every other decision, not a separate mechanism.
|
||||||
@@ -986,7 +986,7 @@ BriefViewDto ToView(HttpContext ctx, BriefEntity e) => new(
|
|||||||
BriefSeed.PassagesFor(e.Beroep),
|
BriefSeed.PassagesFor(e.Beroep),
|
||||||
Authz.Decisions(Authz.ResolvePrincipal(ctx), e.Status.Tag, e.DrafterId),
|
Authz.Decisions(Authz.ResolvePrincipal(ctx), e.Status.Tag, e.DrafterId),
|
||||||
// Sent letters render with the version pinned at send; everything else follows
|
// Sent letters render with the version pinned at send; everything else follows
|
||||||
// the sub-org's current published template (WP-23 immutability invariant).
|
// the sub-org's current published template (immutability invariant).
|
||||||
OrgTemplateStore.TemplateForBrief(e.SubOrgId, e.Status.Tag == "sent" ? e.SentOrgTemplateVersion : null),
|
OrgTemplateStore.TemplateForBrief(e.SubOrgId, e.Status.Tag == "sent" ? e.SentOrgTemplateVersion : null),
|
||||||
// The case this letter is about — joined from the seeded zorgverlener so the
|
// The case this letter is about — joined from the seeded zorgverlener so the
|
||||||
// behandel scherm can show whom/what it concerns without brief/ importing registratie.
|
// behandel scherm can show whom/what it concerns without brief/ importing registratie.
|
||||||
@@ -1003,9 +1003,9 @@ IResult BriefResult(HttpContext ctx, (BriefStore.Outcome outcome, BriefEntity? e
|
|||||||
_ => Results.Problem(detail: "Ongeldige overgang voor de huidige status van de brief.", statusCode: StatusCodes.Status409Conflict),
|
_ => Results.Problem(detail: "Ongeldige overgang voor de huidige status van de brief.", statusCode: StatusCodes.Status409Conflict),
|
||||||
};
|
};
|
||||||
|
|
||||||
// RB-07/BIO-007: every brief transition already funnelled through here for its log line,
|
// BIO-007: every brief transition already funnelled through here for its log line,
|
||||||
// so the audit row goes here too — a fifth transition cannot be added that logs but leaves
|
// so the audit row goes here too — a fifth transition cannot be added that logs but leaves
|
||||||
// no trail. Resource is the bare "brief" (RB-02: never the owner's BSN); the decision is
|
// no trail. Resource is the bare "brief" (never the owner's BSN); the decision is
|
||||||
// the transition's own outcome, so a 403 or a 409 is as visible as a success.
|
// the transition's own outcome, so a 403 or a 409 is as visible as a success.
|
||||||
void LogBrief(HttpContext ctx, string action, (BriefStore.Outcome outcome, BriefEntity? entity) r)
|
void LogBrief(HttpContext ctx, string action, (BriefStore.Outcome outcome, BriefEntity? entity) r)
|
||||||
{
|
{
|
||||||
@@ -1019,7 +1019,7 @@ void LogBrief(HttpContext ctx, string action, (BriefStore.Outcome outcome, Brief
|
|||||||
// real system ships this to structured logging / an audit store). A repeated
|
// real system ships this to structured logging / an audit store). A repeated
|
||||||
// Idempotency-Key short-circuits to the first call's result — see IdempotencyStore
|
// Idempotency-Key short-circuits to the first call's result — see IdempotencyStore
|
||||||
// — so a retried submit dedupes instead of minting a second reference. The key is
|
// — so a retried submit dedupes instead of minting a second reference. The key is
|
||||||
// scoped to the caller (RB-18/BIO-018): two callers who happen to send the same
|
// scoped to the caller (BIO-018): two callers who happen to send the same
|
||||||
// client-chosen header value do not share a cached result.
|
// client-chosen header value do not share a cached result.
|
||||||
IResult Submit(HttpContext ctx, string kind, string? reject, IReadOnlyList<DocumentRefDto>? documents = null)
|
IResult Submit(HttpContext ctx, string kind, string? reject, IReadOnlyList<DocumentRefDto>? documents = null)
|
||||||
{
|
{
|
||||||
@@ -1062,7 +1062,7 @@ IResult Submit(HttpContext ctx, string kind, string? reject, IReadOnlyList<Docum
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// RB-12/BIO-016: a machine-checkable "this endpoint passes through one of the five admin
|
// BIO-016: a machine-checkable "this endpoint passes through one of the five admin
|
||||||
// authz wrappers" signal, attached at mapping time. It has to be attached here — reflecting
|
// authz wrappers" signal, attached at mapping time. It has to be attached here — reflecting
|
||||||
// over the compiled lambda at test time cannot see which local function a closure calls, but
|
// over the compiled lambda at test time cannot see which local function a closure calls, but
|
||||||
// endpoint metadata set when the route is mapped is exactly what EndpointDataSource exposes
|
// endpoint metadata set when the route is mapped is exactly what EndpointDataSource exposes
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ using System.Text.Json.Serialization;
|
|||||||
namespace BigRegister.Api.Zgw;
|
namespace BigRegister.Api.Zgw;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The Notificaties API (NRC) webhook body (WP-52) — the standard ZGW notification shape POSTed
|
/// The Notificaties API (NRC) webhook body — the standard ZGW notification shape POSTed
|
||||||
/// to a subscribed <c>abonnement</c>'s <c>callbackUrl</c> on every zaak event. Only
|
/// to a subscribed <c>abonnement</c>'s <c>callbackUrl</c> on every zaak event. Only
|
||||||
/// <see cref="HoofdObject"/> (the zaak's URL — not PII) is read today, for the audit trail; the
|
/// <see cref="HoofdObject"/> (the zaak's URL — not PII) is read today, for the audit trail; the
|
||||||
/// rest is parsed because it's the real payload shape a live OpenZaak actually sends, not
|
/// rest is parsed because it's the real payload shape a live OpenZaak actually sends, not
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ using Microsoft.Extensions.Logging;
|
|||||||
namespace BigRegister.Api.Zgw;
|
namespace BigRegister.Api.Zgw;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The <see cref="IDocumentSource"/> backed by a real OpenZaak / ZGW Documenten API (DRC,
|
/// The <see cref="IDocumentSource"/> backed by a real OpenZaak / ZGW Documenten API (DRC).
|
||||||
/// WP-51). An upload always lands locally first (<see cref="DocumentStore"/> stays the record
|
/// An upload always lands locally first (<see cref="DocumentStore"/> stays the record
|
||||||
/// of truth for preview/download/audit, same reasoning as <see cref="OpenZaakZaakSource"/>'s
|
/// of truth for preview/download/audit, same reasoning as <see cref="OpenZaakZaakSource"/>'s
|
||||||
/// dual-write for aanvragen, WP-50) and is then ALSO registered as a DRC
|
/// dual-write for aanvragen) and is then ALSO registered as a DRC
|
||||||
/// enkelvoudiginformatieobject, whose url is persisted (<see cref="DocumentStore.SetDrcUrl"/>)
|
/// enkelvoudiginformatieobject, whose url is persisted (<see cref="DocumentStore.SetDrcUrl"/>)
|
||||||
/// so <see cref="LinkToZaak"/> can find it later without a re-upload. Selected only when
|
/// so <see cref="LinkToZaak"/> can find it later without a re-upload. Selected only when
|
||||||
/// <c>Zgw:Enabled=true</c>; the default stays <see cref="LocalDocumentSource"/>.
|
/// <c>Zgw:Enabled=true</c>; the default stays <see cref="LocalDocumentSource"/>.
|
||||||
@@ -27,7 +27,7 @@ public sealed class OpenZaakDocumentSource(
|
|||||||
{
|
{
|
||||||
private readonly ZgwHttpClient zgw = new(http, tokens);
|
private readonly ZgwHttpClient zgw = new(http, tokens);
|
||||||
|
|
||||||
// WP-59: per-document-type confidentiality (stamdata, ADR-0004) — "openbaar" if the
|
// Per-document-type confidentiality (stamdata, ADR-0004) — "openbaar" if the
|
||||||
// category isn't in the table, so an unconfigured category never fails the upload.
|
// category isn't in the table, so an unconfigured category never fails the upload.
|
||||||
private static readonly IReadOnlyDictionary<string, string> ConfidentialiteitByCategory =
|
private static readonly IReadOnlyDictionary<string, string> ConfidentialiteitByCategory =
|
||||||
StamdataFile.Load<DocumentConfidentialiteit>("documentconfidentialiteit")
|
StamdataFile.Load<DocumentConfidentialiteit>("documentconfidentialiteit")
|
||||||
@@ -44,7 +44,7 @@ public sealed class OpenZaakDocumentSource(
|
|||||||
UploadAsync(localId, categoryId, wizardId, fileName, contentType, content, caller)
|
UploadAsync(localId, categoryId, wizardId, fileName, contentType, content, caller)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
|
|
||||||
// WP-60: once DocumentStore.Add (below) has committed, the local document is the record of
|
// Once DocumentStore.Add (below) has committed, the local document is the record of
|
||||||
// truth (per the class doc above) — a ZGW failure past that point is caught, logged, and
|
// truth (per the class doc above) — a ZGW failure past that point is caught, logged, and
|
||||||
// leaves DrcUrl null rather than throwing. DrcUrl == null is already the meaningful "not
|
// leaves DrcUrl null rather than throwing. DrcUrl == null is already the meaningful "not
|
||||||
// registered in ZGW yet" detector LinkToZaak skips on, so no separate flag column is needed
|
// registered in ZGW yet" detector LinkToZaak skips on, so no separate flag column is needed
|
||||||
@@ -89,7 +89,7 @@ public sealed class OpenZaakDocumentSource(
|
|||||||
/// once a zaak exists, POST a zaakinformatieobject for every document that has a DRC url —
|
/// once a zaak exists, POST a zaakinformatieobject for every document that has a DRC url —
|
||||||
/// documents uploaded before Zgw:Enabled was ever true (or under a config gap) simply have
|
/// documents uploaded before Zgw:Enabled was ever true (or under a config gap) simply have
|
||||||
/// no DrcUrl yet and are skipped, matching "nothing extra to link" for the local case.
|
/// no DrcUrl yet and are skipped, matching "nothing extra to link" for the local case.
|
||||||
/// WP-60: unlike Upload, a ZGW failure here still throws — DocumentStore.Link (the local
|
/// Unlike Upload, a ZGW failure here still throws — DocumentStore.Link (the local
|
||||||
/// half) already ran above, so the caller (Program.cs's submit endpoint) catching this and
|
/// half) already ran above, so the caller (Program.cs's submit endpoint) catching this and
|
||||||
/// recording it as a flagged divergence is what closes the gap, not a try/catch in here.</summary>
|
/// recording it as a flagged divergence is what closes the gap, not a try/catch in here.</summary>
|
||||||
public void LinkToZaak(IReadOnlyList<string> documentIds, string? zaakUrl, CallerIdentity caller)
|
public void LinkToZaak(IReadOnlyList<string> documentIds, string? zaakUrl, CallerIdentity caller)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ public sealed record ZgwPage<T>(
|
|||||||
[property: JsonPropertyName("results")] IReadOnlyList<T> Results);
|
[property: JsonPropertyName("results")] IReadOnlyList<T> Results);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The <see cref="IZaakSource"/> backed by a real OpenZaak / ZGW Zaken API (WP-49 read, WP-50
|
/// The <see cref="IZaakSource"/> backed by a real OpenZaak / ZGW Zaken API (read and
|
||||||
/// write). Reads zaken (following pagination), maps each zaak's zaaktype URL back to the
|
/// write). Reads zaken (following pagination), maps each zaak's zaaktype URL back to the
|
||||||
/// internal aanvraag-type key via <c>Zgw:ZaaktypeUrls</c> (a local lookup — NOT OpenZaak's
|
/// internal aanvraag-type key via <c>Zgw:ZaaktypeUrls</c> (a local lookup — NOT OpenZaak's
|
||||||
/// human zaaktype label, which isn't a value <see cref="AanvraagSummaryDto.Type"/>'s
|
/// human zaaktype label, which isn't a value <see cref="AanvraagSummaryDto.Type"/>'s
|
||||||
@@ -39,7 +39,7 @@ public sealed class OpenZaakZaakSource(HttpClient http, ZgwTokenProvider tokens,
|
|||||||
public IReadOnlyList<AanvraagSummaryDto> ListCases(DateTimeOffset now) =>
|
public IReadOnlyList<AanvraagSummaryDto> ListCases(DateTimeOffset now) =>
|
||||||
ListCasesAsync(bsn: null, caller: null).GetAwaiter().GetResult();
|
ListCasesAsync(bsn: null, caller: null).GetAwaiter().GetResult();
|
||||||
|
|
||||||
/// <summary>WP-53: same read, filtered to one citizen's own zaken via ZGW's rol filter param
|
/// <summary>Same read, filtered to one citizen's own zaken via ZGW's rol filter param
|
||||||
/// (see <see cref="ListCasesAsync"/>) — and minted with that citizen's identity, not the
|
/// (see <see cref="ListCasesAsync"/>) — and minted with that citizen's identity, not the
|
||||||
/// system-level one <see cref="ListCases"/> uses.</summary>
|
/// system-level one <see cref="ListCases"/> uses.</summary>
|
||||||
public IReadOnlyList<AanvraagSummaryDto> ListMyCases(ZorgverlenerCaller caller, DateTimeOffset now) =>
|
public IReadOnlyList<AanvraagSummaryDto> ListMyCases(ZorgverlenerCaller caller, DateTimeOffset now) =>
|
||||||
@@ -81,14 +81,14 @@ public sealed class OpenZaakZaakSource(HttpClient http, ZgwTokenProvider tokens,
|
|||||||
return all;
|
return all;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Write path (WP-50): create a Zaak, then a Status, then a Rol ------------------------
|
// --- Write path: create a Zaak, then a Status, then a Rol ------------------------
|
||||||
|
|
||||||
/// <summary>Create a zaak for a just-submitted aanvraag: POST zaak → resolve + POST the
|
/// <summary>Create a zaak for a just-submitted aanvraag: POST zaak → resolve + POST the
|
||||||
/// initial status → resolve + POST the initiator rol (BSN). Sync-over-async for the same
|
/// initial status → resolve + POST the initiator rol (BSN). Sync-over-async for the same
|
||||||
/// reason as <see cref="ListCases"/> (see the ponytail note there) — a submit is already a
|
/// reason as <see cref="ListCases"/> (see the ponytail note there) — a submit is already a
|
||||||
/// single request/response round trip, so no extra concurrency concern.
|
/// single request/response round trip, so no extra concurrency concern.
|
||||||
///
|
///
|
||||||
/// WP-60: still no compensating transaction — if any call here throws (after
|
/// Still no compensating transaction — if any call here throws (after
|
||||||
/// <see cref="ZgwHttpClient"/>'s retry gives up), the aanvraag stays Submitted locally with
|
/// <see cref="ZgwHttpClient"/>'s retry gives up), the aanvraag stays Submitted locally with
|
||||||
/// no zaak; rolling it back risks an orphan zaak if the failure landed after the zaak POST
|
/// no zaak; rolling it back risks an orphan zaak if the failure landed after the zaak POST
|
||||||
/// succeeded. The caller (Program.cs's submit endpoint) catches this and records it as a
|
/// succeeded. The caller (Program.cs's submit endpoint) catches this and records it as a
|
||||||
@@ -108,7 +108,7 @@ public sealed class OpenZaakZaakSource(HttpClient http, ZgwTokenProvider tokens,
|
|||||||
Bronorganisatie: options.Bronorganisatie,
|
Bronorganisatie: options.Bronorganisatie,
|
||||||
VerantwoordelijkeOrganisatie: options.VerantwoordelijkeOrganisatie,
|
VerantwoordelijkeOrganisatie: options.VerantwoordelijkeOrganisatie,
|
||||||
Startdatum: DateOnly.FromDateTime(now.UtcDateTime),
|
Startdatum: DateOnly.FromDateTime(now.UtcDateTime),
|
||||||
// WP-73: Aanvraag.Submitted's Referentie is a required, non-nullable member — a
|
// Aanvraag.Submitted's Referentie is a required, non-nullable member — a
|
||||||
// just-submitted aanvraag always has one, so there is nothing left to null-check here.
|
// just-submitted aanvraag always has one, so there is nothing left to null-check here.
|
||||||
Identificatie: aanvraag.Referentie), caller);
|
Identificatie: aanvraag.Referentie), caller);
|
||||||
|
|
||||||
@@ -140,19 +140,19 @@ public sealed class OpenZaakZaakSource(HttpClient http, ZgwTokenProvider tokens,
|
|||||||
return first.Url;
|
return first.Url;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Write path (WP-66): record a behandelaar's decision as a new zaak status -----------
|
// --- Write path: record a behandelaar's decision as a new zaak status -----------
|
||||||
|
|
||||||
/// <summary>POST a new Statussen entry to <paramref name="aanvraag"/>'s zaak, carrying the
|
/// <summary>POST a new Statussen entry to <paramref name="aanvraag"/>'s zaak, carrying the
|
||||||
/// besluit (+ toelichting) in <c>statustoelichting</c> — the harness's catalogus (WP-56)
|
/// besluit (+ toelichting) in <c>statustoelichting</c> — the harness's catalogus
|
||||||
/// provisions only a begin/eind statustype pair per zaaktype, not one per decision outcome
|
/// provisions only a begin/eind statustype pair per zaaktype, not one per decision outcome
|
||||||
/// (a real deployment's Besluiten API is future work, see openzaak-integration.md), so this
|
/// (a real deployment's Besluiten API is future work, see openzaak-integration.md), so this
|
||||||
/// reuses the SAME "last statustype" resolution WP-50's create uses for "first", rather than
|
/// reuses the SAME "last statustype" resolution the create path above uses for "first", rather than
|
||||||
/// adding a besluittype abstraction this catalogus doesn't have. No-op if this aanvraag never
|
/// adding a besluittype abstraction this catalogus doesn't have. No-op if this aanvraag never
|
||||||
/// got a zaak (Zgw was off at submit time, or the create diverged) — same "nothing to do"
|
/// got a zaak (Zgw was off at submit time, or the create diverged) — same "nothing to do"
|
||||||
/// skip <see cref="OpenZaakDocumentSource.LinkToZaak"/> uses for a null zaakUrl. Sync-over-async
|
/// skip <see cref="OpenZaakDocumentSource.LinkToZaak"/> uses for a null zaakUrl. Sync-over-async
|
||||||
/// for the same reason as <see cref="CreateZaak"/>.
|
/// for the same reason as <see cref="CreateZaak"/>.
|
||||||
///
|
///
|
||||||
/// WP-60: no compensating transaction here either — the local decision already committed
|
/// No compensating transaction here either — the local decision already committed
|
||||||
/// (<c>ApplicationStore.RecordBesluit</c>, called by the endpoint before this). A failure here
|
/// (<c>ApplicationStore.RecordBesluit</c>, called by the endpoint before this). A failure here
|
||||||
/// is caught by the endpoint and recorded as a flagged divergence (<c>Aanvraag.ZgwError</c>),
|
/// is caught by the endpoint and recorded as a flagged divergence (<c>Aanvraag.ZgwError</c>),
|
||||||
/// the same way the submit endpoint's create-zaak/document writes are.
|
/// the same way the submit endpoint's create-zaak/document writes are.
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ namespace BigRegister.Api.Zgw;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Shared GET/POST-with-Bearer-JWT plumbing for the ZGW source classes. Factored out of
|
/// Shared GET/POST-with-Bearer-JWT plumbing for the ZGW source classes. Factored out of
|
||||||
/// <see cref="OpenZaakZaakSource"/> once <c>OpenZaakDocumentSource</c> (WP-51) needed the
|
/// <see cref="OpenZaakZaakSource"/> once <c>OpenZaakDocumentSource</c> needed the
|
||||||
/// identical auth + JSON + error-handling boilerplate — every ZGW call mints a fresh token
|
/// identical auth + JSON + error-handling boilerplate — every ZGW call mints a fresh token
|
||||||
/// (<see cref="ZgwTokenProvider"/>) and expects/returns JSON. <paramref name="caller"/> is
|
/// (<see cref="ZgwTokenProvider"/>) and expects/returns JSON. <paramref name="caller"/> is
|
||||||
/// optional (WP-53): omitted for calls not tied to one citizen (metadata lookups, the admin
|
/// optional: omitted for calls not tied to one citizen (metadata lookups, the admin
|
||||||
/// cross-owner list), which mint with the BFF's own system identity instead.
|
/// cross-owner list), which mint with the BFF's own system identity instead.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class ZgwHttpClient(HttpClient http, ZgwTokenProvider tokens)
|
internal sealed class ZgwHttpClient(HttpClient http, ZgwTokenProvider tokens)
|
||||||
{
|
{
|
||||||
// WP-60: bounded retry for transport-shaped failures only (gateway restarts, timeouts) —
|
// Bounded retry for transport-shaped failures only (gateway restarts, timeouts) —
|
||||||
// never a substitute for reconciliation. 3 attempts, doubling from 200ms.
|
// never a substitute for reconciliation. 3 attempts, doubling from 200ms.
|
||||||
private const int MaxAttempts = 3;
|
private const int MaxAttempts = 3;
|
||||||
private static readonly TimeSpan BaseDelay = TimeSpan.FromMilliseconds(200);
|
private static readonly TimeSpan BaseDelay = TimeSpan.FromMilliseconds(200);
|
||||||
@@ -73,7 +73,7 @@ internal sealed class ZgwHttpClient(HttpClient http, ZgwTokenProvider tokens)
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// RB-05/BIO-009: path only — no query string, no response-body snippet. The
|
// BIO-009: path only — no query string, no response-body snippet. The
|
||||||
// BSN-filtered zaken list puts a BSN in the query, and OpenZaak echoes the request in
|
// BSN-filtered zaken list puts a BSN in the query, and OpenZaak echoes the request in
|
||||||
// its error bodies, so both used to reach a message Program.cs persists as a flagged
|
// its error bodies, so both used to reach a message Program.cs persists as a flagged
|
||||||
// divergence and writes to the application log. Status + path routes the failure;
|
// divergence and writes to the application log. Status + path routes the failure;
|
||||||
@@ -87,7 +87,7 @@ internal sealed class ZgwHttpClient(HttpClient http, ZgwTokenProvider tokens)
|
|||||||
|
|
||||||
/// <summary>The path without its query string — ZGW filters travel as query parameters and
|
/// <summary>The path without its query string — ZGW filters travel as query parameters and
|
||||||
/// one of them is a BSN (<c>rol__betrokkeneIdentificatie__natuurlijkPersoon__inpBsn</c>), so no
|
/// one of them is a BSN (<c>rol__betrokkeneIdentificatie__natuurlijkPersoon__inpBsn</c>), so no
|
||||||
/// ZGW url may be interpolated into a message that is logged or persisted (RB-05).</summary>
|
/// ZGW url may be interpolated into a message that is logged or persisted.</summary>
|
||||||
private static string Redact(string url) =>
|
private static string Redact(string url) =>
|
||||||
Uri.TryCreate(url, UriKind.Absolute, out var u) ? u.GetLeftPart(UriPartial.Path) : url.Split('?')[0];
|
Uri.TryCreate(url, UriKind.Absolute, out var u) ? u.GetLeftPart(UriPartial.Path) : url.Split('?')[0];
|
||||||
|
|
||||||
@@ -103,8 +103,8 @@ internal sealed class ZgwHttpClient(HttpClient http, ZgwTokenProvider tokens)
|
|||||||
req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||||
// Every ZGW request must declare a coordinate reference system, even when no geometry is
|
// Every ZGW request must declare a coordinate reference system, even when no geometry is
|
||||||
// involved (Zaak has an optional zaakgeometrie) — a real OpenZaak 412s ("Content-Crs
|
// involved (Zaak has an optional zaakgeometrie) — a real OpenZaak 412s ("Content-Crs
|
||||||
// header ontbreekt") without it. Only surfaced by WP-54's live harness: the fixture/stub
|
// header ontbreekt") without it. Only surfaced by a live harness: the fixture/stub
|
||||||
// tests never modelled this header, so this bug shipped unnoticed since WP-49/50.
|
// tests never modelled this header, so this bug shipped unnoticed for a long time.
|
||||||
req.Headers.Add("Accept-Crs", "EPSG:4326");
|
req.Headers.Add("Accept-Crs", "EPSG:4326");
|
||||||
if (req.Content is not null) req.Content.Headers.Add("Content-Crs", "EPSG:4326");
|
if (req.Content is not null) req.Content.Headers.Add("Content-Crs", "EPSG:4326");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
namespace BigRegister.Api.Zgw;
|
namespace BigRegister.Api.Zgw;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Config for connecting to OpenZaak / the ZGW APIs (WP-49), bound from the <c>Zgw</c>
|
/// Config for connecting to OpenZaak / the ZGW APIs, bound from the <c>Zgw</c>
|
||||||
/// section of appsettings. Disabled by default so the POC runs fully offline on the local
|
/// section of appsettings. Disabled by default so the POC runs fully offline on the local
|
||||||
/// SQLite store; set <c>Zgw:Enabled=true</c> (plus the URLs + credentials) to source cases
|
/// SQLite store; set <c>Zgw:Enabled=true</c> (plus the URLs + credentials) to source cases
|
||||||
/// from a real OpenZaak.
|
/// from a real OpenZaak.
|
||||||
///
|
///
|
||||||
/// The ZGW standard is FIVE separate services, each its own base URL — slice 1 (WP-49) only
|
/// The ZGW standard is FIVE separate services, each its own base URL — the first slice only
|
||||||
/// needed the Zaken API (ZRC) and, to resolve human labels for a zaaktype, the Catalogi API
|
/// needed the Zaken API (ZRC) and, to resolve human labels for a zaaktype, the Catalogi API
|
||||||
/// (ZTC). WP-50 (create-zaak) stayed on those two; WP-51 adds the Documenten API (DRC); WP-52
|
/// (ZTC). Create-zaak stayed on those two; a later slice adds the Documenten API (DRC); another
|
||||||
/// adds the Notificaties API (NRC) — inbound only, see <see cref="NotificatieAuthorization"/>.
|
/// adds the Notificaties API (NRC) — inbound only, see <see cref="NotificatieAuthorization"/>.
|
||||||
/// BRC arrives with a later slice, if ever.
|
/// BRC arrives with a later slice, if ever.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -35,32 +35,32 @@ public sealed class ZgwOptions
|
|||||||
public string UserRepresentation { get; init; } = "BIG-register BFF";
|
public string UserRepresentation { get; init; } = "BIG-register BFF";
|
||||||
|
|
||||||
/// <summary>Aanvraag <c>Type</c> (registratie/herregistratie/intake) → zaaktype URL (Catalogi),
|
/// <summary>Aanvraag <c>Type</c> (registratie/herregistratie/intake) → zaaktype URL (Catalogi),
|
||||||
/// so create-zaak (WP-50) knows which zaaktype to open per wizard. OpenZaak validates the URL
|
/// so create-zaak knows which zaaktype to open per wizard. OpenZaak validates the URL
|
||||||
/// by fetching it, so an unconfigured or wrong entry fails loudly at create time.</summary>
|
/// by fetching it, so an unconfigured or wrong entry fails loudly at create time.</summary>
|
||||||
public Dictionary<string, string> ZaaktypeUrls { get; init; } = new();
|
public Dictionary<string, string> ZaaktypeUrls { get; init; } = new();
|
||||||
|
|
||||||
/// <summary>RSIN of the organisation registering the zaak (<c>bronorganisatie</c>, WP-50).</summary>
|
/// <summary>RSIN of the organisation registering the zaak (<c>bronorganisatie</c>).</summary>
|
||||||
public string Bronorganisatie { get; init; } = "";
|
public string Bronorganisatie { get; init; } = "";
|
||||||
|
|
||||||
/// <summary>RSIN of the organisation responsible for the zaak (<c>verantwoordelijkeOrganisatie</c>,
|
/// <summary>RSIN of the organisation responsible for the zaak (<c>verantwoordelijkeOrganisatie</c>)
|
||||||
/// WP-50) — usually the same RSIN as <see cref="Bronorganisatie"/>.</summary>
|
/// — usually the same RSIN as <see cref="Bronorganisatie"/>.</summary>
|
||||||
public string VerantwoordelijkeOrganisatie { get; init; } = "";
|
public string VerantwoordelijkeOrganisatie { get; init; } = "";
|
||||||
|
|
||||||
/// <summary>Documenten API (DRC) base URL, e.g. <c>https://open-zaak.example/documenten/api/v1</c> (WP-51).</summary>
|
/// <summary>Documenten API (DRC) base URL, e.g. <c>https://open-zaak.example/documenten/api/v1</c>.</summary>
|
||||||
public string DrcBaseUrl { get; init; } = "";
|
public string DrcBaseUrl { get; init; } = "";
|
||||||
|
|
||||||
/// <summary>Upload <c>CategoryId</c> (diploma/identiteit/taalvaardigheid/...) → informatieobjecttype
|
/// <summary>Upload <c>CategoryId</c> (diploma/identiteit/taalvaardigheid/...) → informatieobjecttype
|
||||||
/// URL (Catalogi), so create-document (WP-51) knows which type to register per category —
|
/// URL (Catalogi), so create-document knows which type to register per category —
|
||||||
/// the document analogue of <see cref="ZaaktypeUrls"/>.</summary>
|
/// the document analogue of <see cref="ZaaktypeUrls"/>.</summary>
|
||||||
public Dictionary<string, string> InformatieobjecttypeUrls { get; init; } = new();
|
public Dictionary<string, string> InformatieobjecttypeUrls { get; init; } = new();
|
||||||
|
|
||||||
/// <summary>Notificaties API (NRC) base URL (WP-52) — documentation/provisioning only, no
|
/// <summary>Notificaties API (NRC) base URL — documentation/provisioning only, no
|
||||||
/// code in this app calls it: subscribing an <c>abonnement</c> is a one-time out-of-band
|
/// code in this app calls it: subscribing an <c>abonnement</c> is a one-time out-of-band
|
||||||
/// step (see <c>openzaak-integration.md</c>), not something the BFF does at runtime.</summary>
|
/// step (see <c>openzaak-integration.md</c>), not something the BFF does at runtime.</summary>
|
||||||
public string NrcBaseUrl { get; init; } = "";
|
public string NrcBaseUrl { get; init; } = "";
|
||||||
|
|
||||||
/// <summary>The exact <c>Authorization</c> header value NRC must send on every
|
/// <summary>The exact <c>Authorization</c> header value NRC must send on every
|
||||||
/// <c>POST /zgw/notificaties</c> callback (WP-52) — a plain shared secret set into the
|
/// <c>POST /zgw/notificaties</c> callback — a plain shared secret set into the
|
||||||
/// <c>abonnement</c>'s <c>auth</c> field when provisioning, not a JWT. Empty (the default)
|
/// <c>abonnement</c>'s <c>auth</c> field when provisioning, not a JWT. Empty (the default)
|
||||||
/// means every notification is rejected — an unconfigured secret must never mean "accept
|
/// means every notification is rejected — an unconfigured secret must never mean "accept
|
||||||
/// anything".</summary>
|
/// anything".</summary>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ using BigRegister.Domain.Authorization;
|
|||||||
namespace BigRegister.Api.Zgw;
|
namespace BigRegister.Api.Zgw;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Mints the JWT that authenticates the BFF to the ZGW APIs (WP-49). OpenZaak expects a
|
/// Mints the JWT that authenticates the BFF to the ZGW APIs. OpenZaak expects a
|
||||||
/// short-lived HS256 assertion signed with the client secret, carrying <c>iss</c>/
|
/// short-lived HS256 assertion signed with the client secret, carrying <c>iss</c>/
|
||||||
/// <c>client_id</c> (identity), <c>iat</c> (issued-at), and <c>user_id</c>/
|
/// <c>client_id</c> (identity), <c>iat</c> (issued-at), and <c>user_id</c>/
|
||||||
/// <c>user_representation</c> (for the ZGW audit trail). There is no OAuth refresh dance —
|
/// <c>user_representation</c> (for the ZGW audit trail). There is no OAuth refresh dance —
|
||||||
@@ -23,12 +23,12 @@ public sealed class ZgwTokenProvider(ZgwOptions options)
|
|||||||
/// specific citizen (e.g. the admin cross-owner <c>ListCases</c>).</summary>
|
/// specific citizen (e.g. the admin cross-owner <c>ListCases</c>).</summary>
|
||||||
public string Mint() => MintCore(options.UserId, options.UserRepresentation);
|
public string Mint() => MintCore(options.UserId, options.UserRepresentation);
|
||||||
|
|
||||||
/// <summary>Per-request variant (WP-53, extended WP-62): the ZGW audit trail (<c>user_id</c>/
|
/// <summary>Per-request variant: the ZGW audit trail (<c>user_id</c>/
|
||||||
/// <c>user_representation</c>) reflects the acting caller instead of this BFF's static
|
/// <c>user_representation</c>) reflects the acting caller instead of this BFF's static
|
||||||
/// config identity, for any call made on a specific caller's behalf (create zaak, upload,
|
/// config identity, for any call made on a specific caller's behalf (create zaak, upload,
|
||||||
/// link, citizen-scoped list). <see cref="CallerIdentity.SubjectId"/> is the BSN for a
|
/// link, citizen-scoped list). <see cref="CallerIdentity.SubjectId"/> is the BSN for a
|
||||||
/// zorgverlener or the medewerkerId for a medewerker (WP-66 mints this for a besluit write
|
/// zorgverlener or the medewerkerId for a medewerker (a besluit write mints this the
|
||||||
/// the same way, with no further change needed here).</summary>
|
/// same way, with no further change needed here).</summary>
|
||||||
public string Mint(CallerIdentity caller) => MintCore(caller.SubjectId, caller.DisplayName);
|
public string Mint(CallerIdentity caller) => MintCore(caller.SubjectId, caller.DisplayName);
|
||||||
|
|
||||||
private string MintCore(string userId, string userRepresentation)
|
private string MintCore(string userId, string userRepresentation)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ public sealed record ZgwZaak(
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Anti-corruption map: ZGW Zaak → the existing <see cref="AanvraagSummaryDto"/> the FE
|
/// Anti-corruption map: ZGW Zaak → the existing <see cref="AanvraagSummaryDto"/> the FE
|
||||||
/// already renders (WP-49). This is where "URL as identity" and the cross-service zaaktype
|
/// already renders. This is where "URL as identity" and the cross-service zaaktype
|
||||||
/// join get flattened away, so nothing downstream (the FE) sees ZGW shapes.
|
/// join get flattened away, so nothing downstream (the FE) sees ZGW shapes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class ZgwZaakMapper
|
public static class ZgwZaakMapper
|
||||||
@@ -45,7 +45,7 @@ public static class ZgwZaakMapper
|
|||||||
Id: Uuid(z.Url),
|
Id: Uuid(z.Url),
|
||||||
Type: zaaktypeLabel,
|
Type: zaaktypeLabel,
|
||||||
Status: status,
|
Status: status,
|
||||||
DocumentIds: Array.Empty<string>(), // zaak↔document links arrive with WP-51 (DRC)
|
DocumentIds: Array.Empty<string>(), // zaak↔document links arrive with a later slice (DRC)
|
||||||
CreatedAt: created,
|
CreatedAt: created,
|
||||||
UpdatedAt: updated,
|
UpdatedAt: updated,
|
||||||
SubmittedAt: created,
|
SubmittedAt: created,
|
||||||
@@ -57,7 +57,7 @@ public static class ZgwZaakMapper
|
|||||||
private static string Iso(DateOnly d) =>
|
private static string Iso(DateOnly d) =>
|
||||||
d.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc).ToString("o");
|
d.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc).ToString("o");
|
||||||
|
|
||||||
/// <summary>Status for a zaak that was JUST created (WP-50) — always the open/InBehandeling
|
/// <summary>Status for a zaak that was JUST created — always the open/InBehandeling
|
||||||
/// coarse status (no einddatum yet), same convention as <see cref="ToSummaryDto"/>.</summary>
|
/// coarse status (no einddatum yet), same convention as <see cref="ToSummaryDto"/>.</summary>
|
||||||
public static AanvraagStatusDto ToCreatedStatusDto(string identificatie) =>
|
public static AanvraagStatusDto ToCreatedStatusDto(string identificatie) =>
|
||||||
AanvraagStatus.InBehandeling(identificatie, manual: true).ToDto();
|
AanvraagStatus.InBehandeling(identificatie, manual: true).ToDto();
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
"_Zgw": "WP-49..52: set Enabled=true + the URLs/credentials/RSINs/type maps to source, create and document cases against a real OpenZaak; NrcBaseUrl/NotificatieAuthorization configure the inbound notificaties webhook. Off = local SQLite store (offline POC default).",
|
"_Zgw": "Set Enabled=true + the URLs/credentials/RSINs/type maps to source, create and document cases against a real OpenZaak; NrcBaseUrl/NotificatieAuthorization configure the inbound notificaties webhook. Off = local SQLite store (offline POC default).",
|
||||||
"Zgw": {
|
"Zgw": {
|
||||||
"Enabled": false,
|
"Enabled": false,
|
||||||
"ZrcBaseUrl": "",
|
"ZrcBaseUrl": "",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ public class AanvraagTests(TestWebApplicationFactory factory) : IClassFixture<Te
|
|||||||
|
|
||||||
private async Task<AanvraagDetailDto> Create(string type = "registratie")
|
private async Task<AanvraagDetailDto> Create(string type = "registratie")
|
||||||
{
|
{
|
||||||
// WP-35: one Concept per type is now server-enforced, and these tests share one DB
|
// One Concept per type is now server-enforced, and these tests share one DB
|
||||||
// (IClassFixture). Clear any leftover Concept so each test starts from a clean slate.
|
// (IClassFixture). Clear any leftover Concept so each test starts from a clean slate.
|
||||||
var existing = await List();
|
var existing = await List();
|
||||||
Assert.NotNull(existing);
|
Assert.NotNull(existing);
|
||||||
@@ -103,7 +103,7 @@ public class AanvraagTests(TestWebApplicationFactory factory) : IClassFixture<Te
|
|||||||
Assert.Equal(HttpStatusCode.Conflict, again.StatusCode);
|
Assert.Equal(HttpStatusCode.Conflict, again.StatusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- WP-35: one Concept per case type (server-enforced) ---
|
// --- One Concept per case type (server-enforced) ---
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Creating_a_second_concept_of_the_same_type_conflicts()
|
public async Task Creating_a_second_concept_of_the_same_type_conflicts()
|
||||||
@@ -146,7 +146,7 @@ public class AanvraagTests(TestWebApplicationFactory factory) : IClassFixture<Te
|
|||||||
Assert.Equal(HttpStatusCode.Conflict, (await _client.DeleteAsync($"/api/v1/aanvragen/{a.Id}")).StatusCode);
|
Assert.Equal(HttpStatusCode.Conflict, (await _client.DeleteAsync($"/api/v1/aanvragen/{a.Id}")).StatusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- WP-53: citizen-scoping — GET /aanvragen must never leak across identities. ---
|
// --- Citizen-scoping — GET /aanvragen must never leak across identities. ---
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Applications_are_scoped_to_the_caller_bsn()
|
public async Task Applications_are_scoped_to_the_caller_bsn()
|
||||||
@@ -181,7 +181,7 @@ public class AanvraagTests(TestWebApplicationFactory factory) : IClassFixture<Te
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- WP-68 (F1): a citizen may only reference their own uploads — submit/draft-sync must
|
// --- A citizen may only reference their own uploads — submit/draft-sync must
|
||||||
// reject a foreign documentId rather than silently attaching it. ---
|
// reject a foreign documentId rather than silently attaching it. ---
|
||||||
|
|
||||||
private static async Task<UploadResponse> UploadAs(HttpClient client, string owner, string localId)
|
private static async Task<UploadResponse> UploadAs(HttpClient client, string owner, string localId)
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ using BigRegister.Tests.Builders;
|
|||||||
namespace BigRegister.Tests.Acceptance;
|
namespace BigRegister.Tests.Acceptance;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Behaviour-level tests for the besluit lifecycle (WP-65b/66/68), built through the
|
/// Behaviour-level tests for the besluit lifecycle, built through the
|
||||||
/// <see cref="Given"/> type-state builder (WP-70) rather than the full wizard/upload dance
|
/// <see cref="Given"/> type-state builder rather than the full wizard/upload dance
|
||||||
/// <see cref="BeoordelingTests"/> uses — a fixture that's already Submitted (or already
|
/// <see cref="BeoordelingTests"/> uses — a fixture that's already Submitted (or already
|
||||||
/// Decided) is a two-line Given, not fifteen. Each test persists its own Given-built
|
/// Decided) is a two-line Given, not fifteen. Each test persists its own Given-built
|
||||||
/// <see cref="Aanvraag"/> straight into the isolated per-class SQLite file (no HTTP round trip
|
/// <see cref="Aanvraag"/> straight into the isolated per-class SQLite file (no HTTP round trip
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ using BigRegister.Tests.Builders;
|
|||||||
namespace BigRegister.Tests.Acceptance;
|
namespace BigRegister.Tests.Acceptance;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Behaviour-level tests for the scholing-threshold enforcement (WP-69) over
|
/// Behaviour-level tests for the scholing-threshold enforcement over
|
||||||
/// <c>POST /aanvragen/{id}/submit</c> (the wizard's real path — WP-72 deleted the legacy
|
/// <c>POST /aanvragen/{id}/submit</c> (the wizard's real path — a later change deleted the legacy
|
||||||
/// <c>POST /intakes</c> endpoint this once also covered). Built through the <see
|
/// <c>POST /intakes</c> endpoint this once also covered). Built through the <see
|
||||||
/// cref="Given"/> type-state builder, mirroring <see cref="BesluitLifecycleTests"/> rather
|
/// cref="Given"/> type-state builder, mirroring <see cref="BesluitLifecycleTests"/> rather
|
||||||
/// than the full wizard/upload dance — the builder's default owner IS <see
|
/// than the full wizard/upload dance — the builder's default owner IS <see
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using BigRegister.Api.Contracts;
|
|||||||
namespace BigRegister.Tests.Acceptance;
|
namespace BigRegister.Tests.Acceptance;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Contract test for the FE/BE seam on phone-number stripping (WP-75). Both sides share
|
/// Contract test for the FE/BE seam on phone-number stripping. Both sides share
|
||||||
/// the same format regex (<c>^0\d{9}$</c>) but, until this test, diverged on what they
|
/// the same format regex (<c>^0\d{9}$</c>) but, until this test, diverged on what they
|
||||||
/// strip before checking it: the FE's <c>parseTelefoonnummer</c>
|
/// strip before checking it: the FE's <c>parseTelefoonnummer</c>
|
||||||
/// (registratie/domain/value-objects/telefoonnummer.ts) also drops parentheses and maps a
|
/// (registratie/domain/value-objects/telefoonnummer.ts) also drops parentheses and maps a
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Mvc.Testing;
|
|||||||
|
|
||||||
namespace BigRegister.Tests;
|
namespace BigRegister.Tests;
|
||||||
|
|
||||||
/// WP-36: admin cross-owner case list + admin delete, gated by `cases:manage`.
|
/// Admin cross-owner case list + admin delete, gated by `cases:manage`.
|
||||||
public class AdminCasesTests(TestWebApplicationFactory factory) : IClassFixture<TestWebApplicationFactory>
|
public class AdminCasesTests(TestWebApplicationFactory factory) : IClassFixture<TestWebApplicationFactory>
|
||||||
{
|
{
|
||||||
private readonly HttpClient _client = factory.CreateClient();
|
private readonly HttpClient _client = factory.CreateClient();
|
||||||
@@ -35,7 +35,7 @@ public class AdminCasesTests(TestWebApplicationFactory factory) : IClassFixture<
|
|||||||
list.EnsureSuccessStatusCode();
|
list.EnsureSuccessStatusCode();
|
||||||
var cases = (await list.Content.ReadFromJsonAsync<List<AanvraagSummaryDto>>())!;
|
var cases = (await list.Content.ReadFromJsonAsync<List<AanvraagSummaryDto>>())!;
|
||||||
var mine = cases.Single(x => x.Id == a.Id);
|
var mine = cases.Single(x => x.Id == a.Id);
|
||||||
// RB-03/BIO-003: the owner is carried, but masked — it is a BSN, and this list is
|
// BIO-003: the owner is carried, but masked — it is a BSN, and this list is
|
||||||
// read by someone who is not the subject.
|
// read by someone who is not the subject.
|
||||||
Assert.Equal("******782", mine.Owner);
|
Assert.Equal("******782", mine.Owner);
|
||||||
Assert.DoesNotContain(DocumentStore.DemoOwner, mine.Owner);
|
Assert.DoesNotContain(DocumentStore.DemoOwner, mine.Owner);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ using Microsoft.AspNetCore.Mvc.Testing;
|
|||||||
|
|
||||||
namespace BigRegister.Tests;
|
namespace BigRegister.Tests;
|
||||||
|
|
||||||
/// WP-41: the persisted authz/PII-reveal audit trail is queryable, data-minimised (no PII).
|
/// The persisted authz/PII-reveal audit trail is queryable, data-minimised (no PII).
|
||||||
public class AuthzAuditTests(TestWebApplicationFactory factory) : IClassFixture<TestWebApplicationFactory>
|
public class AuthzAuditTests(TestWebApplicationFactory factory) : IClassFixture<TestWebApplicationFactory>
|
||||||
{
|
{
|
||||||
private readonly HttpClient _client = factory.CreateClient();
|
private readonly HttpClient _client = factory.CreateClient();
|
||||||
@@ -59,7 +59,7 @@ public class AuthzAuditTests(TestWebApplicationFactory factory) : IClassFixture<
|
|||||||
Assert.Contains(await AuditLog(), e => e.Action == "brief:reveal-bignummer");
|
Assert.Contains(await AuditLog(), e => e.Action == "brief:reveal-bignummer");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RB-07/BIO-007: the trail used to record only denials, so `/beheer/audit` could answer
|
/// BIO-007: the trail used to record only denials, so `/beheer/audit` could answer
|
||||||
/// "who was turned away" but not "who changed this" — for a register whose integrity is the
|
/// "who was turned away" but not "who changed this" — for a register whose integrity is the
|
||||||
/// product, the wrong half. Every gate now audits the real decision.
|
/// product, the wrong half. Every gate now audits the real decision.
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -69,10 +69,10 @@ public class AuthzAuditTests(TestWebApplicationFactory factory) : IClassFixture<
|
|||||||
Assert.Contains(await AuditLog(), e => e.Action == "cases:manage" && e.Decision == "allow" && e.Role == "Admin");
|
Assert.Contains(await AuditLog(), e => e.Action == "cases:manage" && e.Decision == "allow" && e.Role == "Admin");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RB-08/BIO-003: the admin upload delete used to be gated by a standalone X-Admin
|
/// BIO-003: the admin upload delete used to be gated by a standalone X-Admin
|
||||||
/// header, outside Authz and writing no AuthzAuditStore row at all. Routing it through
|
/// header, outside Authz and writing no AuthzAuditStore row at all. Routing it through
|
||||||
/// CasesAdmin (cases:manage) gives it the same allow-path row every other admin-cases
|
/// CasesAdmin (cases:manage) gives it the same allow-path row every other admin-cases
|
||||||
/// endpoint gets, for free, per RB-07. `CasesAdmin` audits under a fixed "cases"
|
/// endpoint gets, for free. `CasesAdmin` audits under a fixed "cases"
|
||||||
/// resource shared with the other admin-cases endpoints, so this asserts a **count**
|
/// resource shared with the other admin-cases endpoints, so this asserts a **count**
|
||||||
/// increase — reading the store directly (not via `GET /admin/audit`, itself a
|
/// increase — reading the store directly (not via `GET /admin/audit`, itself a
|
||||||
/// `CasesAdmin` endpoint that would write its own row and confound the count) —
|
/// `CasesAdmin` endpoint that would write its own row and confound the count) —
|
||||||
@@ -120,7 +120,7 @@ public class AuthzAuditTests(TestWebApplicationFactory factory) : IClassFixture<
|
|||||||
Assert.Contains(await AuditLog(), e => e.Action == "brief:submit" && e.Decision == "deny");
|
Assert.Contains(await AuditLog(), e => e.Action == "brief:submit" && e.Decision == "deny");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RB-02/BIO-008: the schema test below asserts on **column names**, so a BSN inside a
|
/// BIO-008: the schema test below asserts on **column names**, so a BSN inside a
|
||||||
/// column called `Resource` was invisible to it — and one was there, concatenated as
|
/// column called `Resource` was invisible to it — and one was there, concatenated as
|
||||||
/// `"brief/" + Bsn`. This asserts on the stored **values** instead. Four documents
|
/// `"brief/" + Bsn`. This asserts on the stored **values** instead. Four documents
|
||||||
/// promise this trail holds no PII; this is the test that makes the promise checkable.
|
/// promise this trail holds no PII; this is the test that makes the promise checkable.
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ public class AuthzTests
|
|||||||
Assert.False(Authz.Decisions(Approver, "draft", DrafterId).CanRevealBigNummer);
|
Assert.False(Authz.Decisions(Approver, "draft", DrafterId).CanRevealBigNummer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- CanBeoordelen (WP-62) --------------------------------------------------------------
|
// --- CanBeoordelen --------------------------------------------------------------
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CanBeoordelen_true_for_a_medewerker_with_the_behandelaar_rol()
|
public void CanBeoordelen_true_for_a_medewerker_with_the_behandelaar_rol()
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ using Microsoft.AspNetCore.Mvc.Testing;
|
|||||||
|
|
||||||
namespace BigRegister.Tests;
|
namespace BigRegister.Tests;
|
||||||
|
|
||||||
/// WP-65 (read side): one aanvraag's case-treatment detail, gated by the same medewerker
|
/// One aanvraag's case-treatment detail (read side), gated by the same medewerker
|
||||||
/// capability (`CanBeoordelen`, WP-62) as the werkvoorraad list (WP-64).
|
/// capability (`CanBeoordelen`) as the werkvoorraad list.
|
||||||
public class BeoordelingTests(TestWebApplicationFactory factory) : IClassFixture<TestWebApplicationFactory>
|
public class BeoordelingTests(TestWebApplicationFactory factory) : IClassFixture<TestWebApplicationFactory>
|
||||||
{
|
{
|
||||||
private readonly HttpClient _client = factory.CreateClient();
|
private readonly HttpClient _client = factory.CreateClient();
|
||||||
@@ -152,7 +152,7 @@ public class BeoordelingTests(TestWebApplicationFactory factory) : IClassFixture
|
|||||||
Assert.Equal("Goedgekeurd", view.Aanvraag.Status.Tag);
|
Assert.Equal("Goedgekeurd", view.Aanvraag.Status.Tag);
|
||||||
Assert.False(view.Decisions.CanBesluiten); // terminal — no further decision allowed
|
Assert.False(view.Decisions.CanBesluiten); // terminal — no further decision allowed
|
||||||
|
|
||||||
// RB-07/BIO-007: the gate records that a behandelaar was allowed to act; this records
|
// BIO-007: the gate records that a behandelaar was allowed to act; this records
|
||||||
// what they decided, which is the question /beheer/audit exists to answer.
|
// what they decided, which is the question /beheer/audit exists to answer.
|
||||||
Assert.Contains(AuthzAuditStore.List(), e =>
|
Assert.Contains(AuthzAuditStore.List(), e =>
|
||||||
e.Action == "aanvraag:besluit" && e.Decision == "allow" &&
|
e.Action == "aanvraag:besluit" && e.Decision == "allow" &&
|
||||||
@@ -222,7 +222,7 @@ public class BeoordelingTests(TestWebApplicationFactory factory) : IClassFixture
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WP-68 (F2): the transition-legality check now runs inside RecordBesluit's write lock, so
|
// The transition-legality check now runs inside RecordBesluit's write lock, so
|
||||||
// two besluiten racing on the same still-open aanvraag can't both pass the check before
|
// two besluiten racing on the same still-open aanvraag can't both pass the check before
|
||||||
// either writes — exactly one commits, the other sees the now-terminal status.
|
// either writes — exactly one commits, the other sees the now-terminal status.
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ public class BriefEndpointTests(TestWebApplicationFactory factory) : IClassFixtu
|
|||||||
return new SaveBriefRequest(sections);
|
return new SaveBriefRequest(sections);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RB-23: `GET /brief` no longer seeds a brief on first call, so every test that
|
/// `GET /brief` no longer seeds a brief on first call, so every test that
|
||||||
/// needs one present creates it explicitly through `POST /brief/reset`
|
/// needs one present creates it explicitly through `POST /brief/reset`
|
||||||
/// (`BriefStore.ResetAndCreate`) — the same command the "start over" affordance uses.
|
/// (`BriefStore.ResetAndCreate`) — the same command the "start over" affordance uses.
|
||||||
private async Task<BriefDto> SeedBrief()
|
private async Task<BriefDto> SeedBrief()
|
||||||
@@ -48,7 +48,7 @@ public class BriefEndpointTests(TestWebApplicationFactory factory) : IClassFixtu
|
|||||||
return req;
|
return req;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- RB-23/CQ-007: GET /brief is a pure query — it must not create a row. ---
|
// --- CQ-007: GET /brief is a pure query — it must not create a row. ---
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Get_returns_404_and_writes_no_row_when_no_brief_exists_for_the_owner()
|
public async Task Get_returns_404_and_writes_no_row_when_no_brief_exists_for_the_owner()
|
||||||
@@ -181,8 +181,8 @@ public class BriefEndpointTests(TestWebApplicationFactory factory) : IClassFixtu
|
|||||||
Assert.NotNull(submitted);
|
Assert.NotNull(submitted);
|
||||||
Assert.Equal("submitted", submitted.Brief.Status.Tag);
|
Assert.Equal("submitted", submitted.Brief.Status.Tag);
|
||||||
|
|
||||||
// RB-07/BIO-007: the allow side of the transition leaves a row, not just a log line.
|
// BIO-007: the allow side of the transition leaves a row, not just a log line.
|
||||||
// Resource is the bare "brief" — never the owner's BSN (RB-02).
|
// Resource is the bare "brief" — never the owner's BSN.
|
||||||
Assert.Contains(AuthzAuditStore.List(),
|
Assert.Contains(AuthzAuditStore.List(),
|
||||||
e => e.Action == "brief:submit" && e.Decision == "allow" && e.Resource == "brief");
|
e => e.Action == "brief:submit" && e.Decision == "allow" && e.Resource == "brief");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ public static class TestIdentities
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Type-state test-data builder for <see cref="Aanvraag"/> (WP-70; simplified at WP-73). "Build
|
/// Type-state test-data builder for <see cref="Aanvraag"/>. "Build
|
||||||
/// test data through the same door production code uses" — <see cref="Aanvraag"/> itself is now
|
/// test data through the same door production code uses" — <see cref="Aanvraag"/> itself is now
|
||||||
/// the closed Concept/Submitted/Decided union WP-73 introduced, so this builder no longer needs
|
/// the closed Concept/Submitted/Decided union, so this builder no longer needs
|
||||||
/// to mirror production's guards (step-index bounds, "Afwijzen needs a toelichting") by hand —
|
/// to mirror production's guards (step-index bounds, "Afwijzen needs a toelichting") by hand —
|
||||||
/// it just calls the real nested constructors/required members, which enforce them. A call that
|
/// it just calls the real nested constructors/required members, which enforce them. A call that
|
||||||
/// would build an illegal Aanvraag (e.g. deciding a still-Concept aanvraag, or an Afwijzen with
|
/// would build an illegal Aanvraag (e.g. deciding a still-Concept aanvraag, or an Afwijzen with
|
||||||
@@ -61,7 +61,7 @@ public sealed class ConceptAanvraag
|
|||||||
|
|
||||||
/// Submits the draft — always assigns a Referentie AND SubmittedAt together (mirrors
|
/// Submits the draft — always assigns a Referentie AND SubmittedAt together (mirrors
|
||||||
/// <c>ApplicationStore.Submit</c>), so a fixture built this way can never hit the
|
/// <c>ApplicationStore.Submit</c>), so a fixture built this way can never hit the
|
||||||
/// null-forgiving derefs the pre-WP-73 flat Aanvraag needed (there's nothing to force any
|
/// null-forgiving derefs the earlier flat Aanvraag needed (there's nothing to force any
|
||||||
/// more: both are required, non-null members of <see cref="Aanvraag.Submitted"/>).
|
/// more: both are required, non-null members of <see cref="Aanvraag.Submitted"/>).
|
||||||
public SubmittedAanvraag Submitted(bool autoApprovable = false) => new(_type, _owner, autoApprovable);
|
public SubmittedAanvraag Submitted(bool autoApprovable = false) => new(_type, _owner, autoApprovable);
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ public sealed class SubmittedAanvraag
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Records a behandelaar's decision. Unlike the pre-WP-73 builder, there is no
|
/// <summary>Records a behandelaar's decision. Unlike the earlier builder, there is no
|
||||||
/// hand-written toelichting guard mirroring <c>BeoordelingRules.RequiresToelichting</c> any
|
/// hand-written toelichting guard mirroring <c>BeoordelingRules.RequiresToelichting</c> any
|
||||||
/// more — <see cref="Aanvraag.Decided.Afgewezen"/>/<see cref="Aanvraag.Decided.MeerInfoGevraagd"/>
|
/// more — <see cref="Aanvraag.Decided.Afgewezen"/>/<see cref="Aanvraag.Decided.MeerInfoGevraagd"/>
|
||||||
/// simply have a `required string Toelichting` member; the null-coalescing throw below is the
|
/// simply have a `required string Toelichting` member; the null-coalescing throw below is the
|
||||||
@@ -186,7 +186,7 @@ public sealed class SubmittedAanvraag
|
|||||||
/// (<see cref="BigRegister.Domain.Beoordeling.BeoordelingRules.CanDecide"/>); a fixture that
|
/// (<see cref="BigRegister.Domain.Beoordeling.BeoordelingRules.CanDecide"/>); a fixture that
|
||||||
/// needs a SECOND besluit (the MeerInfoGevraagd "still decidable" case) builds fresh from
|
/// needs a SECOND besluit (the MeerInfoGevraagd "still decidable" case) builds fresh from
|
||||||
/// <see cref="Given.Concept"/> again, exactly as a real second request would. Just a one-line
|
/// <see cref="Given.Concept"/> again, exactly as a real second request would. Just a one-line
|
||||||
/// wrapper around the already-fully-built <see cref="Aanvraag.Decided"/> value — WP-73 moved
|
/// wrapper around the already-fully-built <see cref="Aanvraag.Decided"/> value — construction moved
|
||||||
/// all the actual construction (and its invariant enforcement) into
|
/// all the actual construction (and its invariant enforcement) into
|
||||||
/// <see cref="SubmittedAanvraag.Decided"/> itself, so there's nothing left for this type to do
|
/// <see cref="SubmittedAanvraag.Decided"/> itself, so there's nothing left for this type to do
|
||||||
/// except keep <c>.Decided(...).Build()</c> a valid two-call chain for the existing test
|
/// except keep <c>.Decided(...).Build()</c> a valid two-call chain for the existing test
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ namespace BigRegister.Tests.Domain;
|
|||||||
|
|
||||||
public class ApplicationRuleTests
|
public class ApplicationRuleTests
|
||||||
{
|
{
|
||||||
// WP-63: the published lifecycle (ADR-0002) must name exactly these five tags, in this
|
// The published lifecycle (ADR-0002) must name exactly these five tags, in this
|
||||||
// order — ToStatusDto's string literals must keep matching Enum.ToString(), and Ingediend/
|
// order — ToStatusDto's string literals must keep matching Enum.ToString(), and Ingediend/
|
||||||
// MeerInfoGevraagd (unreachable until WP-65 adds the behandelaar transition) stay defined.
|
// MeerInfoGevraagd (unreachable until a later change adds the behandelaar transition) stay defined.
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AanvraagStatusTag_covers_the_published_lifecycle()
|
public void AanvraagStatusTag_covers_the_published_lifecycle()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ public class BeoordelingRuleTests
|
|||||||
public void Only_open_statuses_are_decidable(AanvraagStatusTag tag, bool expected) =>
|
public void Only_open_statuses_are_decidable(AanvraagStatusTag tag, bool expected) =>
|
||||||
Assert.Equal(expected, BeoordelingRules.CanDecide(tag));
|
Assert.Equal(expected, BeoordelingRules.CanDecide(tag));
|
||||||
|
|
||||||
// WP-68 (F6): the toelichting rule, moved here from an inline endpoint check.
|
// The toelichting rule, moved here from an inline endpoint check.
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(Besluit.Goedkeuren, false)]
|
[InlineData(Besluit.Goedkeuren, false)]
|
||||||
[InlineData(Besluit.Afwijzen, true)]
|
[InlineData(Besluit.Afwijzen, true)]
|
||||||
@@ -24,12 +24,12 @@ public class BeoordelingRuleTests
|
|||||||
public void Only_a_non_approval_requires_a_toelichting(Besluit besluit, bool expected) =>
|
public void Only_a_non_approval_requires_a_toelichting(Besluit besluit, bool expected) =>
|
||||||
Assert.Equal(expected, BeoordelingRules.RequiresToelichting(besluit));
|
Assert.Equal(expected, BeoordelingRules.RequiresToelichting(besluit));
|
||||||
|
|
||||||
// WP-68 (T3): the transition table at the AGGREGATE level, not just against a bare tag —
|
// The transition table at the AGGREGATE level, not just against a bare tag —
|
||||||
// an Aanvraag whose BesluitStatus already records a terminal decision computes a terminal
|
// an Aanvraag whose BesluitStatus already records a terminal decision computes a terminal
|
||||||
// StatusAt, and CanDecide refuses a further besluit regardless of which one. Pins the
|
// StatusAt, and CanDecide refuses a further besluit regardless of which one. Pins the
|
||||||
// domain statement "Afgewezen/Goedgekeurd → no further besluit" independent of the
|
// domain statement "Afgewezen/Goedgekeurd → no further besluit" independent of the
|
||||||
// endpoint's own (integration-level) Already_decided_case_rejects_a_further_besluit.
|
// endpoint's own (integration-level) Already_decided_case_rejects_a_further_besluit.
|
||||||
// WP-70: built via Given, not a hand-rolled Aanvraag literal — Decided(Besluit.Afwijzen) with
|
// Built via Given, not a hand-rolled Aanvraag literal — Decided(Besluit.Afwijzen) with
|
||||||
// no toelichting simply couldn't compile as a fixture here.
|
// no toelichting simply couldn't compile as a fixture here.
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(Besluit.Goedkeuren)]
|
[InlineData(Besluit.Goedkeuren)]
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ namespace BigRegister.Tests.Domain;
|
|||||||
|
|
||||||
public class IntakeRuleTests
|
public class IntakeRuleTests
|
||||||
{
|
{
|
||||||
// The arguments ARE the Given (WP-69/bdd.mdx) — these degenerate to When/Then.
|
// The arguments ARE the Given (bdd.mdx) — these degenerate to When/Then.
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Below_threshold_with_no_answer_is_incomplete() =>
|
public void Below_threshold_with_no_answer_is_incomplete() =>
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ public class EndpointTests(TestWebApplicationFactory factory) : IClassFixture<Te
|
|||||||
new { telefoon = "nope" });
|
new { telefoon = "nope" });
|
||||||
Assert.Equal(HttpStatusCode.UnprocessableEntity, res.StatusCode);
|
Assert.Equal(HttpStatusCode.UnprocessableEntity, res.StatusCode);
|
||||||
// The Submit helper's rejection shape — was asserted through POST /registrations until
|
// The Submit helper's rejection shape — was asserted through POST /registrations until
|
||||||
// RB-06 deleted it; /change-requests is the other endpoint on the same helper.
|
// that endpoint was deleted; /change-requests is the other endpoint on the same helper.
|
||||||
var contentType = res.Content.Headers.ContentType;
|
var contentType = res.Content.Headers.ContentType;
|
||||||
Assert.NotNull(contentType);
|
Assert.NotNull(contentType);
|
||||||
Assert.Contains("application/problem+json", contentType.ToString());
|
Assert.Contains("application/problem+json", contentType.ToString());
|
||||||
@@ -200,8 +200,8 @@ public class EndpointTests(TestWebApplicationFactory factory) : IClassFixture<Te
|
|||||||
public async Task User_delete_blocked_with_409_once_linked_to_submission()
|
public async Task User_delete_blocked_with_409_once_linked_to_submission()
|
||||||
{
|
{
|
||||||
var doc = await Upload(Guid.NewGuid().ToString());
|
var doc = await Upload(Guid.NewGuid().ToString());
|
||||||
// Through the real submit path (RB-06 deleted POST /registrations, which was the only
|
// Through the real submit path (POST /registrations was the only other caller of
|
||||||
// other caller of DocumentStore.Link and had no ownership guard on it).
|
// DocumentStore.Link and had no ownership guard on it; it has since been deleted).
|
||||||
var created = await _client.PostAsJsonAsync("/api/v1/aanvragen", new { type = "registratie" });
|
var created = await _client.PostAsJsonAsync("/api/v1/aanvragen", new { type = "registratie" });
|
||||||
var aanvraag = (await created.Content.ReadFromJsonAsync<AanvraagDetailDto>())!;
|
var aanvraag = (await created.Content.ReadFromJsonAsync<AanvraagDetailDto>())!;
|
||||||
var submit = await _client.PostAsJsonAsync($"/api/v1/aanvragen/{aanvraag.Id}/submit",
|
var submit = await _client.PostAsJsonAsync($"/api/v1/aanvragen/{aanvraag.Id}/submit",
|
||||||
@@ -213,7 +213,7 @@ public class EndpointTests(TestWebApplicationFactory factory) : IClassFixture<Te
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Admin_delete_requires_admin_role()
|
public async Task Admin_delete_requires_admin_role()
|
||||||
{
|
{
|
||||||
// RB-08: routed through CasesAdmin (cases:manage), like the other admin-cases
|
// Routed through CasesAdmin (cases:manage), like the other admin-cases
|
||||||
// endpoints, not the standalone X-Admin header this used to accept.
|
// endpoints, not the standalone X-Admin header this used to accept.
|
||||||
var doc = await Upload(Guid.NewGuid().ToString());
|
var doc = await Upload(Guid.NewGuid().ToString());
|
||||||
Assert.Equal(HttpStatusCode.Forbidden, (await _client.DeleteAsync($"/api/v1/admin/uploads/{doc.DocumentId}")).StatusCode);
|
Assert.Equal(HttpStatusCode.Forbidden, (await _client.DeleteAsync($"/api/v1/admin/uploads/{doc.DocumentId}")).StatusCode);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Mvc.Testing;
|
|||||||
|
|
||||||
namespace BigRegister.Tests;
|
namespace BigRegister.Tests;
|
||||||
|
|
||||||
/// WP-47: runtime feature flags — catalog in code, admin-toggled, server-enforced.
|
/// Runtime feature flags — catalog in code, admin-toggled, server-enforced.
|
||||||
public class FeatureFlagTests(TestWebApplicationFactory factory) : IClassFixture<TestWebApplicationFactory>
|
public class FeatureFlagTests(TestWebApplicationFactory factory) : IClassFixture<TestWebApplicationFactory>
|
||||||
{
|
{
|
||||||
private readonly HttpClient _client = factory.CreateClient();
|
private readonly HttpClient _client = factory.CreateClient();
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ public class IdempotencyTests(TestWebApplicationFactory factory) : IClassFixture
|
|||||||
Assert.NotEqual(firstBody!.Referentie, secondBody!.Referentie);
|
Assert.NotEqual(firstBody!.Referentie, secondBody!.Referentie);
|
||||||
}
|
}
|
||||||
|
|
||||||
// RB-18/BIO-018: IdempotencyStore used to key on the raw client-supplied header alone, so
|
// BIO-018: IdempotencyStore used to key on the raw client-supplied header alone, so
|
||||||
// caller B replaying caller A's Idempotency-Key got caller A's cached reference back —
|
// caller B replaying caller A's Idempotency-Key got caller A's cached reference back —
|
||||||
// a cross-caller leak of a value caller B never submitted. The store now keys on
|
// a cross-caller leak of a value caller B never submitted. The store now keys on
|
||||||
// "{SubjectId}:{idemKey}", so the same header value from two different callers is two
|
// "{SubjectId}:{idemKey}", so the same header value from two different callers is two
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<!doctype html><html lang="nl"><head><meta charset="utf-8"><title>golden-brief-1</title><style>/* letter.css — the FE⇄BE letter-rendering CONTRACT (WP-24/WP-25).
|
<!doctype html><html lang="nl"><head><meta charset="utf-8"><title>golden-brief-1</title><style>/* letter.css — the FE⇄BE letter-rendering CONTRACT.
|
||||||
*
|
*
|
||||||
* One stylesheet, two consumers: the FE letter canvas loads it via <link>
|
* One stylesheet, two consumers: the FE letter canvas loads it via <link>
|
||||||
* (index.html + Storybook preview-head), the backend HTML renderer (WP-25)
|
* (index.html + Storybook preview-head), the backend HTML renderer
|
||||||
* inlines this same file. Its class-parity test is the fence against drift.
|
* inlines this same file. Its class-parity test is the fence against drift.
|
||||||
*
|
*
|
||||||
* Class vocabulary: .letter, .letter__letterhead, .letter__body,
|
* Class vocabulary: .letter, .letter__letterhead, .letter__body,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ using BigRegister.Domain.Letters;
|
|||||||
namespace BigRegister.Tests;
|
namespace BigRegister.Tests;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// WP-25's fence against drift between the backend renderer and the FE letter
|
/// The fence against drift between the backend renderer and the FE letter
|
||||||
/// canvas: a golden-file snapshot of a fixed brief + template, and a class-parity
|
/// canvas: a golden-file snapshot of a fixed brief + template, and a class-parity
|
||||||
/// check that every `letter`-prefixed class the renderer emits exists in the
|
/// check that every `letter`-prefixed class the renderer emits exists in the
|
||||||
/// shared `public/letter.css` contract. Neither test launches a browser.
|
/// shared `public/letter.css` contract. Neither test launches a browser.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Mvc.Testing;
|
|||||||
|
|
||||||
namespace BigRegister.Tests;
|
namespace BigRegister.Tests;
|
||||||
|
|
||||||
/// WP-52: the inbound Notificaties (NRC) webhook — auth accept/reject + the audit trail it
|
/// The inbound Notificaties (NRC) webhook — auth accept/reject + the audit trail it
|
||||||
/// writes via AuthzAuditStore (no Principal exists for an NRC caller, so this doesn't go
|
/// writes via AuthzAuditStore (no Principal exists for an NRC caller, so this doesn't go
|
||||||
/// through the Principal-shaped AuditAuthz helper the user-facing endpoints use).
|
/// through the Principal-shaped AuditAuthz helper the user-facing endpoints use).
|
||||||
public class NotificatieTests(TestWebApplicationFactory factory) : IClassFixture<TestWebApplicationFactory>
|
public class NotificatieTests(TestWebApplicationFactory factory) : IClassFixture<TestWebApplicationFactory>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ using BigRegister.Domain.Authorization;
|
|||||||
namespace BigRegister.Tests;
|
namespace BigRegister.Tests;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Exercises the OpenZaak document source against a stub HttpMessageHandler (WP-51): an
|
/// Exercises the OpenZaak document source against a stub HttpMessageHandler: an
|
||||||
/// upload registers a DRC enkelvoudiginformatieobject, and linking to a zaak POSTs a
|
/// upload registers a DRC enkelvoudiginformatieobject, and linking to a zaak POSTs a
|
||||||
/// zaakinformatieobject per document once a zaak URL is known.
|
/// zaakinformatieobject per document once a zaak URL is known.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -48,7 +48,7 @@ public class OpenZaakDocumentSourceTests
|
|||||||
Assert.Equal("local-1", response.LocalId);
|
Assert.Equal("local-1", response.LocalId);
|
||||||
Assert.NotEmpty(response.DocumentId);
|
Assert.NotEmpty(response.DocumentId);
|
||||||
|
|
||||||
// Registered locally too (dual-write, same reasoning as CreateZaak/WP-50) — content
|
// Registered locally too (dual-write, same reasoning as CreateZaak) — content
|
||||||
// preview/download keeps working regardless of Zgw:Enabled.
|
// preview/download keeps working regardless of Zgw:Enabled.
|
||||||
var stored = DocumentStore.Get(response.DocumentId);
|
var stored = DocumentStore.Get(response.DocumentId);
|
||||||
Assert.NotNull(stored);
|
Assert.NotNull(stored);
|
||||||
@@ -59,7 +59,7 @@ public class OpenZaakDocumentSourceTests
|
|||||||
Assert.Contains("123443210", body); // bronorganisatie
|
Assert.Contains("123443210", body); // bronorganisatie
|
||||||
Assert.Contains("paspoort.pdf", body);
|
Assert.Contains("paspoort.pdf", body);
|
||||||
Assert.Contains(Convert.ToBase64String("%PDF-1.4 fake"u8.ToArray()), body); // inhoud
|
Assert.Contains(Convert.ToBase64String("%PDF-1.4 fake"u8.ToArray()), body); // inhoud
|
||||||
// WP-59: "identiteit" is mapped to "vertrouwelijk" in the confidentialiteit stamdata.
|
// "identiteit" is mapped to "vertrouwelijk" in the confidentialiteit stamdata.
|
||||||
Assert.Contains("\"vertrouwelijkheidaanduiding\":\"vertrouwelijk\"", body);
|
Assert.Contains("\"vertrouwelijkheidaanduiding\":\"vertrouwelijk\"", body);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ public class OpenZaakDocumentSourceTests
|
|||||||
Assert.Contains("\"vertrouwelijkheidaanduiding\":\"openbaar\"", body);
|
Assert.Contains("\"vertrouwelijkheidaanduiding\":\"openbaar\"", body);
|
||||||
}
|
}
|
||||||
|
|
||||||
// WP-60: once DocumentStore.Add has committed, a ZGW-side failure (config gap or transport)
|
// Once DocumentStore.Add has committed, a ZGW-side failure (config gap or transport)
|
||||||
// no longer throws — the local document is authoritative and DrcUrl stays null (the same
|
// no longer throws — the local document is authoritative and DrcUrl stays null (the same
|
||||||
// detector LinkToZaak already skips on for pre-Zgw documents).
|
// detector LinkToZaak already skips on for pre-Zgw documents).
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Mvc.Testing;
|
|||||||
namespace BigRegister.Tests;
|
namespace BigRegister.Tests;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// WP-54: the one test that proves the BFF actually talks to a REAL OpenZaak — auth accepted,
|
/// The one test that proves the BFF actually talks to a REAL OpenZaak — auth accepted,
|
||||||
/// real response shapes, real pagination/zaaktype→aanvraag-type mapping — rather than the stub
|
/// real response shapes, real pagination/zaaktype→aanvraag-type mapping — rather than the stub
|
||||||
/// HttpMessageHandler every other Zgw test (<see cref="ZgwZaakMapperTests"/>,
|
/// HttpMessageHandler every other Zgw test (<see cref="ZgwZaakMapperTests"/>,
|
||||||
/// <see cref="OpenZaakZaakSourceTests"/>) uses. Requires the harness in <c>backend/openzaak/</c>
|
/// <see cref="OpenZaakZaakSourceTests"/>) uses. Requires the harness in <c>backend/openzaak/</c>
|
||||||
@@ -36,7 +36,7 @@ public class OpenZaakIntegrationTests
|
|||||||
.UseSetting("Zgw:ClientId", "bigregister-test")
|
.UseSetting("Zgw:ClientId", "bigregister-test")
|
||||||
.UseSetting("Zgw:Secret", "bigregister-test-secret")
|
.UseSetting("Zgw:Secret", "bigregister-test-secret")
|
||||||
.UseSetting("Zgw:UserId", "bigregister-test")
|
.UseSetting("Zgw:UserId", "bigregister-test")
|
||||||
.UseSetting("Zgw:UserRepresentation", "WP-54 integration test")
|
.UseSetting("Zgw:UserRepresentation", "OpenZaak integration test")
|
||||||
.UseSetting("Zgw:ZaaktypeUrls:herregistratie", zaaktypeUrl));
|
.UseSetting("Zgw:ZaaktypeUrls:herregistratie", zaaktypeUrl));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ public class OpenZaakZaakSourceTests
|
|||||||
Assert.Throws<InvalidOperationException>(() => source.CreateZaak(aanvraag, DateTimeOffset.UtcNow, caller));
|
Assert.Throws<InvalidOperationException>(() => source.CreateZaak(aanvraag, DateTimeOffset.UtcNow, caller));
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- WP-66: besluit write (a status transition on an existing zaak) --------------------
|
// --- Besluit write (a status transition on an existing zaak) --------------------
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RecordBesluit_posts_the_last_statustype_with_besluit_and_toelichting()
|
public void RecordBesluit_posts_the_last_statustype_with_besluit_and_toelichting()
|
||||||
@@ -308,7 +308,7 @@ public class OpenZaakZaakSourceTests
|
|||||||
Assert.Throws<InvalidOperationException>(() => source.RecordBesluit(aanvraag, Besluit.Goedkeuren, null, DateTimeOffset.UtcNow, caller));
|
Assert.Throws<InvalidOperationException>(() => source.RecordBesluit(aanvraag, Besluit.Goedkeuren, null, DateTimeOffset.UtcNow, caller));
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- WP-60: bounded retry in ZgwHttpClient, exercised through the create-zaak write path ---
|
// --- Bounded retry in ZgwHttpClient, exercised through the create-zaak write path ---
|
||||||
|
|
||||||
private static (ZgwOptions options, Aanvraag.Submitted aanvraag, CallerIdentity caller) CreateZaakFixture()
|
private static (ZgwOptions options, Aanvraag.Submitted aanvraag, CallerIdentity caller) CreateZaakFixture()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Mvc.Testing;
|
|||||||
namespace BigRegister.Tests;
|
namespace BigRegister.Tests;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Org templates (WP-23): admin-only endpoints, draft→publish versioning, and the
|
/// Org templates: admin-only endpoints, draft→publish versioning, and the
|
||||||
/// sent-brief immutability invariant (pin at send, republish touches unsent only).
|
/// sent-brief immutability invariant (pin at send, republish touches unsent only).
|
||||||
/// Same reset discipline as BriefEndpointTests — the stores are process-global.
|
/// Same reset discipline as BriefEndpointTests — the stores are process-global.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -58,7 +58,7 @@ public class OrgTemplateEndpointTests(TestWebApplicationFactory factory) : IClas
|
|||||||
public async Task Publish_increments_the_version()
|
public async Task Publish_increments_the_version()
|
||||||
{
|
{
|
||||||
ResetStores();
|
ResetStores();
|
||||||
// One unsent brief for this sub-org (RB-23: GET no longer seeds — create explicitly).
|
// One unsent brief for this sub-org (GET no longer seeds — create explicitly).
|
||||||
await _client.PostAsync("/api/v1/brief/reset", null);
|
await _client.PostAsync("/api/v1/brief/reset", null);
|
||||||
|
|
||||||
var res = await _client.SendAsync(Req(HttpMethod.Post, $"/api/v1/admin/org-template/{Registers}/publish", role: "admin"));
|
var res = await _client.SendAsync(Req(HttpMethod.Post, $"/api/v1/admin/org-template/{Registers}/publish", role: "admin"));
|
||||||
@@ -74,7 +74,7 @@ public class OrgTemplateEndpointTests(TestWebApplicationFactory factory) : IClas
|
|||||||
public async Task Publish_appends_to_the_version_history()
|
public async Task Publish_appends_to_the_version_history()
|
||||||
{
|
{
|
||||||
ResetStores();
|
ResetStores();
|
||||||
await _client.PostAsync("/api/v1/brief/reset", null); // RB-23: GET no longer seeds — create explicitly
|
await _client.PostAsync("/api/v1/brief/reset", null); // GET no longer seeds — create explicitly
|
||||||
|
|
||||||
var res = await _client.SendAsync(Req(HttpMethod.Post, $"/api/v1/admin/org-template/{Registers}/publish", role: "admin"));
|
var res = await _client.SendAsync(Req(HttpMethod.Post, $"/api/v1/admin/org-template/{Registers}/publish", role: "admin"));
|
||||||
res.EnsureSuccessStatusCode();
|
res.EnsureSuccessStatusCode();
|
||||||
@@ -87,7 +87,7 @@ public class OrgTemplateEndpointTests(TestWebApplicationFactory factory) : IClas
|
|||||||
public async Task Publish_counts_the_unsent_briefs_it_affects()
|
public async Task Publish_counts_the_unsent_briefs_it_affects()
|
||||||
{
|
{
|
||||||
ResetStores();
|
ResetStores();
|
||||||
// One unsent brief for this sub-org (RB-23: GET no longer seeds — create explicitly).
|
// One unsent brief for this sub-org (GET no longer seeds — create explicitly).
|
||||||
await _client.PostAsync("/api/v1/brief/reset", null);
|
await _client.PostAsync("/api/v1/brief/reset", null);
|
||||||
|
|
||||||
var res = await _client.SendAsync(Req(HttpMethod.Post, $"/api/v1/admin/org-template/{Registers}/publish", role: "admin"));
|
var res = await _client.SendAsync(Req(HttpMethod.Post, $"/api/v1/admin/org-template/{Registers}/publish", role: "admin"));
|
||||||
@@ -154,7 +154,7 @@ public class OrgTemplateEndpointTests(TestWebApplicationFactory factory) : IClas
|
|||||||
private async Task WalkBriefToSentThenRepublish()
|
private async Task WalkBriefToSentThenRepublish()
|
||||||
{
|
{
|
||||||
ResetStores();
|
ResetStores();
|
||||||
var resetRes = await _client.PostAsync("/api/v1/brief/reset", null); // RB-23: create explicitly
|
var resetRes = await _client.PostAsync("/api/v1/brief/reset", null); // create explicitly
|
||||||
var brief = (await resetRes.Content.ReadFromJsonAsync<BriefViewDto>())!.Brief;
|
var brief = (await resetRes.Content.ReadFromJsonAsync<BriefViewDto>())!.Brief;
|
||||||
var filled = brief.Sections
|
var filled = brief.Sections
|
||||||
.Select(s => new LetterSectionDto(s.SectionKey, s.Title, s.Required,
|
.Select(s => new LetterSectionDto(s.SectionKey, s.Title, s.Required,
|
||||||
@@ -211,7 +211,7 @@ public class OrgTemplateEndpointTests(TestWebApplicationFactory factory) : IClas
|
|||||||
public async Task Admin_cannot_slip_into_the_brief_review_flow()
|
public async Task Admin_cannot_slip_into_the_brief_review_flow()
|
||||||
{
|
{
|
||||||
ResetStores();
|
ResetStores();
|
||||||
var resetRes = await _client.PostAsync("/api/v1/brief/reset", null); // RB-23: create explicitly
|
var resetRes = await _client.PostAsync("/api/v1/brief/reset", null); // create explicitly
|
||||||
var brief = (await resetRes.Content.ReadFromJsonAsync<BriefViewDto>())!.Brief;
|
var brief = (await resetRes.Content.ReadFromJsonAsync<BriefViewDto>())!.Brief;
|
||||||
var filled = brief.Sections
|
var filled = brief.Sections
|
||||||
.Select(s => new LetterSectionDto(s.SectionKey, s.Title, s.Required,
|
.Select(s => new LetterSectionDto(s.SectionKey, s.Title, s.Required,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Mvc.Testing;
|
|||||||
namespace BigRegister.Tests;
|
namespace BigRegister.Tests;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// WP-25: the two HTML preview endpoints. Both are excluded from the OpenAPI doc
|
/// The two HTML preview endpoints. Both are excluded from the OpenAPI doc
|
||||||
/// (see the drift check in the API-client generation step) — these tests hit them
|
/// (see the drift check in the API-client generation step) — these tests hit them
|
||||||
/// as plain HTTP, the same way the hand-written FE fetch does.
|
/// as plain HTTP, the same way the hand-written FE fetch does.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -41,7 +41,7 @@ public class PreviewEndpointTests(TestWebApplicationFactory factory) : IClassFix
|
|||||||
public async Task Preview_of_an_unsent_brief_renders_live_with_a_watermark()
|
public async Task Preview_of_an_unsent_brief_renders_live_with_a_watermark()
|
||||||
{
|
{
|
||||||
ResetStores();
|
ResetStores();
|
||||||
await _client.PostAsync("/api/v1/brief/reset", null); // RB-23: GET no longer seeds — create explicitly
|
await _client.PostAsync("/api/v1/brief/reset", null); // GET no longer seeds — create explicitly
|
||||||
|
|
||||||
var res = await _client.GetAsync("/api/v1/brief/preview");
|
var res = await _client.GetAsync("/api/v1/brief/preview");
|
||||||
res.EnsureSuccessStatusCode();
|
res.EnsureSuccessStatusCode();
|
||||||
@@ -54,7 +54,7 @@ public class PreviewEndpointTests(TestWebApplicationFactory factory) : IClassFix
|
|||||||
public async Task Preview_of_a_sent_brief_serves_the_archive_unchanged_after_a_republish()
|
public async Task Preview_of_a_sent_brief_serves_the_archive_unchanged_after_a_republish()
|
||||||
{
|
{
|
||||||
ResetStores();
|
ResetStores();
|
||||||
var resetRes = await _client.PostAsync("/api/v1/brief/reset", null); // RB-23: create explicitly
|
var resetRes = await _client.PostAsync("/api/v1/brief/reset", null); // create explicitly
|
||||||
var brief = (await resetRes.Content.ReadFromJsonAsync<BriefViewDto>())!.Brief;
|
var brief = (await resetRes.Content.ReadFromJsonAsync<BriefViewDto>())!.Brief;
|
||||||
await _client.PutAsJsonAsync("/api/v1/brief", FilledFrom(brief));
|
await _client.PutAsJsonAsync("/api/v1/brief", FilledFrom(brief));
|
||||||
await _client.SendAsync(Req(HttpMethod.Post, "/api/v1/brief/submit"));
|
await _client.SendAsync(Req(HttpMethod.Post, "/api/v1/brief/submit"));
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
|
|
||||||
namespace BigRegister.Tests;
|
namespace BigRegister.Tests;
|
||||||
|
|
||||||
/// RB-12/BIO-016 (BL-006 — "the backend has zero automated architecture enforcement"): the
|
/// BIO-016 (BL-006 — "the backend has zero automated architecture enforcement"): the
|
||||||
/// only thing that used to keep an admin-shaped endpoint behind `Authz` was a human noticing
|
/// only thing that used to keep an admin-shaped endpoint behind `Authz` was a human noticing
|
||||||
/// in review. BIO-003 (`X-Admin`, a second gate outside `Authz`) and BIO-004 (two endpoints
|
/// in review. BIO-003 (`X-Admin`, a second gate outside `Authz`) and BIO-004 (two endpoints
|
||||||
/// with no gate at all) are exactly the failure mode this test is a safety net for — and it is
|
/// with no gate at all) are exactly the failure mode this test is a safety net for — and it is
|
||||||
/// the safety net RB-19 (a 900-line `Program.cs` reorder) leans on, so its value is entirely in
|
/// the safety net a 900-line `Program.cs` reorder leans on, so its value is entirely in
|
||||||
/// being hard to fool.
|
/// being hard to fool.
|
||||||
///
|
///
|
||||||
/// Every mapped route must be accounted for exactly one of two ways:
|
/// Every mapped route must be accounted for exactly one of two ways:
|
||||||
@@ -48,14 +48,14 @@ public class RouteInventoryTests(TestWebApplicationFactory factory) : IClassFixt
|
|||||||
new("GET", "/api/v1/duo/diplomas", "Static DUO reference fixture + manual-diploma policy — same for every caller."),
|
new("GET", "/api/v1/duo/diplomas", "Static DUO reference fixture + manual-diploma policy — same for every caller."),
|
||||||
new("GET", "/api/v1/intake/policy", "Config VALUE shipped for instant FE feedback (ADR-0001); the server re-validates as authority."),
|
new("GET", "/api/v1/intake/policy", "Config VALUE shipped for instant FE feedback (ADR-0001); the server re-validates as authority."),
|
||||||
new("GET", "/api/v1/uploads/categories", "Static per-wizard category config, no PII, no per-caller distinction."),
|
new("GET", "/api/v1/uploads/categories", "Static per-wizard category config, no PII, no per-caller distinction."),
|
||||||
new("GET", "/api/v1/flags", "Feature-flag catalog + state, readable by any principal by design (WP-47) — only the PUT toggle is admin-gated."),
|
new("GET", "/api/v1/flags", "Feature-flag catalog + state, readable by any principal by design — only the PUT toggle is admin-gated."),
|
||||||
new("GET", "/api/v1/me", "Reflects only the ACTING caller's own role-derived capabilities — no other caller's data to leak."),
|
new("GET", "/api/v1/me", "Reflects only the ACTING caller's own role-derived capabilities — no other caller's data to leak."),
|
||||||
|
|
||||||
// --- Citizen-submitted writes / ownership-scoped inline (ctx.Zorgverlener()/ctx.Caller()),
|
// --- Citizen-submitted writes / ownership-scoped inline (ctx.Zorgverlener()/ctx.Caller()),
|
||||||
// not a role-only admin wrapper because the boundary is resource ownership, not a role. ---
|
// not a role-only admin wrapper because the boundary is resource ownership, not a role. ---
|
||||||
new("POST", "/api/v1/change-requests", "Citizen submission; Submit() records outcome + idempotency, attributed to the acting caller."),
|
new("POST", "/api/v1/change-requests", "Citizen submission; Submit() records outcome + idempotency, attributed to the acting caller."),
|
||||||
new("POST", "/api/v1/uploads", "Upload is attributed to ctx.Zorgverlener() as owner — there is no pre-existing resource to own yet."),
|
new("POST", "/api/v1/uploads", "Upload is attributed to ctx.Zorgverlener() as owner — there is no pre-existing resource to own yet."),
|
||||||
new("GET", "/api/v1/uploads/{documentId}/content", "Ownership-scoped inline (RB-01/BIO-004): owning citizen, or a behandelaar via Authz.CanBeoordelen."),
|
new("GET", "/api/v1/uploads/{documentId}/content", "Ownership-scoped inline (BIO-004): owning citizen, or a behandelaar via Authz.CanBeoordelen."),
|
||||||
new("GET", "/api/v1/uploads/status", "Ownership-scoped inline: DocumentStore.ByLocalIds filtered to ctx.Zorgverlener().Bsn."),
|
new("GET", "/api/v1/uploads/status", "Ownership-scoped inline: DocumentStore.ByLocalIds filtered to ctx.Zorgverlener().Bsn."),
|
||||||
new("DELETE", "/api/v1/uploads/{documentId}", "Ownership-scoped inline: DocumentStore.DeleteOwned keyed by ctx.Zorgverlener().Bsn."),
|
new("DELETE", "/api/v1/uploads/{documentId}", "Ownership-scoped inline: DocumentStore.DeleteOwned keyed by ctx.Zorgverlener().Bsn."),
|
||||||
new("GET", "/api/v1/aanvragen", "Ownership-scoped inline: IZaakSource.ListMyCases(ctx.Zorgverlener(), ...)."),
|
new("GET", "/api/v1/aanvragen", "Ownership-scoped inline: IZaakSource.ListMyCases(ctx.Zorgverlener(), ...)."),
|
||||||
@@ -72,14 +72,14 @@ public class RouteInventoryTests(TestWebApplicationFactory factory) : IClassFixt
|
|||||||
// enforce/emit twin for this whole surface (Authz.CanActOn via BriefStore, ToView's
|
// enforce/emit twin for this whole surface (Authz.CanActOn via BriefStore, ToView's
|
||||||
// Decisions dto) — a different single-source-of-truth than the five Program.cs wrappers,
|
// Decisions dto) — a different single-source-of-truth than the five Program.cs wrappers,
|
||||||
// not a missing one. ---
|
// not a missing one. ---
|
||||||
new("GET", "/api/v1/brief", "Ownership-scoped inline: BriefStore.Get(ctx.Zorgverlener().Bsn), 404 when absent (RB-23)."),
|
new("GET", "/api/v1/brief", "Ownership-scoped inline: BriefStore.Get(ctx.Zorgverlener().Bsn), 404 when absent."),
|
||||||
new("PUT", "/api/v1/brief", "Brief status-machine enforcement: BriefStore.Save + Authz.CanActOn (drafter-only)."),
|
new("PUT", "/api/v1/brief", "Brief status-machine enforcement: BriefStore.Save + Authz.CanActOn (drafter-only)."),
|
||||||
new("POST", "/api/v1/brief/submit", "Brief status-machine enforcement: BriefStore.Submit + Authz.CanActOn."),
|
new("POST", "/api/v1/brief/submit", "Brief status-machine enforcement: BriefStore.Submit + Authz.CanActOn."),
|
||||||
new("POST", "/api/v1/brief/approve", "Brief status-machine enforcement: BriefStore.Approve + Authz.CanActOn (approver != drafter)."),
|
new("POST", "/api/v1/brief/approve", "Brief status-machine enforcement: BriefStore.Approve + Authz.CanActOn (approver != drafter)."),
|
||||||
new("POST", "/api/v1/brief/reject", "Brief status-machine enforcement: BriefStore.Reject + Authz.CanActOn."),
|
new("POST", "/api/v1/brief/reject", "Brief status-machine enforcement: BriefStore.Reject + Authz.CanActOn."),
|
||||||
new("POST", "/api/v1/brief/send", "Brief status-machine enforcement: BriefStore.Send; not role-gated today, per the endpoint's own comment."),
|
new("POST", "/api/v1/brief/send", "Brief status-machine enforcement: BriefStore.Send; not role-gated today, per the endpoint's own comment."),
|
||||||
new("POST", "/api/v1/brief/reveal-bignummer", "Own inline capability + step-up check (Authz.CanRevealBigNummer + X-Step-Up), audited directly."),
|
new("POST", "/api/v1/brief/reveal-bignummer", "Own inline capability + step-up check (Authz.CanRevealBigNummer + X-Step-Up), audited directly."),
|
||||||
new("GET", "/api/v1/brief/preview", "Ownership-scoped inline: BriefStore.Get(ctx.Zorgverlener().Bsn), 404 when absent (RB-23); hand-written FE fetch."),
|
new("GET", "/api/v1/brief/preview", "Ownership-scoped inline: BriefStore.Get(ctx.Zorgverlener().Bsn), 404 when absent; hand-written FE fetch."),
|
||||||
new("POST", "/api/v1/brief/reset", "Deliberately unguarded demo affordance — the endpoint's own comment says so: 'showcase affordance only'."),
|
new("POST", "/api/v1/brief/reset", "Deliberately unguarded demo affordance — the endpoint's own comment says so: 'showcase affordance only'."),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ public class StamdataEndpointTests(TestWebApplicationFactory factory) : IClassFi
|
|||||||
Assert.Empty(table.Rows);
|
Assert.Empty(table.Rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RB-16/BIO-019: DateOnly.Parse used to throw FormatException on unparseable input,
|
/// BIO-019: DateOnly.Parse used to throw FormatException on unparseable input,
|
||||||
/// surfacing as an unhandled 500 instead of the 400-with-problem-details every other
|
/// surfacing as an unhandled 500 instead of the 400-with-problem-details every other
|
||||||
/// bad-input check in this endpoint file returns.
|
/// bad-input check in this endpoint file returns.
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ namespace BigRegister.Tests;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class StamdataValidationTests
|
public class StamdataValidationTests
|
||||||
{
|
{
|
||||||
/// Declared references INTO stamdata keys — the FK-like invariants the build gate enforces
|
/// Declared references INTO stamdata keys — the FK-like invariants the build gate enforces.
|
||||||
/// (WP-48). Add an entry when a consumer starts depending on a stamdata key; the gate then
|
/// Add an entry when a consumer starts depending on a stamdata key; the gate then
|
||||||
/// fails a delete/rename/expire that orphans it. Resolvers use the "valid today" views, so
|
/// fails a delete/rename/expire that orphans it. Resolvers use the "valid today" views, so
|
||||||
/// expiring a row (geldigTot in the past) that current data still references also fails —
|
/// expiring a row (geldigTot in the past) that current data still references also fails —
|
||||||
/// which steers the editor toward closing validity only once nothing current relies on it.
|
/// which steers the editor toward closing validity only once nothing current relies on it.
|
||||||
@@ -23,7 +23,7 @@ public class StamdataValidationTests
|
|||||||
private static readonly IReadOnlySet<string> BeroepCodes =
|
private static readonly IReadOnlySet<string> BeroepCodes =
|
||||||
StamdataFile.Load<Beroep>("beroepen").Select(b => b.Code).ToHashSet(StringComparer.Ordinal);
|
StamdataFile.Load<Beroep>("beroepen").Select(b => b.Code).ToHashSet(StringComparer.Ordinal);
|
||||||
|
|
||||||
// Every document category id that exists across any wizard (WP-59's confidentialiteit
|
// Every document category id that exists across any wizard (the confidentialiteit
|
||||||
// table points at these) — "org-logo" resolves too, even though it's deliberately absent
|
// table points at these) — "org-logo" resolves too, even though it's deliberately absent
|
||||||
// from the confidentialiteit table itself (falls back to "openbaar").
|
// from the confidentialiteit table itself (falls back to "openbaar").
|
||||||
private static readonly IReadOnlySet<string> DocumentCategoryIds = new[] { "registratie", "herregistratie", "org-template" }
|
private static readonly IReadOnlySet<string> DocumentCategoryIds = new[] { "registratie", "herregistratie", "org-template" }
|
||||||
@@ -38,7 +38,7 @@ public class StamdataValidationTests
|
|||||||
SeedData.Diplomas.Select(d => d.Opleiding),
|
SeedData.Diplomas.Select(d => d.Opleiding),
|
||||||
key => Professions.ByProgram.ContainsKey(key)),
|
key => Professions.ByProgram.ContainsKey(key)),
|
||||||
// Stamdata → stamdata references: two tables point at beroepen.code, so deleting or
|
// Stamdata → stamdata references: two tables point at beroepen.code, so deleting or
|
||||||
// renaming a beroep that either still uses fails the build (WP-48 gate, generalized).
|
// renaming a beroep that either still uses fails the build (a stamdata gate, generalized).
|
||||||
new StamdataRef(
|
new StamdataRef(
|
||||||
"Opleiding.beroep → beroepen.code",
|
"Opleiding.beroep → beroepen.code",
|
||||||
StamdataFile.Load<Opleiding>("opleidingen").Select(o => o.Beroep),
|
StamdataFile.Load<Opleiding>("opleidingen").Select(o => o.Beroep),
|
||||||
@@ -47,7 +47,7 @@ public class StamdataValidationTests
|
|||||||
"Specialisme.beroep → beroepen.code",
|
"Specialisme.beroep → beroepen.code",
|
||||||
StamdataFile.Load<Specialisme>("specialismen").Select(s => s.Beroep),
|
StamdataFile.Load<Specialisme>("specialismen").Select(s => s.Beroep),
|
||||||
key => BeroepCodes.Contains(key)),
|
key => BeroepCodes.Contains(key)),
|
||||||
// WP-59: a confidentialiteit row for a category that no wizard ever asks for is dead
|
// A confidentialiteit row for a category that no wizard ever asks for is dead
|
||||||
// config — fail the build rather than let it silently rot.
|
// config — fail the build rather than let it silently rot.
|
||||||
new StamdataRef(
|
new StamdataRef(
|
||||||
"DocumentConfidentialiteit.CategoryId → a real document category",
|
"DocumentConfidentialiteit.CategoryId → a real document category",
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ using Microsoft.AspNetCore.Mvc.Testing;
|
|||||||
|
|
||||||
namespace BigRegister.Tests;
|
namespace BigRegister.Tests;
|
||||||
|
|
||||||
/// WP-53 (extended WP-62): the dev stub identity provider — role from X-Role (unchanged
|
/// The dev stub identity provider — role from X-Role (unchanged
|
||||||
/// behaviour, applies to either actor kind), subject BSN from X-Subject defaulting to the
|
/// behaviour, applies to either actor kind), subject BSN from X-Subject defaulting to the
|
||||||
/// single seeded citizen so every existing request (none of which send X-Subject) resolves
|
/// single seeded citizen so every existing request (none of which send X-Subject) resolves
|
||||||
/// exactly as before this WP. X-Medewerker (+ X-Rollen) selects the medewerker actor kind.
|
/// exactly as before. X-Medewerker (+ X-Rollen) selects the medewerker actor kind.
|
||||||
public class StubIdentityProviderTests
|
public class StubIdentityProviderTests
|
||||||
{
|
{
|
||||||
private static CallerIdentity Resolve(
|
private static CallerIdentity Resolve(
|
||||||
@@ -96,7 +96,7 @@ public class StubIdentityProviderTests
|
|||||||
Assert.Equal(PrincipalRole.Admin, caller.Role);
|
Assert.Equal(PrincipalRole.Admin, caller.Role);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RB-09/BIO-002: IIdentityProvider.Resolve can now return null ("no identity"), but this
|
/// BIO-002: IIdentityProvider.Resolve can now return null ("no identity"), but this
|
||||||
/// stub's own contract stays non-nullable — it is a developer convenience that always invents
|
/// stub's own contract stays non-nullable — it is a developer convenience that always invents
|
||||||
/// a caller, never a source of "no identity" itself. A request with genuinely no headers at
|
/// a caller, never a source of "no identity" itself. A request with genuinely no headers at
|
||||||
/// all still resolves to the seeded citizen, unchanged.
|
/// all still resolves to the seeded citizen, unchanged.
|
||||||
@@ -107,7 +107,7 @@ public class StubIdentityProviderTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RB-09/BIO-002: in Production, StubIdentityProvider is not registered at all (it is
|
/// BIO-002: in Production, StubIdentityProvider is not registered at all (it is
|
||||||
/// Development-only) and there is no real DigiD/employee-SSO IIdentityProvider in this POC yet —
|
/// Development-only) and there is no real DigiD/employee-SSO IIdentityProvider in this POC yet —
|
||||||
/// so a Production build must fail at startup rather than silently resolving every request to
|
/// so a Production build must fail at startup rather than silently resolving every request to
|
||||||
/// the seeded citizen (the failure mode BIO-002 documents).
|
/// the seeded citizen (the failure mode BIO-002 documents).
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
|
|
||||||
namespace BigRegister.Tests;
|
namespace BigRegister.Tests;
|
||||||
|
|
||||||
/// RB-15/BIO-015: `app.UseSwagger()`/`app.UseSwaggerUI()` used to run unconditionally — the
|
/// BIO-015: `app.UseSwagger()`/`app.UseSwaggerUI()` used to run unconditionally — the
|
||||||
/// OpenAPI document (every route + request/response shape) and SwaggerUI's "Try it out" were
|
/// OpenAPI document (every route + request/response shape) and SwaggerUI's "Try it out" were
|
||||||
/// reachable in every environment, including a real deployment. Both are now gated behind
|
/// reachable in every environment, including a real deployment. Both are now gated behind
|
||||||
/// `app.Environment.IsDevelopment()`.
|
/// `app.Environment.IsDevelopment()`.
|
||||||
@@ -16,26 +16,26 @@ public class SwaggerGateTests(TestWebApplicationFactory factory) : IClassFixture
|
|||||||
public async Task Swagger_document_is_served_in_development()
|
public async Task Swagger_document_is_served_in_development()
|
||||||
{
|
{
|
||||||
// The default test environment (WebApplicationFactory<T> defaults to "Development" when
|
// The default test environment (WebApplicationFactory<T> defaults to "Development" when
|
||||||
// nothing overrides it — same fact RB-09's implementation note relies on) — this is the
|
// nothing overrides it — same fact a related implementation note relies on) — this is the
|
||||||
// regression guard that the gate didn't also break the documented `npm run gen:api` /
|
// regression guard that the gate didn't also break the documented `npm run gen:api` /
|
||||||
// local-dev-Swagger-UI experience.
|
// local-dev-Swagger-UI experience.
|
||||||
var res = await factory.CreateClient().GetAsync("/swagger/v1/swagger.json");
|
var res = await factory.CreateClient().GetAsync("/swagger/v1/swagger.json");
|
||||||
Assert.Equal(HttpStatusCode.OK, res.StatusCode);
|
Assert.Equal(HttpStatusCode.OK, res.StatusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Production cannot boot at all today (RB-09: no real IIdentityProvider exists yet), which
|
/// Production cannot boot at all today (no real IIdentityProvider exists yet), which
|
||||||
/// is a *stronger* guarantee than "no Swagger in Production" — but it also means a plain
|
/// is a *stronger* guarantee than "no Swagger in Production" — but it also means a plain
|
||||||
/// `UseEnvironment("Production")` host never reaches this middleware to prove the gate
|
/// `UseEnvironment("Production")` host never reaches this middleware to prove the gate
|
||||||
/// itself works, only that the whole app refuses to start. This uses a third environment
|
/// itself works, only that the whole app refuses to start. This uses a third environment
|
||||||
/// name (neither "Development" nor "Production") with a test-supplied `IIdentityProvider` —
|
/// name (neither "Development" nor "Production") with a test-supplied `IIdentityProvider` —
|
||||||
/// the one thing Program.cs doesn't register outside those two branches — so the host
|
/// the one thing Program.cs doesn't register outside those two branches — so the host
|
||||||
/// actually boots and this test exercises the real gate, not RB-09's unrelated startup throw.
|
/// actually boots and this test exercises the real gate, not that unrelated startup throw.
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Swagger_document_is_not_served_outside_development()
|
public async Task Swagger_document_is_not_served_outside_development()
|
||||||
{
|
{
|
||||||
// Built on top of the shared `factory` fixture (via WithWebHostBuilder), not a bare `new
|
// Built on top of the shared `factory` fixture (via WithWebHostBuilder), not a bare `new
|
||||||
// WebApplicationFactory<Program>()` — that keeps this host on the fixture's own per-class
|
// WebApplicationFactory<Program>()` — that keeps this host on the fixture's own per-class
|
||||||
// isolated AppDb temp path (see TestWebApplicationFactory's doc comment; RB-12's
|
// isolated AppDb temp path (see TestWebApplicationFactory's doc comment; an earlier
|
||||||
// implementation note records the "table already exists" collision a bare factory hits
|
// implementation note records the "table already exists" collision a bare factory hits
|
||||||
// by sharing the mutable static Db.ConnectionString instead).
|
// by sharing the mutable static Db.ConnectionString instead).
|
||||||
using var staging = factory.WithWebHostBuilder(builder => builder
|
using var staging = factory.WithWebHostBuilder(builder => builder
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Mvc.Testing;
|
using Microsoft.AspNetCore.Mvc.Testing;
|
||||||
|
|
||||||
// WP-22's stores read a single static Db.ConnectionString (there's no DI, matching
|
// These stores read a single static Db.ConnectionString (there's no DI, matching
|
||||||
// their pre-WP-22 static-Dictionary shape — see Data/Db.cs). That's correct for a
|
// their earlier static-Dictionary shape — see Data/Db.cs). That's correct for a
|
||||||
// real single-instance process, but xUnit's default parallel-across-classes
|
// real single-instance process, but xUnit's default parallel-across-classes
|
||||||
// execution would run multiple WebApplicationFactory hosts concurrently in this
|
// execution would run multiple WebApplicationFactory hosts concurrently in this
|
||||||
// ONE test process, each overwriting that same static field with its own temp-file
|
// ONE test process, each overwriting that same static field with its own temp-file
|
||||||
@@ -14,7 +14,7 @@ using Microsoft.AspNetCore.Mvc.Testing;
|
|||||||
namespace BigRegister.Tests;
|
namespace BigRegister.Tests;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// WP-22 moved Applications/Documents/Briefs off in-memory dictionaries onto a real
|
/// A migration moved Applications/Documents/Briefs off in-memory dictionaries onto a real
|
||||||
/// SQLite file (see Data/Db.cs). Unlike static dictionaries, a shared file path
|
/// SQLite file (see Data/Db.cs). Unlike static dictionaries, a shared file path
|
||||||
/// would let concurrent test classes' WebApplicationFactory instances hit the same
|
/// would let concurrent test classes' WebApplicationFactory instances hit the same
|
||||||
/// file at once — xUnit runs different test classes in parallel by default, and
|
/// file at once — xUnit runs different test classes in parallel by default, and
|
||||||
@@ -29,7 +29,7 @@ public sealed class TestWebApplicationFactory : WebApplicationFactory<Program>
|
|||||||
|
|
||||||
protected override void ConfigureWebHost(IWebHostBuilder builder) => builder
|
protected override void ConfigureWebHost(IWebHostBuilder builder) => builder
|
||||||
.UseSetting("ConnectionStrings:AppDb", $"Data Source={_dbPath}")
|
.UseSetting("ConnectionStrings:AppDb", $"Data Source={_dbPath}")
|
||||||
// WP-52: a fixed shared secret so NotificatieTests can exercise the accept path —
|
// A fixed shared secret so NotificatieTests can exercise the accept path —
|
||||||
// the appsettings.json default is "" (reject everything), which no test should rely on.
|
// the appsettings.json default is "" (reject everything), which no test should rely on.
|
||||||
.UseSetting("Zgw:NotificatieAuthorization", "test-nrc-secret");
|
.UseSetting("Zgw:NotificatieAuthorization", "test-nrc-secret");
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ using Microsoft.AspNetCore.Mvc.Testing;
|
|||||||
|
|
||||||
namespace BigRegister.Tests;
|
namespace BigRegister.Tests;
|
||||||
|
|
||||||
/// Who may see what about an upload. RB-01/BIO-004: GET /uploads/{id}/content and
|
/// Who may see what about an upload. BIO-004: GET /uploads/{id}/content and
|
||||||
/// /uploads/status used to take no HttpContext at all — a diploma or identity scan was
|
/// /uploads/status used to take no HttpContext at all — a diploma or identity scan was
|
||||||
/// protected by GUID unguessability alone, while DELETE on the same resource was
|
/// protected by GUID unguessability alone, while DELETE on the same resource was
|
||||||
/// owner-scoped. RB-04/BIO-005: the document audit trail recorded the raw owner BSN as
|
/// owner-scoped. BIO-005: the document audit trail recorded the raw owner BSN as
|
||||||
/// its Actor, on a store whose own doc comment says it holds no PII.
|
/// its Actor, on a store whose own doc comment says it holds no PII.
|
||||||
public class UploadAccessTests(TestWebApplicationFactory factory) : IClassFixture<TestWebApplicationFactory>
|
public class UploadAccessTests(TestWebApplicationFactory factory) : IClassFixture<TestWebApplicationFactory>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ using Microsoft.AspNetCore.Mvc.Testing;
|
|||||||
|
|
||||||
namespace BigRegister.Tests;
|
namespace BigRegister.Tests;
|
||||||
|
|
||||||
/// WP-64: the behandelportal's queue of aanvragen needing treatment, gated by the
|
/// The behandelportal's queue of aanvragen needing treatment, gated by the
|
||||||
/// medewerker capability `CanBeoordelen` (WP-62) — not the admin role.
|
/// medewerker capability `CanBeoordelen` — not the admin role.
|
||||||
public class WerkvoorraadTests(TestWebApplicationFactory factory) : IClassFixture<TestWebApplicationFactory>
|
public class WerkvoorraadTests(TestWebApplicationFactory factory) : IClassFixture<TestWebApplicationFactory>
|
||||||
{
|
{
|
||||||
private readonly HttpClient _client = factory.CreateClient();
|
private readonly HttpClient _client = factory.CreateClient();
|
||||||
@@ -38,7 +38,7 @@ public class WerkvoorraadTests(TestWebApplicationFactory factory) : IClassFixtur
|
|||||||
var queue = (await res.Content.ReadFromJsonAsync<List<AanvraagSummaryDto>>())!;
|
var queue = (await res.Content.ReadFromJsonAsync<List<AanvraagSummaryDto>>())!;
|
||||||
var mine = queue.Single(x => x.Id == a.Id);
|
var mine = queue.Single(x => x.Id == a.Id);
|
||||||
Assert.Equal("InBehandeling", mine.Status.Tag);
|
Assert.Equal("InBehandeling", mine.Status.Tag);
|
||||||
// RB-03/BIO-003: masked, like /admin/cases — both inherit ToAdminSummaryDto.
|
// BIO-003: masked, like /admin/cases — both inherit ToAdminSummaryDto.
|
||||||
Assert.Equal("******782", mine.Owner);
|
Assert.Equal("******782", mine.Owner);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
namespace BigRegister.Tests;
|
namespace BigRegister.Tests;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// WP-60's required verification: a ZGW failure mid-submit must not leave the two write sides
|
/// The required verification: a ZGW failure mid-submit must not leave the two write sides
|
||||||
/// silently diverged — it's flagged (<see cref="Aanvraag.ZgwError"/>, an audit row) instead.
|
/// silently diverged — it's flagged (<see cref="Aanvraag.ZgwError"/>, an audit row) instead.
|
||||||
/// Not an <see cref="IClassFixture{TFixture}"/> off <see cref="TestWebApplicationFactory"/>: that
|
/// Not an <see cref="IClassFixture{TFixture}"/> off <see cref="TestWebApplicationFactory"/>: that
|
||||||
/// fixture hardcodes <c>Zgw:Enabled=false</c>, so this builds its own factory the same way
|
/// fixture hardcodes <c>Zgw:Enabled=false</c>, so this builds its own factory the same way
|
||||||
@@ -111,7 +111,7 @@ public class ZgwDivergenceTests
|
|||||||
Assert.Null(stored.ZgwError);
|
Assert.Null(stored.ZgwError);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RB-05/BIO-009: `ZgwError` is persisted to SQLite and written to the application log, so
|
/// BIO-009: `ZgwError` is persisted to SQLite and written to the application log, so
|
||||||
/// the message it carries may not include the response body (OpenZaak echoes the request in
|
/// the message it carries may not include the response body (OpenZaak echoes the request in
|
||||||
/// its errors) or the request's query string (ZGW filters travel there, and one of them is
|
/// its errors) or the request's query string (ZGW filters travel there, and one of them is
|
||||||
/// `rol__betrokkeneIdentificatie__natuurlijkPersoon__inpBsn`).
|
/// `rol__betrokkeneIdentificatie__natuurlijkPersoon__inpBsn`).
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ namespace BigRegister.Tests;
|
|||||||
/// Factored out of OpenZaakZaakSourceTests once OpenZaakDocumentSourceTests needed the
|
/// Factored out of OpenZaakZaakSourceTests once OpenZaakDocumentSourceTests needed the
|
||||||
/// identical stub.
|
/// identical stub.
|
||||||
///
|
///
|
||||||
/// WP-60: an optional <paramref name="status"/> callback lets a test inject a failing status
|
/// An optional <paramref name="status"/> callback lets a test inject a failing status
|
||||||
/// for a given url on a given (0-based) attempt — e.g. "503 on the first call to /zaken, then
|
/// for a given url on a given (0-based) attempt — e.g. "503 on the first call to /zaken, then
|
||||||
/// let it through" — to exercise ZgwHttpClient's retry without a live server. When it returns
|
/// let it through" — to exercise ZgwHttpClient's retry without a live server. When it returns
|
||||||
/// a non-2xx code, <paramref name="respond"/> is not called for that attempt (so a test that
|
/// a non-2xx code, <paramref name="respond"/> is not called for that attempt (so a test that
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ public class ZgwTokenProviderTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Mint_with_a_medewerker_caller_uses_the_medewerkerId_as_user_id()
|
public void Mint_with_a_medewerker_caller_uses_the_medewerkerId_as_user_id()
|
||||||
{
|
{
|
||||||
// WP-62: SubjectId is what ZgwTokenProvider.Mint reads — a medewerker's is its
|
// SubjectId is what ZgwTokenProvider.Mint reads — a medewerker's is its
|
||||||
// medewerkerId, not a BSN, and this is the only place that's directly observable.
|
// medewerkerId, not a BSN, and this is the only place that's directly observable.
|
||||||
var caller = new MedewerkerCaller("m.jansen", [MedewerkerRol.Behandelaar], "M. Jansen", PrincipalRole.Drafter);
|
var caller = new MedewerkerCaller("m.jansen", [MedewerkerRol.Behandelaar], "M. Jansen", PrincipalRole.Drafter);
|
||||||
var token = new ZgwTokenProvider(Options).Mint(caller);
|
var token = new ZgwTokenProvider(Options).Mint(caller);
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
# RD-19 — Strip the `WP-`/`RB-` ticket references from `backend/`
|
||||||
|
|
||||||
|
Status: done
|
||||||
|
Source: PLAN.md Phase 2, item 4
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
The backend half of the sweep RD-18 did for the front end. 370 `WP-NN`/`RB-NN` references sit
|
||||||
|
in comments across 86 files under `backend/` — 70 `.cs` files plus the Dockerfile, four shell
|
||||||
|
scripts, four compose files, two `README.md`s and a handful of config files. `git blame` holds
|
||||||
|
the provenance and stays correct when the code moves; the comment names a closed ticket and
|
||||||
|
tells the reader nothing the sentence around it does not.
|
||||||
|
|
||||||
|
Strip the reference, keep the sentence. No behaviour changes.
|
||||||
|
|
||||||
|
## Read first
|
||||||
|
|
||||||
|
- `backend/tests/BigRegister.Tests/LetterHtmlTests.cs:8-13` — the golden-file test, and the
|
||||||
|
reason decision 2 exists.
|
||||||
|
- `public/letter.css` lines 1 and 4 — three references, mirrored byte for byte inside the
|
||||||
|
golden file.
|
||||||
|
- `backend/src/BigRegister.Api/appsettings.json:9` — the `_Zgw` key, a documentation string
|
||||||
|
rather than a comment.
|
||||||
|
- `RD-18`'s decisions block, for the sentence-quality rule this ticket repeats.
|
||||||
|
|
||||||
|
## Decisions (pre-made, don't relitigate)
|
||||||
|
|
||||||
|
1. **Nothing in `backend/` is exempt. Strip all 370.** Unlike the front end, no backend file
|
||||||
|
disables a check by naming the ticket that removes it — verified against
|
||||||
|
`pragma warning disable`, `Skip =`, `NoWarn` and `SuppressMessage`, none of which carries a
|
||||||
|
reference. Keep all 40 `ADR-000x` references; that count must not move.
|
||||||
|
|
||||||
|
2. **`public/letter.css` and `LetterHtml.golden.html` change together, or the build goes red.**
|
||||||
|
`LetterHtml.cs:161` finds `public/letter.css` at run time and **inlines it** into the
|
||||||
|
rendered letter. `LetterHtml.golden.html` is a snapshot of that output, so it embeds the
|
||||||
|
same CSS comment verbatim. Both hold the same three references (WP-24, WP-25 on line 1;
|
||||||
|
WP-25 on line 4).
|
||||||
|
|
||||||
|
- Edit `public/letter.css`.
|
||||||
|
- Apply the identical edit to the copy inside `LetterHtml.golden.html`.
|
||||||
|
- Editing either one alone fails `Renders_the_golden_brief`.
|
||||||
|
|
||||||
|
`public/letter.css` is the one file outside `backend/` that this ticket touches, exactly as
|
||||||
|
`scripts/gen-behaviour-spec.mjs` was for RD-18.
|
||||||
|
|
||||||
|
3. **No spec regeneration is needed, and that is a fact about C#, not an oversight.**
|
||||||
|
`scripts/gen-behaviour-spec.mjs:124` extracts backend `[Fact]`/`[Theory]` **method names**
|
||||||
|
into `behaviour-spec.mdx`. A C# method name cannot contain a hyphen, so no backend test name
|
||||||
|
can carry a `WP-NN`. Verified: zero `WP_NN`/`RB_NN` underscore variants exist either. Do not
|
||||||
|
run `gen:behaviour-spec`. If the drift check fires, you changed something this ticket did not
|
||||||
|
intend.
|
||||||
|
|
||||||
|
4. **Three kinds of reference live in a string, not a comment. All three are display-only and
|
||||||
|
all three get stripped:**
|
||||||
|
|
||||||
|
| Site | What it is |
|
||||||
|
| ---------------------------------------------------------------------------------------- | -------------------------------------------- |
|
||||||
|
| `appsettings.json:9`, the `_Zgw` key | a documentation string; no code binds `_Zgw` |
|
||||||
|
| `setup_configuration/data.yaml:15,29` + `.template` | the OpenZaak harness's `name:` / `label:` |
|
||||||
|
| `bootstrap-notificaties.sh:44`, `verify-notificatie.sh:32`, `bootstrap-catalogus.sh:171` | a `label=` value and two `echo` lines |
|
||||||
|
|
||||||
|
Verified: nothing in the repository greps for these labels, so renaming them breaks no
|
||||||
|
script. They are read by humans looking at an OpenZaak admin page.
|
||||||
|
|
||||||
|
5. **Keep the sentence readable, not merely shorter** — the same rule as RD-18. Several `.cs`
|
||||||
|
XML doc comments read "… (WP-53) is the acting citizen" or "— WP-73: a freshly submitted
|
||||||
|
aanvraag …", where the reference sits mid-sentence. Rewrite the clause. A stripped line must
|
||||||
|
not leave an empty `()`, a stranded "see", a dangling dash, or a doubled space.
|
||||||
|
|
||||||
|
6. **Leave `<paramref>`, `<see cref=…>` and every other XML doc tag intact.** They are compiled
|
||||||
|
references; breaking one is a build warning at best and a silent documentation hole at worst.
|
||||||
|
Only the ticket number inside the prose goes.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
Everything under `backend/`, plus `public/letter.css`. About 87 files change.
|
||||||
|
|
||||||
|
As in RD-18, this ticket's acceptance commands address the directory rather than a file list,
|
||||||
|
because the sweep's contract is "no reference survives".
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. Strip the references in `backend/`, working directory by directory so the diff stays
|
||||||
|
reviewable.
|
||||||
|
2. Apply decision 2: edit `public/letter.css`, then make the golden file's embedded copy match
|
||||||
|
it exactly.
|
||||||
|
3. Run `cd backend && dotnet test` on its own before the full gate. It is the fastest proof
|
||||||
|
that step 2 landed correctly.
|
||||||
|
4. `git add -A`, then run the acceptance commands.
|
||||||
|
5. Update this ticket's `Status:` to `done` and the README's RD-19 row to `done`.
|
||||||
|
6. Commit all of it together.
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
Measured against the tree before handover. `git grep -o … | wc -l` counts **occurrences**;
|
||||||
|
`git grep -c` counts lines and would give a different, wrong number.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git grep -oE "\b(WP|RB)-[0-9]+" -- backend public/letter.css | wc -l # is 373 -> MUST be 0
|
||||||
|
```
|
||||||
|
|
||||||
|
The ADR references survive, and the sweep leaves no damaged prose:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git grep -oE "ADR-[0-9]+" -- backend | wc -l # unchanged: 40
|
||||||
|
git grep -nE "^\s*(//|\*|#).*\s\(\)" -- backend public/letter.css | wc -l # unchanged: 0
|
||||||
|
git grep -nE "^\s*(//|\*|#).*[a-z] [a-z]" -- backend public/letter.css | wc -l # unchanged: 0
|
||||||
|
```
|
||||||
|
|
||||||
|
The golden file still matches the renderer (decision 2):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend && dotnet test --filter FullyQualifiedName~LetterHtmlTests # exits 0
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run ci # exits 0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
`npm run ci` is enough. `--full` is **not** required: this ticket touches no story, no `.mdx`,
|
||||||
|
and nothing under `libs/shared/src/ui/`. The README's Order table already leaves that column
|
||||||
|
blank for RD-19, and it is right this time.
|
||||||
|
|
||||||
|
If `dotnet test` fails with `SQLite Error 1: 'no such table: …'`, that is the stale-database
|
||||||
|
trap, not your change. See this README's Troubleshooting section.
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
- `apps/`, `libs/`, `docs/`, `.claude/`, `e2e/` — RD-18 did the first two; the rest keep their
|
||||||
|
references.
|
||||||
|
- `public/` beyond `letter.css`.
|
||||||
|
- Rewording a comment beyond what removing the reference requires.
|
||||||
|
- The `Case`/`Zaak` vocabulary rename (PLAN, "Deliberately out of scope").
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- **The golden file is the trap in this ticket.** Three references in `public/letter.css` are
|
||||||
|
mirrored inside `LetterHtml.golden.html`. Change one without the other and the golden test
|
||||||
|
fails. Change neither and the acceptance count cannot reach 0.
|
||||||
|
- **A reference inside a string is still a reference.** Decision 4 lists all three kinds. They
|
||||||
|
do not look like comments, so a comment-only regular expression misses them and the count
|
||||||
|
stops short of 0.
|
||||||
|
- **Do not touch `<see cref=…>` or `<paramref name=…>`** (decision 6).
|
||||||
|
- **`git grep`, never `grep -r`.** `grep -r` reaches `backend/bin`, `backend/obj` and the
|
||||||
|
gitignored SQLite files.
|
||||||
|
- **370 is measured today.** If your first count differs, re-measure before assuming the ticket
|
||||||
|
is stale.
|
||||||
@@ -113,7 +113,7 @@ two. Note that RD-15 exists because 22 abandoned agent worktrees are still on di
|
|||||||
| RD-16 | ~~`parseDashboardView` returns `BigProfile`~~ — DROPPED, see PLAN.md 2.2 | 01 | | n/a |
|
| RD-16 | ~~`parseDashboardView` returns `BigProfile`~~ — DROPPED, see PLAN.md 2.2 | 01 | | n/a |
|
||||||
| RD-17 | `successOf`/`successOr` sweep — 10 sites, 8 files | 01 | | done |
|
| RD-17 | `successOf`/`successOr` sweep — 10 sites, 8 files | 01 | | done |
|
||||||
| RD-18 | Ticket-reference sweep, frontend — 181 refs, 100 files | 01 | yes | done |
|
| RD-18 | Ticket-reference sweep, frontend — 181 refs, 100 files | 01 | yes | done |
|
||||||
| RD-19 | Ticket-reference sweep, backend — 370 refs, 86 files | 01 | | todo |
|
| RD-19 | Ticket-reference sweep, backend — 370 refs, 86 files | 01 | | done |
|
||||||
| RD-20 | `wizard-errors.ts` + spec, adopted by all 3 wizards | 02 | | todo |
|
| RD-20 | `wizard-errors.ts` + spec, adopted by all 3 wizards | 02 | | todo |
|
||||||
| RD-21 | `rich-text-dom.ts` helpers + spec cases | 02 | yes | todo |
|
| RD-21 | `rich-text-dom.ts` helpers + spec cases | 02 | yes | todo |
|
||||||
| RD-22 | `intake-wizard` to 3 step components | 08, 20 | yes | todo |
|
| RD-22 | `intake-wizard` to 3 step components | 08, 20 | yes | todo |
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
/* letter.css — the FE⇄BE letter-rendering CONTRACT (WP-24/WP-25).
|
/* letter.css — the FE⇄BE letter-rendering CONTRACT.
|
||||||
*
|
*
|
||||||
* One stylesheet, two consumers: the FE letter canvas loads it via <link>
|
* One stylesheet, two consumers: the FE letter canvas loads it via <link>
|
||||||
* (index.html + Storybook preview-head), the backend HTML renderer (WP-25)
|
* (index.html + Storybook preview-head), the backend HTML renderer
|
||||||
* inlines this same file. Its class-parity test is the fence against drift.
|
* inlines this same file. Its class-parity test is the fence against drift.
|
||||||
*
|
*
|
||||||
* Class vocabulary: .letter, .letter__letterhead, .letter__body,
|
* Class vocabulary: .letter, .letter__letterhead, .letter__body,
|
||||||
|
|||||||
Reference in New Issue
Block a user