CI / lint (pull_request) Successful in 1m19s
CI / build (pull_request) Failing after 56s
CI / unit (pull_request) Failing after 1m6s
CI / frontend (pull_request) Successful in 2m37s
CI / mutation (pull_request) Successful in 5m44s
CI / verify-stack (pull_request) Has been cancelled
Records the ACL-fronted Documenten create+relate, the base64-JSON transport chosen over proposal #107's multipart, the informatieobjecttype seeding, and the S-10c (zaak-close) carve-off. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
75 lines
4.7 KiB
Markdown
75 lines
4.7 KiB
Markdown
# 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).
|