Compare commits

..
Author SHA1 Message Date
notandClaude Opus 4.8 5f77dae587 test(e2e): provide documents on the live self-service tab after the zaak opens (refs #102)
CI / lint (pull_request) Successful in 1m20s
CI / build (pull_request) Successful in 1m2s
CI / unit (pull_request) Successful in 1m12s
CI / frontend (pull_request) Successful in 2m35s
CI / mutation (pull_request) Successful in 5m15s
CI / verify-stack (pull_request) Successful in 7m54s
The process only parks at WachtOpDocumenten once the OpenZaak worker has opened the
zaak, so providing documents immediately after submit raced the wait and no-op'd.
Check the openbaar INGEDIEND row on a second page (proving the zaak is open, hence
the process is at the wait) while the self-service tab keeps its submitted state,
then provide documents there — unblocking the werkbak.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 11:22:07 +02:00
notandClaude Opus 4.8 8d936ffdaa test(acceptance): CapturingDomainClient implements ProvideDocumentsAsync (refs #102)
CI / unit (pull_request) Successful in 1m10s
CI / lint (pull_request) Successful in 1m22s
CI / build (pull_request) Successful in 1m2s
CI / frontend (pull_request) Successful in 2m42s
CI / mutation (pull_request) Successful in 5m28s
CI / verify-stack (pull_request) Failing after 15m47s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 10:48:58 +02:00
notandClaude Opus 4.8 990db61ba7 docs(backlog): S-10a includes the provision trigger; S-10b is real ZGW storage (refs #102)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 10:46:45 +02:00
notandClaude Opus 4.8 5402bc179c docs(workflow): S-10a owns the document-provision trigger (ADR-0017, demo) (refs #102)
Records why the provision trigger (domain + BFF + portal 'Documenten aanleveren')
lives in S-10a — the WachtOpDocumenten gate would otherwise leave the e2e red — and
narrows S-10b to the real ZGW document storage. Notes the withdrawal-while-waiting
follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 10:46:13 +02:00
notandClaude Opus 4.8 2b60f8e51f feat(portal): 'Documenten aanleveren' action on the self-service page (refs #102)
After submitting, the zorgprofessional supplies their documents; the page posts to
the BFF keyed by the reference and confirms ("Uw documenten zijn aangeleverd"), with
a surfaced failure + retry. The registration e2e provides documents before the
behandelaar step, since the process now parks at WachtOpDocumenten first.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 10:45:24 +02:00
notandClaude Opus 4.8 07139324a3 feat(domain): timeout worker skips an already-resolved registration (refs #102)
Expire only a still-open (INGEDIEND/IN_BEHANDELING) registration; an already
resolved one (expired, or withdrawn/decided while it waited) is left untouched so
the job completes without violating the aggregate invariant (§8.6). Closes the
S-10a/S-11 race where a withdrawal-while-waiting would loop the expiry job.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 10:45:11 +02:00
notandClaude Opus 4.8 0d1e2825e5 test(domain): timeout worker no-ops on an already-resolved registration (refs #102)
RED: if the citizen withdrew while parked at WachtOpDocumenten, the RegistratieVerlopen
job finds a terminal (INGETROKKEN) aggregate; the worker must no-op and let the job
complete, not throw into a redelivery loop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 10:45:11 +02:00
notandClaude Opus 4.8 cf1c77489d test(portal): self-service offers 'documenten aanleveren' after submit (refs #102)
RED: after submitting, a "Documenten aanleveren" action posts to the BFF keyed by
the reference and the page confirms; a failure surfaces an alert and keeps the
action. Regenerates the api-client from the updated BFF spec.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 10:42:36 +02:00
notandClaude Opus 4.8 404454d270 feat(bff): POST /self-service/registrations/{id}/documents (S-10a) (refs #102)
Authenticated self-service endpoint that takes the bsn from the DigiD token,
forwards "documenten aanleveren" to the domain, and relays 404 for an unknown or
not-owned registration. Regenerates the committed openapi.json.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 10:41:11 +02:00
notandClaude Opus 4.8 771450d46d test(bff): self-service documents endpoint forwards id + bsn to the domain (refs #102)
RED: POST /self-service/registrations/{id}/documents requires a digid token, takes
the bsn from the token, forwards to the domain, and relays the domain's 404 for an
unknown/not-owned registration. Adds the IDomainClient.ProvideDocumentsAsync port +
client + fake; the endpoint itself follows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 10:39:31 +02:00
notandClaude Opus 4.8 c21becd5b9 feat(domain): ProvideDocuments completes the wait + POST /registrations/{id}/documents (refs #102)
The provide-documents use case completes the WachtOpDocumenten task via the
Workflow Client (owner-scoped by bsn, best-effort), exposed as an owner-scoped
domain endpoint. This is the trigger that unblocks the process; the real file
upload + ZGW storage lands in S-10b.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 10:37:25 +02:00
notandClaude Opus 4.8 7ceb22d46d test(domain): providing documents completes the WachtOpDocumenten wait (refs #102)
RED: the ProvideDocuments use case completes the document wait via the Workflow
Client, owner-scoped by the caller's bsn (a different bsn is NotFound), and is
best-effort when no process was started yet — mirroring WithdrawRegistration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 10:36:21 +02:00
notandClaude Opus 4.8 d943b54ce8 docs(workflow): ADR-0017 + demo note for the document-wait timeout (refs #102)
CI / lint (pull_request) Successful in 1m18s
CI / build (pull_request) Successful in 59s
CI / unit (pull_request) Successful in 1m10s
CI / frontend (pull_request) Successful in 2m33s
CI / mutation (pull_request) Successful in 10m22s
CI / verify-stack (pull_request) Failing after 11m24s
Records the interrupting P30D WachtOpDocumenten timer, the RegistratieVerlopen
worker, and the new terminal Verlopen status; notes the S-10a/S-10b boundary
(ZGW zaak-close deferred). Demo covers both branches.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 09:59:36 +02:00
notandClaude Opus 4.8 00c5077fe4 test(infra): verify-domain drives the document wait + 30-day timeout live (refs #102)
Every registration now parks at WachtOpDocumenten first, so each existing block
completes that task (documents received) before expecting Beoordelen/CBGVAdvies.
Adds a timeout block: a registration whose documents never arrive has its P30D
timer fired via the management-API move idiom, and the domain expires it to VERLOPEN.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 09:58:08 +02:00
notandClaude Opus 4.8 5180253826 test(acceptance): document-termijn verloopt (both branches) (refs #102)
BDD for S-10a: a registration parked at WachtOpDocumenten expires to VERLOPEN when
the 30-day timer fires, and does NOT expire when documents arrive first. Drives the
real RegistratieVerlopenProcessor + ExpireRegistrationWorker against an in-memory
Flowable stand-in, mirroring the escalation feature.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 09:55:47 +02:00
notandClaude Opus 4.8 9bd71f1e78 feat(workflow): WachtOpDocumenten wait task + 30-day timeout cancellation (refs #102)
Inserts a WachtOpDocumenten user task after OpenZaakAanmaken with an interrupting
P30D boundary timer: "documents received" completes it and the process continues to
the diploma routing; on timeout the RegistratieVerlopen external-worker task runs
and the process ends as verlopen (ADR-0017). Verified live against flowable-rest:
complete -> routes to Beoordelen; timer fire -> RegistratieVerlopen job (carrying
registrationId) + the wait task cancelled.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 09:53:01 +02:00
notandClaude Opus 4.8 3f04cb856f feat(infra): Flowable RegistratieVerlopen worker + document-wait completion (refs #102)
FlowableWorkflowClient implements IRegistratieVerlopenClient (acquire/complete the
RegistratieVerlopen jobs) and CompleteDocumentWaitAsync (complete WachtOpDocumenten,
best-effort). Wires the RegistratieVerlopenProcessor + hosted RegistratieVerlopenPump
into the domain host and excludes the pump from mutation (like the other pumps).
Fakes updated for the new IWorkflowClient member.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 09:48:38 +02:00
notandClaude Opus 4.8 9421aa007a test(infra): Flowable client acquires/completes RegistratieVerlopen + completes wait task (refs #102)
RED: the Workflow Client posts the RegistratieVerlopen topic and parses the
correlated registration id, completes the expiry job, and (documents-in-time)
completes the WachtOpDocumenten user task in the instance — best-effort no-op if
that task is no longer open.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 09:45:26 +02:00
notandClaude Opus 4.8 c536c965de feat(domain): RegistratieVerlopen worker + processor expire on document timeout (refs #102)
Adds RegistratieVerlopenJob, IRegistratieVerlopenClient, the ExpireRegistrationWorker
application handler, and the RegistratieVerlopenProcessor drain loop — the timeout
counterpart to the OpenZaak/escalation worker trios.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 09:44:15 +02:00
notandClaude Opus 4.8 5add817c10 test(domain): RegistratieVerlopen worker expires the correlated registration (refs #102)
RED: ExpireRegistrationWorker loads the registration a RegistratieVerlopen job
correlates to and expires it (idempotent on redelivery, throws on unknown so the
job is redelivered); RegistratieVerlopenProcessor drains the parked jobs and
completes each, leaving a failing one un-completed (§8.6). Mirrors the OpenZaak
and escalation worker/processor pairs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 09:43:13 +02:00
notandClaude Opus 4.8 11ef26d8cc feat(domain): Registration.Expire() lapses an open registration to Verlopen (refs #102)
Adds the terminal Verlopen status and Expire(), reusing the RequireOpenForDecision
guard so only an INGEDIEND/IN_BEHANDELING registration can lapse; idempotent once
Verlopen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 09:42:07 +02:00
notandClaude Opus 4.8 f39ec2afa3 test(domain): a document-wait timeout expires the registration to Verlopen (refs #102)
RED: Registration.Expire() moves an open registration to a new terminal
Verlopen status, needs no zaak, is idempotent on redelivery, and is rejected
once the registration has been decided or withdrawn.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 09:41:29 +02:00
notandClaude Opus 4.8 67a60e7f63 docs(backlog): split S-10 into S-10a (#102) and S-10b (#103) (refs #102)
S-10 (#11) spanned six net-new surfaces incl. a new ZGW boundary — too large
for one slice (§13). S-10a is the workflow/timeout spine (backend); S-10b is
the document-upload vertical (ACL Documenten API + portal). #11 closed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 09:36:10 +02:00
@@ -432,111 +432,4 @@ public class OpenZaakGatewayTests
b64 = (b64.Length % 4) switch { 2 => b64 + "==", 3 => b64 + "=", _ => b64 };
return Encoding.UTF8.GetString(Convert.FromBase64String(b64));
}
// --- StoreDocumentAsync (diploma upload / S-10b) ---
private static readonly Uri Informatieobjecttype =
new("http://openzaak/catalogi/api/v1/informatieobjecttypen/dip");
private static DocumentRequest SampleDocument(byte[]? inhoud = null) => new(
Bronorganisatie: "517439943",
Informatieobjecttype: Informatieobjecttype,
Vertrouwelijkheidaanduiding: "openbaar",
Zaak: new Uri(ZaakUrl),
Creatiedatum: new DateOnly(2026, 6, 4),
Titel: "Diploma",
Auteur: "zorgprofessional",
Taal: "nld",
Bestandsnaam: "diploma.pdf",
Formaat: "application/pdf",
Inhoud: inhoud ?? [1, 2, 3, 4]);
// Routes the two document calls: POST /enkelvoudiginformatieobjecten (documenten) then
// POST /zaakinformatieobjecten (zaken).
private static StubHandler DocumentStub(Recorder rec) => new(async req =>
{
rec.Requests.Add(req);
rec.ContentLengths.Add(req.Content?.Headers.ContentLength);
rec.Bodies.Add(req.Content is null ? null : await req.Content.ReadAsStringAsync());
return req.RequestUri!.ToString().Contains("/enkelvoudiginformatieobjecten")
? Json(HttpStatusCode.Created, """{"url":"http://openzaak/documenten/api/v1/enkelvoudiginformatieobjecten/doc-1"}""")
: Json(HttpStatusCode.Created, """{"url":"http://openzaak/zaken/api/v1/zaakinformatieobjecten/rel-1"}""");
});
[Fact]
public async Task Storing_a_document_creates_the_informatieobject_then_relates_it_to_the_zaak()
{
var rec = new Recorder();
var url = await Gateway(DocumentStub(rec)).StoreDocumentAsync(SampleDocument([10, 20, 30]));
Assert.Equal("http://openzaak/documenten/api/v1/enkelvoudiginformatieobjecten/doc-1", url.ToString());
// 1. Create the enkelvoudiginformatieobject in the Documenten API.
var create = rec.Sent("/enkelvoudiginformatieobjecten");
Assert.Equal(HttpMethod.Post, create.Request.Method);
Assert.Equal("http://openzaak/documenten/api/v1/enkelvoudiginformatieobjecten",
create.Request.RequestUri!.ToString());
Assert.Equal("Bearer", create.Request.Headers.Authorization!.Scheme);
Assert.Contains("\"bronorganisatie\":\"517439943\"", create.Body);
Assert.Contains("\"informatieobjecttype\":\"http://openzaak/catalogi/api/v1/informatieobjecttypen/dip\"", create.Body);
Assert.Contains("\"creatiedatum\":\"2026-06-04\"", create.Body);
Assert.Contains("\"titel\":\"Diploma\"", create.Body);
Assert.Contains("\"auteur\":\"zorgprofessional\"", create.Body);
Assert.Contains("\"taal\":\"nld\"", create.Body);
Assert.Contains("\"bestandsnaam\":\"diploma.pdf\"", create.Body);
Assert.Contains("\"formaat\":\"application/pdf\"", create.Body);
Assert.Contains("\"vertrouwelijkheidaanduiding\":\"openbaar\"", create.Body);
Assert.Contains("\"status\":\"definitief\"", create.Body);
// The file content is base64-encoded into `inhoud`, with its byte length in `bestandsomvang`.
Assert.Contains($"\"inhoud\":\"{Convert.ToBase64String([10, 20, 30])}\"", create.Body);
Assert.Contains("\"bestandsomvang\":3", create.Body);
// 2. Relate that informatieobject to the zaak (Zaken API — no CRS).
var relate = rec.Sent("/zaakinformatieobjecten");
Assert.Equal(HttpMethod.Post, relate.Request.Method);
Assert.Equal("http://openzaak/zaken/api/v1/zaakinformatieobjecten",
relate.Request.RequestUri!.ToString());
Assert.Contains($"\"zaak\":\"{ZaakUrl}\"", relate.Body);
Assert.Contains("\"informatieobject\":\"http://openzaak/documenten/api/v1/enkelvoudiginformatieobjecten/doc-1\"", relate.Body);
}
[Fact]
public async Task Storing_a_document_buffers_the_body_and_sends_no_crs_headers()
{
// uwsgi rejects a chunked body (Content-Length must be present); the Documenten API is not a
// geo API, so no CRS headers (unlike the Zaken zaak-create).
var rec = new Recorder();
await Gateway(DocumentStub(rec)).StoreDocumentAsync(SampleDocument());
var create = rec.Sent("/enkelvoudiginformatieobjecten");
Assert.NotNull(create.Length);
Assert.True(create.Length > 0);
Assert.False(create.Request.Headers.Contains("Accept-Crs"));
Assert.False(create.Request.Content!.Headers.Contains("Content-Crs"));
}
[Fact]
public async Task Storing_a_document_surfaces_an_openzaak_rejection()
{
var handler = new StubHandler(_ =>
Task.FromResult(new HttpResponseMessage(HttpStatusCode.BadRequest)
{
Content = new StringContent("""{"detail":"bad"}""", Encoding.UTF8, "application/json"),
}));
var ex = await Assert.ThrowsAsync<HttpRequestException>(
() => Gateway(handler).StoreDocumentAsync(SampleDocument()));
Assert.Contains("bad", ex.Message);
}
[Fact]
public async Task Storing_a_document_rejects_a_null_request()
{
var handler = new StubHandler(_ => throw new InvalidOperationException("should not be sent"));
await Assert.ThrowsAsync<ArgumentNullException>(() => Gateway(handler).StoreDocumentAsync(null!));
}
}