From aa6132a6a9f8175a3db2a41514dd3a9203a6062f Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Fri, 17 Jul 2026 09:06:27 +0200 Subject: [PATCH] feat(workflow): 14-day beoordeling escalation timer + hosted worker (S-14, refs #15) Add a non-interrupting P14D boundary timer on the Beoordelen user task that fires an external-worker task (BeoordelingEscaleren) to the escalation end; wire the hosted BeoordelingEscalatiePump and register the escalation client/processor in DI. On timeout the still-open beoordeling is reassigned to teamlead (ADR-0015). Co-Authored-By: Claude Opus 4.8 (1M context) --- services/domain/Big.Api/Program.cs | 5 ++ .../BeoordelingEscalatiePump.cs | 49 +++++++++++++++++++ workflows/registratie.bpmn | 44 ++++++++++++++++- 3 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 services/domain/Big.Infrastructure/BeoordelingEscalatiePump.cs diff --git a/services/domain/Big.Api/Program.cs b/services/domain/Big.Api/Program.cs index 46ed8fd..b6ee497 100644 --- a/services/domain/Big.Api/Program.cs +++ b/services/domain/Big.Api/Program.cs @@ -21,6 +21,7 @@ builder.Services.AddHttpClient(); builder.Services.AddTransient(sp => sp.GetRequiredService()); builder.Services.AddTransient(sp => sp.GetRequiredService()); builder.Services.AddTransient(sp => sp.GetRequiredService()); +builder.Services.AddTransient(sp => sp.GetRequiredService()); builder.Services.AddHttpClient(); builder.Services.AddScoped(); @@ -30,9 +31,13 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // The hosted external-task job worker polls Flowable and drives OpenZaakAanmaken to completion. builder.Services.AddHostedService(); +// The escalation worker polls the BeoordelingEscaleren jobs the 14-day timer parks and reassigns +// each overdue beoordeling to the teamlead (S-14). +builder.Services.AddHostedService(); var app = builder.Build(); diff --git a/services/domain/Big.Infrastructure/BeoordelingEscalatiePump.cs b/services/domain/Big.Infrastructure/BeoordelingEscalatiePump.cs new file mode 100644 index 0000000..9d444b2 --- /dev/null +++ b/services/domain/Big.Infrastructure/BeoordelingEscalatiePump.cs @@ -0,0 +1,49 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Big.Infrastructure; + +/// +/// The hosted polling loop of the beoordeling-escalation worker (S-14, ADR-0015): on an interval it +/// resolves a scoped and asks it to drain the parked +/// BeoordelingEscaleren jobs. A deliberately thin shell — all acquire/reassign/complete logic +/// lives in the processor, which is unit-tested; this class only owns the timer, the per-tick scope, +/// and loop resilience. Structurally identical to . +/// +public sealed class BeoordelingEscalatiePump( + IServiceScopeFactory scopeFactory, + FlowableOptions options, + ILogger logger) : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + try + { + using var scope = scopeFactory.CreateScope(); + var processor = scope.ServiceProvider.GetRequiredService(); + await processor.PumpOnceAsync(options.MaxJobsPerPoll, stoppingToken); + } + catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) + { + break; + } + catch (Exception ex) + { + // A transient fault (e.g. Flowable briefly unreachable) must not kill the loop. + logger.LogError(ex, "BeoordelingEscaleren job poll failed; retrying after the poll interval."); + } + + try + { + await Task.Delay(options.PollInterval, stoppingToken); + } + catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) + { + break; + } + } + } +} diff --git a/workflows/registratie.bpmn b/workflows/registratie.bpmn index 99a7ec7..4817d99 100644 --- a/workflows/registratie.bpmn +++ b/workflows/registratie.bpmn @@ -15,7 +15,12 @@ S-11 adds withdrawal: while parked at Beoordelen the citizen can trek de aanvraag in — an interrupting message boundary event (RegistratieIngetrokken) cancels the task and ends the process via a dedicated "ingetrokken" end (ADR-0014). The Workflow Client delivers the message - to the task's execution; the BPMN owns the cancellation path. --> + to the task's execution; the BPMN owns the cancellation path. + S-14 adds escalation: a NON-interrupting boundary timer (P14D) on Beoordelen. If a behandelaar + has not picked the task up within 14 days it fires a parallel token to an external-worker task + (BeoordelingEscaleren); the Workflow Client reassigns the still-open Beoordelen task from the + behandelaar group to teamlead (ADR-0015). The Beoordelen task stays open throughout — the timer + only changes who may claim it. --> @@ -45,6 +50,25 @@ + + + + + P14D + + + + + + + + + + @@ -83,6 +107,24 @@ + + + + + + + + + + + + + + + + + +