feat(workflow): beoordeling escalation to teamlead after 14 days (S-14, closes #15) #99
@@ -22,6 +22,9 @@
|
|||||||
<ProjectReference Include="..\..\services\acl\Acl.Infrastructure\Acl.Infrastructure.csproj" />
|
<ProjectReference Include="..\..\services\acl\Acl.Infrastructure\Acl.Infrastructure.csproj" />
|
||||||
<ProjectReference Include="..\..\services\event-subscriber\EventSubscriber.Application\EventSubscriber.Application.csproj" />
|
<ProjectReference Include="..\..\services\event-subscriber\EventSubscriber.Application\EventSubscriber.Application.csproj" />
|
||||||
<ProjectReference Include="..\..\services\domain\Big.Application\Big.Application.csproj" />
|
<ProjectReference Include="..\..\services\domain\Big.Application\Big.Application.csproj" />
|
||||||
|
<!-- The beoordeling-escalation scenario drives the escalation worker (Infrastructure), as the
|
||||||
|
ACL scenario drives Acl.Infrastructure — escalation has no domain-aggregate surface (S-14). -->
|
||||||
|
<ProjectReference Include="..\..\services\domain\Big.Infrastructure\Big.Infrastructure.csproj" />
|
||||||
<ProjectReference Include="..\..\services\bff\Bff.Api\Bff.Api.csproj" />
|
<ProjectReference Include="..\..\services\bff\Bff.Api\Bff.Api.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
22
tests/acceptance/Features/EenBeoordelingEscaleren.feature
Normal file
22
tests/acceptance/Features/EenBeoordelingEscaleren.feature
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# language: en
|
||||||
|
# Drives S-14 (#15). A beoordeling a behandelaar does not pick up within 14 days escalates to the
|
||||||
|
# teamlead: a non-interrupting boundary timer parks a BeoordelingEscaleren job (ADR-0015) which the
|
||||||
|
# escalation worker drains, reassigning the still-open Beoordelen task's candidate group to teamlead.
|
||||||
|
# A beoordeling completed before the timer fires does not escalate. This scenario exercises the
|
||||||
|
# escalation worker against an in-memory Flowable stand-in; the timer firing live is verify-domain.
|
||||||
|
Feature: Een beoordeling escaleren
|
||||||
|
Als teamleider wil ik dat een beoordeling die na 14 dagen niet is opgepakt naar mij escaleert
|
||||||
|
zodat aanvragen niet blijven liggen.
|
||||||
|
|
||||||
|
Scenario: Na 14 dagen zonder oppakken escaleert de beoordeling naar de teamlead
|
||||||
|
Given a registration parked at the Beoordelen task for the behandelaar
|
||||||
|
When the 14-day escalation timer fires
|
||||||
|
And the escalation worker runs
|
||||||
|
Then the beoordeling is reassigned to the teamlead
|
||||||
|
|
||||||
|
Scenario: Een tijdig afgeronde beoordeling escaleert niet
|
||||||
|
Given a registration parked at the Beoordelen task for the behandelaar
|
||||||
|
When the behandelaar completes the beoordeling before the timer fires
|
||||||
|
And the 14-day escalation timer fires
|
||||||
|
And the escalation worker runs
|
||||||
|
Then the beoordeling stays with the behandelaar
|
||||||
44
tests/acceptance/Steps/EenBeoordelingEscalerenSteps.cs
Normal file
44
tests/acceptance/Steps/EenBeoordelingEscalerenSteps.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
using Acceptance.Support;
|
||||||
|
using Big.Infrastructure;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Reqnroll;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Acceptance.Steps;
|
||||||
|
|
||||||
|
/// <summary>Bindings for <c>EenBeoordelingEscaleren.feature</c> (S-14). Drives the escalation worker
|
||||||
|
/// (<see cref="BeoordelingEscalatieProcessor"/>) against an in-memory Flowable stand-in; one instance
|
||||||
|
/// per scenario. Escalation has no domain-aggregate surface — it only reassigns who may claim the
|
||||||
|
/// still-open Beoordelen task — so the scenario asserts on the task's candidate group.</summary>
|
||||||
|
[Binding]
|
||||||
|
[Scope(Feature = "Een beoordeling escaleren")]
|
||||||
|
public sealed class EenBeoordelingEscalerenSteps
|
||||||
|
{
|
||||||
|
private readonly InMemoryEscalatieClient _flowable = new();
|
||||||
|
private string _processInstanceId = "";
|
||||||
|
|
||||||
|
[Given("a registration parked at the Beoordelen task for the behandelaar")]
|
||||||
|
public void GivenARegistrationParkedForTheBehandelaar()
|
||||||
|
=> _processInstanceId = _flowable.ParkBeoordeling();
|
||||||
|
|
||||||
|
[When("the 14-day escalation timer fires")]
|
||||||
|
public void WhenTheEscalationTimerFires()
|
||||||
|
=> _flowable.FireEscalationTimer(_processInstanceId);
|
||||||
|
|
||||||
|
[When("the behandelaar completes the beoordeling before the timer fires")]
|
||||||
|
public void WhenTheBehandelaarCompletesBeforeTheTimer()
|
||||||
|
=> _flowable.CompleteBeoordeling(_processInstanceId);
|
||||||
|
|
||||||
|
[When("the escalation worker runs")]
|
||||||
|
public async Task WhenTheEscalationWorkerRuns()
|
||||||
|
=> await new BeoordelingEscalatieProcessor(
|
||||||
|
_flowable, NullLogger<BeoordelingEscalatieProcessor>.Instance).PumpOnceAsync(5);
|
||||||
|
|
||||||
|
[Then("the beoordeling is reassigned to the teamlead")]
|
||||||
|
public void ThenTheBeoordelingIsReassignedToTheTeamlead()
|
||||||
|
=> Assert.Equal("teamlead", _flowable.CandidateGroupFor(_processInstanceId));
|
||||||
|
|
||||||
|
[Then("the beoordeling stays with the behandelaar")]
|
||||||
|
public void ThenTheBeoordelingStaysWithTheBehandelaar()
|
||||||
|
=> Assert.Equal("behandelaar", _flowable.CandidateGroupFor(_processInstanceId));
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using Big.Application;
|
using Big.Application;
|
||||||
using Big.Domain;
|
using Big.Domain;
|
||||||
|
using Big.Infrastructure;
|
||||||
|
|
||||||
namespace Acceptance.Support;
|
namespace Acceptance.Support;
|
||||||
|
|
||||||
@@ -73,6 +74,59 @@ public sealed class InMemoryUserTaskClient : IUserTaskClient
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>An in-memory Flowable stand-in for the beoordeling-escalation scenario (S-14): it models
|
||||||
|
/// one Beoordelen task per process instance — its candidate group and whether it is still open — and
|
||||||
|
/// the escalation jobs the non-interrupting 14-day boundary timer parks. It drives the escalation
|
||||||
|
/// worker's behaviour without a running Flowable; the timer firing live is the verify-domain check.</summary>
|
||||||
|
public sealed class InMemoryEscalatieClient : IBeoordelingEscalatieClient
|
||||||
|
{
|
||||||
|
private sealed class ParkedTask
|
||||||
|
{
|
||||||
|
public string CandidateGroup { get; set; } = "behandelaar";
|
||||||
|
public bool IsOpen { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly Dictionary<string, ParkedTask> _tasks = [];
|
||||||
|
private readonly List<EscalatieJob> _parked = [];
|
||||||
|
private int _seq;
|
||||||
|
|
||||||
|
/// <summary>A registration parks at Beoordelen, claimable by the behandelaar group.</summary>
|
||||||
|
public string ParkBeoordeling()
|
||||||
|
{
|
||||||
|
var pid = $"pi-{++_seq}";
|
||||||
|
_tasks[pid] = new ParkedTask();
|
||||||
|
return pid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>The behandelaar completes the beoordeling before the timer fires: the task closes.</summary>
|
||||||
|
public void CompleteBeoordeling(string processInstanceId) => _tasks[processInstanceId].IsOpen = false;
|
||||||
|
|
||||||
|
/// <summary>The 14-day boundary timer fires: a non-interrupting token parks an escalation job.</summary>
|
||||||
|
public void FireEscalationTimer(string processInstanceId)
|
||||||
|
=> _parked.Add(new EscalatieJob($"job-{++_seq}", processInstanceId));
|
||||||
|
|
||||||
|
/// <summary>The candidate group that may currently pick the task up.</summary>
|
||||||
|
public string CandidateGroupFor(string processInstanceId) => _tasks[processInstanceId].CandidateGroup;
|
||||||
|
|
||||||
|
public Task<IReadOnlyList<EscalatieJob>> AcquireBeoordelingEscalatieJobsAsync(int maxJobs, CancellationToken ct = default)
|
||||||
|
=> Task.FromResult<IReadOnlyList<EscalatieJob>>(_parked.Take(maxJobs).ToList());
|
||||||
|
|
||||||
|
public Task ReassignBeoordelingToTeamleadAsync(string processInstanceId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
// No-op if the behandelaar already completed it — the timer/completion race (§8.6).
|
||||||
|
var task = _tasks[processInstanceId];
|
||||||
|
if (task.IsOpen)
|
||||||
|
task.CandidateGroup = "teamlead";
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task CompleteBeoordelingEscalatieJobAsync(string jobId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
_parked.RemoveAll(j => j.JobId == jobId);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>An in-memory registration store for the domain acceptance scenario.</summary>
|
/// <summary>An in-memory registration store for the domain acceptance scenario.</summary>
|
||||||
public sealed class InMemoryRegistrationStore : IRegistrationStore
|
public sealed class InMemoryRegistrationStore : IRegistrationStore
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user