Files
register-referentie/services/domain/Big.Application/ExpireRegistrationWorker.cs
T
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

38 lines
1.9 KiB
C#

using Big.Domain;
namespace Big.Application;
/// <summary>
/// Handles one acquired <c>RegistratieVerlopen</c> external-worker job (S-10a, ADR-0017): load the
/// registration the job correlates to and expire it to VERLOPEN — the 30-day document-wait timer fired
/// before the documents arrived, so the case is cancelled. Pure application logic over ports; it knows
/// nothing of Flowable. The polling loop that feeds it jobs lives in Infrastructure. Mirrors
/// <see cref="OpenZaakWorker"/>.
/// </summary>
public sealed class ExpireRegistrationWorker(IRegistrationStore store)
{
/// <summary>
/// Process the job. Idempotent and tolerant of races (§8.6, at-least-once delivery): a job whose
/// registration is already resolved — a redelivered expiry (VERLOPEN), or one withdrawn/decided
/// while it waited (INGETROKKEN/INGESCHREVEN/AFGEWEZEN) — is a no-op, so the job still completes
/// rather than throwing into a redelivery loop. Only a still-open registration is expired. An
/// unknown registration is an error: it throws, leaving the job un-completed for Flowable to redeliver.
/// </summary>
public async Task HandleAsync(RegistratieVerlopenJob job, CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(job);
var registration = await store.GetAsync(job.RegistrationId, ct)
?? throw new InvalidOperationException(
$"No registration {job.RegistrationId} for RegistratieVerlopen job {job.JobId}.");
// Only a still-open registration lapses; an already-resolved one (expired, or withdrawn/decided
// while it waited) is left untouched so the job can complete without violating the aggregate.
if (registration.Status is not (RegistrationStatus.Ingediend or RegistrationStatus.InBehandeling))
return;
registration.Expire();
await store.SaveAsync(registration, ct);
}
}