Files
register-referentie/docs/architecture/adr-0018-diploma-upload-via-acl-documenten.md
T
notandClaude Opus 4.8 036005e486
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
docs(acl): ADR-0018 + demo/backlog for diploma upload via the ACL Documenten API (refs #103)
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>
2026-07-20 12:18:46 +02:00

4.7 KiB

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).