diff --git a/BACKLOG.md b/BACKLOG.md index db28395..c1c90a3 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -209,9 +209,13 @@ Split (issue #11 closed) into two independently-demoable slices per §13 — the #### S-10b · Real diploma upload stored via the ACL Documenten API — #103 -**Outcome:** the self-service "Documenten aanleveren" action becomes a real file upload; the document is stored in the ZGW Documenten (DRC) API and related to the zaak, with all document calls routed through the ACL (§8.1), and the zaak is set to a cancellation status on timeout expiry. Builds on the S-10a trigger/wait. Depends on #102. +**Outcome:** the self-service "Documenten aanleveren" action becomes a real file upload; the file (base64-encoded end-to-end) is stored in the ZGW Documenten (DRC) API as an `enkelvoudiginformatieobject` and related to the zaak, with all document calls routed through the ACL (§8.1, ADR-0018). Builds on the S-10a trigger/wait. Depends on #102. -**Acceptance:** ACL Documenten gateway integration test; Playwright e2e uploads a real document; the openbaar/zaak reflects the stored document. +**Acceptance:** ACL Documenten gateway integration test (real OpenZaak); Playwright e2e uploads a real PDF. + +#### S-10c · Close the ZGW zaak on document-timeout expiry — #106 + +**Outcome:** when the 30-day term lapses (S-10a `RegistratieVerlopen`), the ZGW zaak is set to a cancellation status (not just the domain aggregate → `Verlopen`). Adds a cancellation statustype/resultaattype to the seed + an ACL method + expiry-worker wiring. Carved from S-10b (ADR-0017/0018). Depends on #103. ### S-11 · Withdrawal (Flow 3) diff --git a/docs/architecture/adr-0018-diploma-upload-via-acl-documenten.md b/docs/architecture/adr-0018-diploma-upload-via-acl-documenten.md new file mode 100644 index 0000000..4f97e68 --- /dev/null +++ b/docs/architecture/adr-0018-diploma-upload-via-acl-documenten.md @@ -0,0 +1,74 @@ +# ADR-0018: Diploma upload is stored in the ZGW Documenten API, fronted by the ACL + +- **Status:** Accepted +- **Date:** 2026-07-20 +- **Deciders:** Respellion engineering +- **Relates to:** S-10b (#103); proposal #107. Builds on ADR-0001 (ACL is the only ZGW caller), + ADR-0003 (ACL default-fill), ADR-0017 (document-wait + provision trigger). Carves the zaak-close on + expiry to #106 (S-10c). + +## Context + +S-10a wired the "documenten aanleveren" trigger (portal → BFF → domain → complete the WachtOpDocumenten +wait) with the file itself stubbed. S-10b makes the upload real: the diploma must be **stored in the +ZGW Documenten (DRC) API** and related to the zaak. §8.1 makes the ACL the only code that talks to ZGW. +The DRC API is served by the same OpenZaak container as the Zaken/Catalogi APIs. + +## Decision + +**The ACL fronts the Documenten API: it creates an `enkelvoudiginformatieobject` and relates it to the +zaak. The file travels base64-encoded in JSON across every hop (the portal encodes it client-side); a +"Diploma" `informatieobjecttype` is seeded in the catalogus and injected into the ACL like the +zaaktype.** + +- **ACL gateway.** `OpenZaakGateway.StoreDocumentAsync` POSTs the `enkelvoudiginformatieobject` + (`/documenten/api/v1/enkelvoudiginformatieobjecten`, base64 `inhoud`, `bestandsomvang`, + `status=definitief`) then relates it to the zaak (`/zaken/api/v1/zaakinformatieobjecten`), reusing the + established gateway patterns (ZGW Bearer JWT, buffered non-chunked body for uwsgi, **no CRS headers** — + the Documenten API is not geo, unlike zaak-create). `AclService.StoreDiplomaAsync` default-fills the + ZGW-mandatory fields (informatieobjecttype, bronorganisatie, vertrouwelijkheidaanduiding, `taal=nld`, + creatiedatum); the domain hands over only the zaak, the bytes, and the file's name/type. No new ZGW + scopes were needed — the seed applicatie holds `heeft_alle_autorisaties`. +- **The file travels as base64 JSON end-to-end.** The portal reads the chosen file client-side + (`FileReader`) and posts `{ contentBase64, fileName, contentType }` as JSON to the BFF; the BFF + forwards it to the domain, and the domain to the ACL, all as JSON. This deviates from proposal #107's + "multipart on the portal→BFF hop": base64 JSON keeps **one** contract shape across all four services + (no `IFormFile`/antiforgery plumbing, no multipart in the generated client), and a diploma is a small + placeholder PDF, so the ~33% base64 overhead is immaterial. The ACL turns the base64 back into the + ZGW `inhoud`. +- **Storing precedes completing the wait.** `ProvideDocuments` (from S-10a) now stores the diploma via + the ACL — once the zaak is opened — and then completes the `WachtOpDocumenten` task, so a registration + reaches beoordeling only after its diploma is stored. Both steps stay best-effort about missing + preconditions (no zaak yet → skip storage; no process yet → skip completion), mirroring withdrawal. +- **Catalogus.** `seed_catalogus.py` (OZ_PUBLISH) creates a "Diploma" `informatieobjecttype`, relates it + to the zaaktype (`zaaktype-informatieobjecttypen`, while both concept), publishes both, and prints + `INFORMATIEOBJECTTYPE_URL`; verify-domain injects it as `Acl__Defaults__InformatieobjecttypeUrl` + (a zeros-uuid placeholder otherwise, so the ACL still boots). + +## Consequences + +**Positive** + +- §8.1 stays intact: the ACL is still the only ZGW caller; the portal only talks to the BFF; the domain + only crosses the ACL boundary. Adding a document was almost entirely additive (one gateway method, one + default, one seed block). +- One JSON contract shape across portal/BFF/domain/ACL keeps the generated client and the service + contracts uniform; the upload is exercised live (ACL integration test against real OpenZaak; the + Playwright journey uploads a real PDF). + +**Negative / costs** + +- Base64 inflates the payload ~33% and holds the whole file in memory at each hop — fine for a small + diploma, but not a pattern to reuse for large documents without streaming/multipart. +- The zaak is **not** set to a cancellation status when the 30-day term lapses — carved to #106 (S-10c), + which adds the cancellation statustype/resultaattype + ACL method + expiry-worker wiring. +- Providing documents before the zaak is opened silently skips storage (best-effort); the e2e/live flow + avoids this by uploading only after the openbaar register shows the zaak (INGEDIEND). + +## Alternatives considered + +- **Multipart on the portal→BFF hop** (proposal #107). Rejected: it splits the transport into two shapes + (multipart then JSON), needs `IFormFile` + antiforgery handling and a multipart method in the generated + client, for no benefit at diploma size. +- **The domain talks to the Documenten API directly.** Rejected outright: violates §8.1 (only the ACL + talks to ZGW). diff --git a/docs/demo-script.md b/docs/demo-script.md index 7b48e0c..3004a40 100644 --- a/docs/demo-script.md +++ b/docs/demo-script.md @@ -434,3 +434,31 @@ routing → `Beoordelen`), OR the `P30D` interrupting timer fires → `Registrat > Both branches are covered by the `Een documenttermijn laten verlopen` acceptance scenarios (worker + > aggregate) and unit tests; the wait completion and the 30-day timer firing are asserted live by the > verify-domain check. + +## S-10b — Diploma upload stored in the ZGW Documenten API (#103, ADR-0018) + +The self-service "Documenten aanleveren" action (S-10a) is now a **real file upload**: after submitting, +the citizen picks a PDF and uploads it. The portal base64-encodes the file client-side and posts it to +the BFF; the BFF forwards it to the domain, which stores it via the **ACL** as a ZGW +`enkelvoudiginformatieobject` in the **Documenten (DRC) API** and relates it to the zaak — then completes +the `WachtOpDocumenten` wait so beoordeling can proceed. Per §8.1 only the ACL talks to ZGW. + +```bash +make up +# 1. Log in as jan-burger / test123, submit, then — once the openbaar register shows the row — +# choose a PDF under "Documenten aanleveren" and upload it. The page confirms "aangeleverd". +open http://localhost:8140 +# +# 2. Automated: the walking-skeleton e2e now uploads a real PDF before the behandelaar approves. +make verify-e2e +# +# 3. The ACL integration test proves the document is really created in the Documenten API and +# related to the zaak (against a live OpenZaak): +make verify-acl # → "Storing a diploma creates a real informatieobject related to the zaak" +``` + +**The path:** portal (base64) → BFF `POST /self-service/registrations/{id}/documents` → domain +`ProvideDocuments` → ACL `POST /documenten` → ZGW `enkelvoudiginformatieobjecten` + +`zaakinformatieobjecten`; the wait is then completed and the case advances to Beoordelen (§8.1, ADR-0018). + +> Setting the ZGW zaak to a cancellation status on 30-day expiry is a follow-up (S-10c, #106).