## What & why S-10b: the self-service **diploma upload** is now real. After submitting, the citizen picks a PDF and uploads it; the portal base64-encodes it client-side → BFF → domain → **ACL**, which stores it in the ZGW **Documenten (DRC) API** as an `enkelvoudiginformatieobject` and relates it to the zaak, then the `WachtOpDocumenten` wait completes and the case advances to beoordeling. Per §8.1 only the ACL talks to ZGW. Closes #103 Mechanism in **ADR-0018** (proposal #107). Builds on S-10a (#102). The zaak-close-on-expiry item is carved to **#106 (S-10c)**. ## Definition of Done - [x] Linked Gitea issue (above). - [x] Failing test committed before the implementation (red→green per layer). - [x] Conventional Commits referencing the issue (`refs #103`). - [ ] CI green — all Gitea Actions jobs (pending on this PR). - [x] `docker compose up` health unaffected (ACL boots on a placeholder informatieobjecttype URL; the real one is injected by verify-domain). - [x] Docs updated (ADR-0018, demo-script, BACKLOG + S-10c). - [x] ADR added (`docs/architecture/adr-0018-diploma-upload-via-acl-documenten.md`). - [x] Demo note in `docs/demo-script.md`. ## Notes for reviewers - **ACL** (`OpenZaakGateway.StoreDocumentAsync` + `AclService.StoreDiplomaAsync` + `POST /documenten`) reuses the existing gateway patterns (ZGW Bearer, buffered non-chunked body, **no CRS** — Documenten isn't geo). Unit-tested via the stub handler; an **integration test** stores a real document against live OpenZaak (verify-acl). - **Transport:** base64 JSON on every hop (portal encodes client-side) — I deviated from proposal #107's multipart to keep one contract shape and avoid `IFormFile`/antiforgery/multipart-client plumbing; fine at diploma size (ADR-0018 §Alternatives). - **Infra:** `seed_catalogus.py` seeds + publishes a "Diploma" `informatieobjecttype` and relates it to the zaaktype (while both concept); `verify-domain` injects its URL into the ACL. No new ZGW scopes (seed applicatie has `heeft_alle_autorisaties`). - **e2e:** uploads a real PDF (`setInputFiles`) after the openbaar INGEDIEND row confirms the zaak is open (so storage doesn't race the OpenZaak worker). - **Scope boundary:** the ZGW zaak is not set to a cancellation status on 30-day expiry — that's #106 (S-10c). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: #108
55 lines
2.8 KiB
C#
55 lines
2.8 KiB
C#
using Big.Domain;
|
|
|
|
namespace Big.Application;
|
|
|
|
/// <summary>A zorgprofessional's upload of the diploma their registration is waiting for ("documenten
|
|
/// aanleveren"). <paramref name="Bsn"/> is the authenticated caller (from the DigiD token, forwarded by
|
|
/// the BFF): only the registration's own bsn may provide its documents. <paramref name="Content"/> is
|
|
/// the raw file, with its <paramref name="FileName"/> and <paramref name="ContentType"/>.</summary>
|
|
public sealed record ProvideDocumentsCommand(
|
|
RegistrationId RegistrationId, string Bsn, byte[] Content, string FileName, string ContentType);
|
|
|
|
/// <summary>The outcome of a provide-documents request.</summary>
|
|
public enum ProvideDocumentsOutcome
|
|
{
|
|
/// <summary>The documents were accepted; the process's document wait was completed (if any).</summary>
|
|
Accepted,
|
|
|
|
/// <summary>No registration with that id belongs to the caller — unknown, or owned by someone else
|
|
/// (the two are deliberately indistinguishable, so the endpoint reveals neither).</summary>
|
|
NotFound,
|
|
}
|
|
|
|
/// <summary>
|
|
/// The provide-documents use case (S-10a/S-10b): a zorgprofessional uploads the diploma their
|
|
/// registration is parked waiting for. The document is stored in ZGW via the ACL (§8.1), then the
|
|
/// WachtOpDocumenten task is completed so the registratie process leaves the 30-day wait and continues
|
|
/// to beoordeling (ADR-0017). Owner-scoped by bsn. Both steps are best-effort about missing preconditions
|
|
/// (mirroring <see cref="WithdrawRegistration"/>): storage needs an opened zaak, and completion needs a
|
|
/// running process — a request that arrives before either still stands, storing/completing what it can.
|
|
/// </summary>
|
|
public sealed class ProvideDocuments(IRegistrationStore store, IWorkflowClient workflow, IAclClient acl)
|
|
{
|
|
public async Task<ProvideDocumentsOutcome> HandleAsync(ProvideDocumentsCommand command, CancellationToken ct = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(command);
|
|
|
|
var registration = await store.GetAsync(command.RegistrationId, ct);
|
|
|
|
// Unknown, or not the caller's registration: report NotFound either way (don't reveal which).
|
|
if (registration is null || registration.Bsn != command.Bsn)
|
|
return ProvideDocumentsOutcome.NotFound;
|
|
|
|
// Store the diploma against the zaak (once it is opened) — the ACL is the only ZGW caller (§8.1).
|
|
if (registration.ZaakUrl is not null)
|
|
await acl.StoreDiplomaAsync(
|
|
registration.ZaakUrl, command.Content, command.FileName, command.ContentType, ct);
|
|
|
|
// Complete the document wait (if a process is running) so beoordeling can proceed.
|
|
if (registration.ProcessInstanceId is not null)
|
|
await workflow.CompleteDocumentWaitAsync(registration.ProcessInstanceId, ct);
|
|
|
|
return ProvideDocumentsOutcome.Accepted;
|
|
}
|
|
}
|