feat(workflow): diploma-eligibility DMN routes foreign diplomas via CBGV-advies (S-13, closes #14) (#101)
## What & why S-13: a diploma's origin decides its route. A **DMN** (`diploma-eligibility`) is evaluated inline by the registratie process as a **`businessRuleTask`**; an exclusive gateway routes a **foreign** (Buitenlands) diploma through a new **CBGVAdvies** user task before `Beoordelen`, a **domestic** one straight there (PRD flow 4). The domain's only new job is carrying the diploma origin and passing it as a process start variable. Chose **Option B (DMN in the BPMN)** over the issue's literal "evaluated by the Domain Service via Workflow Client" wording — keeps the decision a first-class workflow artefact and §8.2 clean. Rationale in **ADR-0016** (proposal #100); noted on this issue. Closes #14 ## Definition of Done - [x] Linked Gitea issue (above). - [x] Failing test committed before the implementation. - [x] Implementation makes the test pass. - [x] Conventional Commits referencing the issue (`refs #14`). - [ ] CI green — all Gitea Actions jobs. - [x] `docker compose up` from a fresh clone reaches green health checks within 3 minutes (additive; DMN deployed by flowable-init). - [x] Docs updated (ADR-0016, demo note). - [x] ADR added (`docs/architecture/adr-0016-diploma-eligibility-dmn.md`). - [x] Demo note in `docs/demo-script.md`. ## How it was built (TDD) - **Domain**: `DiplomaOrigin` on the aggregate + submit command; threaded through the process-start port so the Workflow Client emits a `diplomaOrigin` start variable. Red → green. - **DMN + BPMN**: `workflows/diploma-eligibility.dmn` (origin → route); `businessRuleTask` + exclusive gateway + `CBGVAdvies` user task in `registratie.bpmn`; DMN deployed to Flowable's DMN engine by `flowable-init`. - **Both paths**: `Een diploma op herkomst routeren` acceptance scenarios (origin carried into the process) + unit tests; verify-domain drives a foreign registration through CBGVAdvies→Beoordelen and the domestic one straight to Beoordelen — exercising both DMN branches live. ## Notes for reviewers - Deviation from the issue's Option-A wording is deliberate and recorded (ADR-0016); the outcome is unchanged. - The self-service eIDAS→foreign wiring is out of scope here (this slice is area:domain + area:workflow); the domain submit accepts an optional `diplomaOrigin` so the foreign path is drivable. - Local green: domain unit 109, acceptance 15, `dotnet format`, Release build (0 errors), **domain mutation 95.39%** (break 90). The DMN/`businessRuleTask` REST wiring is CI-verified on verify-stack (no local full-stack run here). Reviewed-on: #101
This commit was merged in pull request #101.
This commit is contained in:
@@ -32,12 +32,15 @@ internal sealed class FakeWorkflowClient(string processInstanceId = "proc-1", Ac
|
||||
: IWorkflowClient
|
||||
{
|
||||
public RegistrationId? StartedFor { get; private set; }
|
||||
public DiplomaOrigin? StartedWithOrigin { 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);
|
||||
StartedFor = registrationId;
|
||||
StartedWithOrigin = diplomaOrigin;
|
||||
return Task.FromResult(processInstanceId);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ public class FlowableWorkflowClientTests
|
||||
var client = Client(capture.Responds(HttpStatusCode.Created, """{"id":"pi-1"}"""));
|
||||
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(HttpMethod.Post, capture.Seen!.Method);
|
||||
@@ -38,6 +38,22 @@ public class FlowableWorkflowClientTests
|
||||
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]
|
||||
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));
|
||||
|
||||
await Assert.ThrowsAsync<HttpRequestException>(
|
||||
() => client.StartRegistrationProcessAsync(RegistrationId.New()));
|
||||
() => client.StartRegistrationProcessAsync(RegistrationId.New(), DiplomaOrigin.Binnenlands));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -135,7 +151,7 @@ public class FlowableWorkflowClientTests
|
||||
var client = Client(capture.Responds(HttpStatusCode.Created, "null"));
|
||||
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => client.StartRegistrationProcessAsync(RegistrationId.New()));
|
||||
() => client.StartRegistrationProcessAsync(RegistrationId.New(), DiplomaOrigin.Binnenlands));
|
||||
Assert.Contains("empty process-instance", ex.Message);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,15 @@ public class RegistrationTests
|
||||
Assert.Null(registration.ProcessInstanceId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_registration_defaults_to_a_domestic_diploma()
|
||||
=> Assert.Equal(DiplomaOrigin.Binnenlands, Registration.Submit("123456782").DiplomaOrigin);
|
||||
|
||||
[Fact]
|
||||
public void A_foreign_diploma_submission_records_its_origin()
|
||||
=> Assert.Equal(DiplomaOrigin.Buitenlands,
|
||||
Registration.Submit("123456782", DiplomaOrigin.Buitenlands).DiplomaOrigin);
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
|
||||
@@ -25,6 +25,19 @@ public class SubmitRegistrationTests
|
||||
Assert.Equal(2, store.SaveCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Submitting_a_foreign_diploma_carries_its_origin_to_the_process()
|
||||
{
|
||||
var store = new FakeRegistrationStore();
|
||||
var workflow = new FakeWorkflowClient();
|
||||
var handler = new SubmitRegistration(store, workflow);
|
||||
|
||||
var id = await handler.HandleAsync(new SubmitRegistrationCommand("123456782", DiplomaOrigin.Buitenlands));
|
||||
|
||||
Assert.Equal(DiplomaOrigin.Buitenlands, (await store.GetAsync(id))!.DiplomaOrigin);
|
||||
Assert.Equal(DiplomaOrigin.Buitenlands, workflow.StartedWithOrigin);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Rejects_a_null_command_without_touching_the_store_or_workflow()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user