From 5180253826294f7db158d61e8b2083e7d274bf7c Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Mon, 20 Jul 2026 09:55:47 +0200 Subject: [PATCH] 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) --- .../EenDocumentTermijnVerlopen.feature | 23 ++++++++ .../Steps/EenDocumentTermijnVerlopenSteps.cs | 52 +++++++++++++++++++ .../acceptance/Support/InMemoryDomainPorts.cs | 51 ++++++++++++++++++ 3 files changed, 126 insertions(+) create mode 100644 tests/acceptance/Features/EenDocumentTermijnVerlopen.feature create mode 100644 tests/acceptance/Steps/EenDocumentTermijnVerlopenSteps.cs diff --git a/tests/acceptance/Features/EenDocumentTermijnVerlopen.feature b/tests/acceptance/Features/EenDocumentTermijnVerlopen.feature new file mode 100644 index 0000000..7ec02bf --- /dev/null +++ b/tests/acceptance/Features/EenDocumentTermijnVerlopen.feature @@ -0,0 +1,23 @@ +# language: en +# Drives S-10a (#102). After the zaak is opened the process parks at WachtOpDocumenten with an +# INTERRUPTING 30-day boundary timer. If the documents do not arrive in time the timer cancels the +# task and parks a RegistratieVerlopen job (ADR-0017) which the timeout worker drains, expiring the +# registration to VERLOPEN. Documents received before the timer fires close the wait, so no expiry +# happens. This scenario exercises the timeout worker against an in-memory Flowable stand-in; the timer +# firing live is verify-domain. +Feature: Een documenttermijn laten verlopen + Als registerbeheerder wil ik dat een aanvraag waarvoor de documenten niet binnen 30 dagen binnen zijn + automatisch vervalt zodat onvolledige aanvragen niet blijven liggen. + + Scenario: Zonder documenten binnen 30 dagen vervalt de registratie + Given a registration parked at the WachtOpDocumenten task + When the 30-day document timer fires + And the document-timeout worker runs + Then the registration is verlopen + + Scenario: Tijdig aangeleverde documenten laten de registratie niet vervallen + Given a registration parked at the WachtOpDocumenten task + When the documents arrive before the timer fires + And the 30-day document timer fires + And the document-timeout worker runs + Then the registration is not verlopen diff --git a/tests/acceptance/Steps/EenDocumentTermijnVerlopenSteps.cs b/tests/acceptance/Steps/EenDocumentTermijnVerlopenSteps.cs new file mode 100644 index 0000000..402cfde --- /dev/null +++ b/tests/acceptance/Steps/EenDocumentTermijnVerlopenSteps.cs @@ -0,0 +1,52 @@ +using Acceptance.Support; +using Big.Application; +using Big.Domain; +using Big.Infrastructure; +using Microsoft.Extensions.Logging.Abstractions; +using Reqnroll; +using Xunit; + +namespace Acceptance.Steps; + +/// Bindings for EenDocumentTermijnVerlopen.feature (S-10a). Drives the timeout worker +/// ( over the ) against +/// an in-memory Flowable stand-in and a shared registration store; one instance per scenario. The +/// interrupting 30-day timer either cancels the wait and expires the registration, or — if the +/// documents arrived first — never fires; the scenario asserts on the aggregate's status. +[Binding] +[Scope(Feature = "Een documenttermijn laten verlopen")] +public sealed class EenDocumentTermijnVerlopenSteps +{ + private readonly InMemoryDocumentTimeoutClient _flowable = new(); + private readonly Support.InMemoryRegistrationStore _store = new(); + private Registration _registration = null!; + private string _processInstanceId = ""; + + [Given("a registration parked at the WachtOpDocumenten task")] + public async Task GivenARegistrationParkedAtWachtOpDocumenten() + { + _registration = Registration.Submit("123456782"); + await _store.SaveAsync(_registration); + _processInstanceId = _flowable.ParkWaitingForDocuments(_registration.Id); + } + + [When("the 30-day document timer fires")] + public void WhenTheDocumentTimerFires() => _flowable.FireDocumentTimer(_processInstanceId); + + [When("the documents arrive before the timer fires")] + public void WhenTheDocumentsArriveBeforeTheTimer() => _flowable.ReceiveDocuments(_processInstanceId); + + [When("the document-timeout worker runs")] + public async Task WhenTheTimeoutWorkerRuns() + => await new RegistratieVerlopenProcessor( + _flowable, new ExpireRegistrationWorker(_store), + NullLogger.Instance).PumpOnceAsync(5); + + [Then("the registration is verlopen")] + public async Task ThenTheRegistrationIsVerlopen() + => Assert.Equal(RegistrationStatus.Verlopen, (await _store.GetAsync(_registration.Id))!.Status); + + [Then("the registration is not verlopen")] + public async Task ThenTheRegistrationIsNotVerlopen() + => Assert.Equal(RegistrationStatus.Ingediend, (await _store.GetAsync(_registration.Id))!.Status); +} diff --git a/tests/acceptance/Support/InMemoryDomainPorts.cs b/tests/acceptance/Support/InMemoryDomainPorts.cs index d52bb05..f480320 100644 --- a/tests/acceptance/Support/InMemoryDomainPorts.cs +++ b/tests/acceptance/Support/InMemoryDomainPorts.cs @@ -137,6 +137,57 @@ public sealed class InMemoryEscalatieClient : IBeoordelingEscalatieClient } } +/// An in-memory Flowable stand-in for the document-timeout scenario (S-10a): it models one +/// WachtOpDocumenten wait per process instance — whether it is still open and the registration it +/// correlates to — and the RegistratieVerlopen jobs the interrupting 30-day boundary timer parks. It +/// drives the timeout worker's behaviour without a running Flowable; the timer firing live is the +/// verify-domain check. +public sealed class InMemoryDocumentTimeoutClient : IRegistratieVerlopenClient +{ + private sealed class Wait + { + public required RegistrationId RegistrationId { get; init; } + public bool IsWaiting { get; set; } = true; + } + + private readonly Dictionary _waits = []; + private readonly List _parked = []; + private int _seq; + + /// A registration parks at WachtOpDocumenten, waiting for the citizen's documents. + public string ParkWaitingForDocuments(RegistrationId registrationId) + { + var pid = $"pi-{++_seq}"; + _waits[pid] = new Wait { RegistrationId = registrationId }; + return pid; + } + + /// The documents arrive before the timer fires: the wait task closes, so the interrupting + /// timer no longer fires (mirrors the Workflow Client completing WachtOpDocumenten). + public void ReceiveDocuments(string processInstanceId) => _waits[processInstanceId].IsWaiting = false; + + /// The 30-day interrupting boundary timer fires: if still waiting, it cancels the wait and + /// parks a RegistratieVerlopen job carrying the correlated registration id. A no-op if the documents + /// already arrived (the wait/timer race, §8.6). + public void FireDocumentTimer(string processInstanceId) + { + var wait = _waits[processInstanceId]; + if (!wait.IsWaiting) + return; + wait.IsWaiting = false; + _parked.Add(new RegistratieVerlopenJob($"job-{++_seq}", wait.RegistrationId)); + } + + public Task> AcquireRegistratieVerlopenJobsAsync(int maxJobs, CancellationToken ct = default) + => Task.FromResult>(_parked.Take(maxJobs).ToList()); + + public Task CompleteRegistratieVerlopenJobAsync(string jobId, CancellationToken ct = default) + { + _parked.RemoveAll(j => j.JobId == jobId); + return Task.CompletedTask; + } +} + /// An in-memory registration store for the domain acceptance scenario. public sealed class InMemoryRegistrationStore : IRegistrationStore {