feat(workflow): diploma-eligibility DMN routes foreign diplomas via CBGV-advies (S-13, closes #14) #101
@@ -11,10 +11,12 @@ public interface IWorkflowClient
|
|||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Start one <c>registratie</c> process instance for the given registration, carrying the
|
/// Start one <c>registratie</c> process instance for the given registration, carrying the
|
||||||
/// registration id so the <c>OpenZaakAanmaken</c> external task can be correlated back to its
|
/// registration id (so the <c>OpenZaakAanmaken</c> external task can be correlated back to its
|
||||||
/// aggregate. Returns the process instance id.
|
/// aggregate) and the diploma origin (so the workflow's DMN can route foreign diplomas through
|
||||||
|
/// CBGV-advies, S-13). Returns the process instance id.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<string> StartRegistrationProcessAsync(RegistrationId registrationId, CancellationToken ct = default);
|
Task<string> StartRegistrationProcessAsync(
|
||||||
|
RegistrationId registrationId, DiplomaOrigin diplomaOrigin, CancellationToken ct = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cancel a running <c>registratie</c> process on withdrawal (S-11): correlate the
|
/// Cancel a running <c>registratie</c> process on withdrawal (S-11): correlate the
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ using Big.Domain;
|
|||||||
|
|
||||||
namespace Big.Application;
|
namespace Big.Application;
|
||||||
|
|
||||||
/// <summary>A zorgprofessional's request to register, in domain language. No ZGW concepts.</summary>
|
/// <summary>A zorgprofessional's request to register, in domain language. No ZGW concepts. The
|
||||||
public sealed record SubmitRegistrationCommand(string Bsn);
|
/// diploma origin defaults to domestic (the DigiD path); a foreign (eIDAS) submission sets it to
|
||||||
|
/// <see cref="DiplomaOrigin.Buitenlands"/> so the workflow's DMN routes it through CBGV-advies (S-13).</summary>
|
||||||
|
public sealed record SubmitRegistrationCommand(string Bsn, DiplomaOrigin DiplomaOrigin = DiplomaOrigin.Binnenlands);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The submit use case: create the <see cref="Registration"/> aggregate (INGEDIEND), persist it,
|
/// The submit use case: create the <see cref="Registration"/> aggregate (INGEDIEND), persist it,
|
||||||
@@ -18,13 +20,14 @@ public sealed class SubmitRegistration(IRegistrationStore store, IWorkflowClient
|
|||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(command);
|
ArgumentNullException.ThrowIfNull(command);
|
||||||
|
|
||||||
var registration = Registration.Submit(command.Bsn);
|
var registration = Registration.Submit(command.Bsn, command.DiplomaOrigin);
|
||||||
|
|
||||||
// Persist before starting the process so the worker can correlate the OpenZaakAanmaken
|
// Persist before starting the process so the worker can correlate the OpenZaakAanmaken
|
||||||
// job back to an aggregate that already exists (ADR-0009).
|
// job back to an aggregate that already exists (ADR-0009).
|
||||||
await store.SaveAsync(registration, ct);
|
await store.SaveAsync(registration, ct);
|
||||||
|
|
||||||
var processInstanceId = await workflow.StartRegistrationProcessAsync(registration.Id, ct);
|
var processInstanceId = await workflow.StartRegistrationProcessAsync(
|
||||||
|
registration.Id, registration.DiplomaOrigin, ct);
|
||||||
registration.RecordProcessStarted(processInstanceId);
|
registration.RecordProcessStarted(processInstanceId);
|
||||||
await store.SaveAsync(registration, ct);
|
await store.SaveAsync(registration, ct);
|
||||||
|
|
||||||
|
|||||||
17
services/domain/Big.Domain/DiplomaOrigin.cs
Normal file
17
services/domain/Big.Domain/DiplomaOrigin.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
namespace Big.Domain;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Where a zorgprofessional's diploma was issued. It is the input to the diploma-eligibility decision
|
||||||
|
/// (S-13): a <see cref="Buitenlands"/> (foreign) diploma routes the registratie through an extra
|
||||||
|
/// CBGV-advies assessment step, a <see cref="Binnenlands"/> (domestic) one goes straight to beoordeling.
|
||||||
|
/// The decision itself lives in the workflow's DMN, not here (ADR-0016); the domain only carries the
|
||||||
|
/// origin and hands it to the process as a start variable.
|
||||||
|
/// </summary>
|
||||||
|
public enum DiplomaOrigin
|
||||||
|
{
|
||||||
|
/// <summary>A Dutch (domestic) diploma. Default for a registration submitted via DigiD.</summary>
|
||||||
|
Binnenlands,
|
||||||
|
|
||||||
|
/// <summary>A foreign diploma (e.g. an eIDAS submission). Triggers the CBGV-advies step.</summary>
|
||||||
|
Buitenlands,
|
||||||
|
}
|
||||||
@@ -7,10 +7,11 @@ namespace Big.Domain;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class Registration
|
public sealed class Registration
|
||||||
{
|
{
|
||||||
private Registration(RegistrationId id, string bsn)
|
private Registration(RegistrationId id, string bsn, DiplomaOrigin diplomaOrigin)
|
||||||
{
|
{
|
||||||
Id = id;
|
Id = id;
|
||||||
Bsn = bsn;
|
Bsn = bsn;
|
||||||
|
DiplomaOrigin = diplomaOrigin;
|
||||||
Status = RegistrationStatus.Ingediend;
|
Status = RegistrationStatus.Ingediend;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,6 +21,10 @@ public sealed class Registration
|
|||||||
/// as the domain payload; the domain never constructs ZGW concepts from it (§8.1).</summary>
|
/// as the domain payload; the domain never constructs ZGW concepts from it (§8.1).</summary>
|
||||||
public string Bsn { get; }
|
public string Bsn { get; }
|
||||||
|
|
||||||
|
/// <summary>Where the diploma was issued. Rides along to the process as a start variable and
|
||||||
|
/// drives the diploma-eligibility DMN's foreign→CBGV-advies routing (S-13, ADR-0016).</summary>
|
||||||
|
public DiplomaOrigin DiplomaOrigin { get; }
|
||||||
|
|
||||||
public RegistrationStatus Status { get; private set; }
|
public RegistrationStatus Status { get; private set; }
|
||||||
|
|
||||||
/// <summary>The Flowable process instance driving this registration, once started.</summary>
|
/// <summary>The Flowable process instance driving this registration, once started.</summary>
|
||||||
@@ -28,11 +33,13 @@ public sealed class Registration
|
|||||||
/// <summary>The zaak the ACL opened for this registration, once the external task has run.</summary>
|
/// <summary>The zaak the ACL opened for this registration, once the external task has run.</summary>
|
||||||
public Uri? ZaakUrl { get; private set; }
|
public Uri? ZaakUrl { get; private set; }
|
||||||
|
|
||||||
/// <summary>Submit a new registration. It begins in <see cref="RegistrationStatus.Ingediend"/>.</summary>
|
/// <summary>Submit a new registration. It begins in <see cref="RegistrationStatus.Ingediend"/>.
|
||||||
public static Registration Submit(string bsn)
|
/// The diploma origin defaults to <see cref="DiplomaOrigin.Binnenlands"/> — the common DigiD path;
|
||||||
|
/// a foreign (eIDAS) submission passes <see cref="DiplomaOrigin.Buitenlands"/>.</summary>
|
||||||
|
public static Registration Submit(string bsn, DiplomaOrigin diplomaOrigin = DiplomaOrigin.Binnenlands)
|
||||||
{
|
{
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(bsn);
|
ArgumentException.ThrowIfNullOrWhiteSpace(bsn);
|
||||||
return new Registration(RegistrationId.New(), bsn);
|
return new Registration(RegistrationId.New(), bsn, diplomaOrigin);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Record that the registratie workflow process has been started for this registration.</summary>
|
/// <summary>Record that the registratie workflow process has been started for this registration.</summary>
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ public sealed class FlowableWorkflowClient(HttpClient http, FlowableOptions opti
|
|||||||
private const string BesluitVariable = "besluit";
|
private const string BesluitVariable = "besluit";
|
||||||
private const string IngetrokkenMessage = "RegistratieIngetrokken";
|
private const string IngetrokkenMessage = "RegistratieIngetrokken";
|
||||||
|
|
||||||
public async Task<string> StartRegistrationProcessAsync(RegistrationId registrationId, CancellationToken ct = default)
|
public async Task<string> StartRegistrationProcessAsync(
|
||||||
|
RegistrationId registrationId, DiplomaOrigin diplomaOrigin, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var request = new StartProcessRequest(
|
var request = new StartProcessRequest(
|
||||||
ProcessDefinitionKey,
|
ProcessDefinitionKey,
|
||||||
|
|||||||
@@ -32,12 +32,15 @@ internal sealed class FakeWorkflowClient(string processInstanceId = "proc-1", Ac
|
|||||||
: IWorkflowClient
|
: IWorkflowClient
|
||||||
{
|
{
|
||||||
public RegistrationId? StartedFor { get; private set; }
|
public RegistrationId? StartedFor { get; private set; }
|
||||||
|
public DiplomaOrigin? StartedWithOrigin { get; private set; }
|
||||||
public string? WithdrawnProcessInstanceId { get; private set; }
|
public string? WithdrawnProcessInstanceId { get; private set; }
|
||||||
|
|
||||||
public Task<string> StartRegistrationProcessAsync(RegistrationId registrationId, CancellationToken ct = default)
|
public Task<string> StartRegistrationProcessAsync(
|
||||||
|
RegistrationId registrationId, DiplomaOrigin diplomaOrigin, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
onStart?.Invoke(registrationId);
|
onStart?.Invoke(registrationId);
|
||||||
StartedFor = registrationId;
|
StartedFor = registrationId;
|
||||||
|
StartedWithOrigin = diplomaOrigin;
|
||||||
return Task.FromResult(processInstanceId);
|
return Task.FromResult(processInstanceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public class FlowableWorkflowClientTests
|
|||||||
var client = Client(capture.Responds(HttpStatusCode.Created, """{"id":"pi-1"}"""));
|
var client = Client(capture.Responds(HttpStatusCode.Created, """{"id":"pi-1"}"""));
|
||||||
var rid = RegistrationId.New();
|
var rid = RegistrationId.New();
|
||||||
|
|
||||||
var pid = await client.StartRegistrationProcessAsync(rid);
|
var pid = await client.StartRegistrationProcessAsync(rid, DiplomaOrigin.Binnenlands);
|
||||||
|
|
||||||
Assert.Equal("pi-1", pid);
|
Assert.Equal("pi-1", pid);
|
||||||
Assert.Equal(HttpMethod.Post, capture.Seen!.Method);
|
Assert.Equal(HttpMethod.Post, capture.Seen!.Method);
|
||||||
@@ -38,6 +38,22 @@ public class FlowableWorkflowClientTests
|
|||||||
Assert.Contains($"\"value\":\"{rid}\"", capture.Body);
|
Assert.Contains($"\"value\":\"{rid}\"", capture.Body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(DiplomaOrigin.Binnenlands, "Binnenlands")]
|
||||||
|
[InlineData(DiplomaOrigin.Buitenlands, "Buitenlands")]
|
||||||
|
public async Task Start_posts_the_diploma_origin_as_a_process_variable(DiplomaOrigin origin, string expected)
|
||||||
|
{
|
||||||
|
// The diploma origin rides along as a start variable so the workflow's DMN can route foreign
|
||||||
|
// diplomas through CBGV-advies (S-13, ADR-0016).
|
||||||
|
var capture = new RequestCapture();
|
||||||
|
var client = Client(capture.Responds(HttpStatusCode.Created, """{"id":"pi-1"}"""));
|
||||||
|
|
||||||
|
await client.StartRegistrationProcessAsync(RegistrationId.New(), origin);
|
||||||
|
|
||||||
|
Assert.Contains("\"name\":\"diplomaOrigin\"", capture.Body);
|
||||||
|
Assert.Contains($"\"value\":\"{expected}\"", capture.Body);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Start_uses_the_configured_worker_credentials_and_defaults()
|
public async Task Start_uses_the_configured_worker_credentials_and_defaults()
|
||||||
{
|
{
|
||||||
@@ -125,7 +141,7 @@ public class FlowableWorkflowClientTests
|
|||||||
var client = Client(capture.Responds(HttpStatusCode.InternalServerError));
|
var client = Client(capture.Responds(HttpStatusCode.InternalServerError));
|
||||||
|
|
||||||
await Assert.ThrowsAsync<HttpRequestException>(
|
await Assert.ThrowsAsync<HttpRequestException>(
|
||||||
() => client.StartRegistrationProcessAsync(RegistrationId.New()));
|
() => client.StartRegistrationProcessAsync(RegistrationId.New(), DiplomaOrigin.Binnenlands));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -135,7 +151,7 @@ public class FlowableWorkflowClientTests
|
|||||||
var client = Client(capture.Responds(HttpStatusCode.Created, "null"));
|
var client = Client(capture.Responds(HttpStatusCode.Created, "null"));
|
||||||
|
|
||||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
() => client.StartRegistrationProcessAsync(RegistrationId.New()));
|
() => client.StartRegistrationProcessAsync(RegistrationId.New(), DiplomaOrigin.Binnenlands));
|
||||||
Assert.Contains("empty process-instance", ex.Message);
|
Assert.Contains("empty process-instance", ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,14 @@ public sealed class InMemoryWorkflowClient : IWorkflowClient
|
|||||||
public const string StartedProcessInstanceId = "proc-acc-1";
|
public const string StartedProcessInstanceId = "proc-acc-1";
|
||||||
|
|
||||||
public RegistrationId? StartedFor { get; private set; }
|
public RegistrationId? StartedFor { get; private set; }
|
||||||
|
public DiplomaOrigin? StartedWithOrigin { get; private set; }
|
||||||
public string? WithdrawnProcessInstanceId { get; private set; }
|
public string? WithdrawnProcessInstanceId { get; private set; }
|
||||||
|
|
||||||
public Task<string> StartRegistrationProcessAsync(RegistrationId registrationId, CancellationToken ct = default)
|
public Task<string> StartRegistrationProcessAsync(
|
||||||
|
RegistrationId registrationId, DiplomaOrigin diplomaOrigin, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
StartedFor = registrationId;
|
StartedFor = registrationId;
|
||||||
|
StartedWithOrigin = diplomaOrigin;
|
||||||
return Task.FromResult(StartedProcessInstanceId);
|
return Task.FromResult(StartedProcessInstanceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user