feat(workflow): diploma-eligibility DMN routes foreign diplomas via CBGV-advies (S-13, closes #14) (#101)
CI / lint (push) Successful in 1m16s
CI / unit (push) Successful in 1m14s
CI / mutation (push) Successful in 5m14s
CI / build (push) Successful in 58s
CI / frontend (push) Successful in 2m29s
CI / verify-stack (push) Successful in 9m20s

## 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:
not
2026-07-20 07:26:52 +00:00
parent 7bcbc726ce
commit ccae27b3da
21 changed files with 497 additions and 67 deletions
+93 -32
View File
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:flowable="http://flowable.org/bpmn"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC"
xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI"
@@ -20,7 +21,11 @@
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. -->
only changes who may claim it.
S-13 adds diploma-eligibility routing: between OpenZaakAanmaken and Beoordelen a DMN service
task (flowable:type="dmn") evaluates the `diploma-eligibility` decision on the diplomaOrigin
start variable; an exclusive gateway routes a foreign diploma through the CBGV-advies user task
before Beoordelen, a domestic one straight there (ADR-0016). -->
<message id="Message_Ingetrokken" name="RegistratieIngetrokken"/>
<process id="registratie" name="Registratie ontvangen" isExecutable="true">
@@ -33,7 +38,37 @@
flowable:type="external-worker"
flowable:topic="OpenZaakAanmaken"/>
<sequenceFlow id="flow2" sourceRef="OpenZaakAanmaken" targetRef="Beoordelen"/>
<sequenceFlow id="flow2" sourceRef="OpenZaakAanmaken" targetRef="DiplomaEligibiliteit"/>
<!-- S-13: evaluate the diploma-eligibility DMN inline (ADR-0016). A Flowable DMN service task
(flowable:type="dmn" — NOT a businessRuleTask, whose default implementation is the legacy
Drools/KIE one that flowable-rest does not bundle) runs the deployed `diploma-eligibility`
decision against the diplomaOrigin start variable and sets the `route` output as a process
variable. The gateway then routes a foreign diploma through CBGV-advies, a domestic one
straight to Beoordelen. -->
<serviceTask id="DiplomaEligibiliteit" name="Diploma-eligibiliteit bepalen" flowable:type="dmn">
<extensionElements>
<flowable:field name="decisionTableReferenceKey">
<flowable:string><![CDATA[diploma-eligibility]]></flowable:string>
</flowable:field>
</extensionElements>
</serviceTask>
<sequenceFlow id="flow2a" sourceRef="DiplomaEligibiliteit" targetRef="RouteOpDiploma"/>
<exclusiveGateway id="RouteOpDiploma" name="Buitenlands diploma?" default="flowDirect"/>
<sequenceFlow id="flowCbgv" sourceRef="RouteOpDiploma" targetRef="CBGVAdvies">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${route == 'CBGV_ADVIES'}]]></conditionExpression>
</sequenceFlow>
<!-- The extra CBGV-style assessment for foreign diplomas (PRD flow 4). A CBGV medewerker
completes it, after which the case continues to the regular beoordeling. -->
<userTask id="CBGVAdvies" name="CBGV-advies" flowable:candidateGroups="cbgv"/>
<sequenceFlow id="flowCbgvNaarBeoordelen" sourceRef="CBGVAdvies" targetRef="Beoordelen"/>
<sequenceFlow id="flowDirect" sourceRef="RouteOpDiploma" targetRef="Beoordelen"/>
<userTask id="Beoordelen" name="Beoordelen" flowable:candidateGroups="behandelaar"/>
@@ -77,53 +112,79 @@
<omgdc:Bounds x="100" y="100" width="30" height="30"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="s_task" bpmnElement="OpenZaakAanmaken">
<omgdc:Bounds x="200" y="85" width="120" height="60"/>
<omgdc:Bounds x="180" y="85" width="120" height="60"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="s_dmn" bpmnElement="DiplomaEligibiliteit">
<omgdc:Bounds x="350" y="85" width="120" height="60"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="s_route" bpmnElement="RouteOpDiploma" isMarkerVisible="true">
<omgdc:Bounds x="520" y="90" width="40" height="40"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="s_cbgv" bpmnElement="CBGVAdvies">
<omgdc:Bounds x="500" y="200" width="120" height="60"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="s_beoordelen" bpmnElement="Beoordelen">
<omgdc:Bounds x="390" y="85" width="120" height="60"/>
<omgdc:Bounds x="630" y="85" width="120" height="60"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="s_end" bpmnElement="end">
<omgdc:Bounds x="580" y="100" width="30" height="30"/>
<omgdc:Bounds x="810" y="100" width="30" height="30"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="s_ingetrokken" bpmnElement="Ingetrokken">
<omgdc:Bounds x="690" y="135" width="30" height="30"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="s_endIngetrokken" bpmnElement="endIngetrokken">
<omgdc:Bounds x="690" y="250" width="30" height="30"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="s_escaleerTimer" bpmnElement="EscaleerTimer">
<omgdc:Bounds x="700" y="70" width="30" height="30"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="s_escaleerBeoordeling" bpmnElement="EscaleerBeoordeling">
<omgdc:Bounds x="800" y="20" width="120" height="60"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="s_endEscaleren" bpmnElement="endEscaleren">
<omgdc:Bounds x="970" y="35" width="30" height="30"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="e_flow1" bpmnElement="flow1">
<omgdi:waypoint x="130" y="115"/>
<omgdi:waypoint x="200" y="115"/>
<omgdi:waypoint x="180" y="115"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="e_flow2" bpmnElement="flow2">
<omgdi:waypoint x="320" y="115"/>
<omgdi:waypoint x="390" y="115"/>
<omgdi:waypoint x="300" y="115"/>
<omgdi:waypoint x="350" y="115"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="e_flow2a" bpmnElement="flow2a">
<omgdi:waypoint x="470" y="115"/>
<omgdi:waypoint x="520" y="110"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="e_flowDirect" bpmnElement="flowDirect">
<omgdi:waypoint x="560" y="110"/>
<omgdi:waypoint x="630" y="115"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="e_flowCbgv" bpmnElement="flowCbgv">
<omgdi:waypoint x="540" y="130"/>
<omgdi:waypoint x="540" y="200"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="e_flowCbgvNaarBeoordelen" bpmnElement="flowCbgvNaarBeoordelen">
<omgdi:waypoint x="620" y="230"/>
<omgdi:waypoint x="660" y="230"/>
<omgdi:waypoint x="660" y="145"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="e_flow3" bpmnElement="flow3">
<omgdi:waypoint x="510" y="115"/>
<omgdi:waypoint x="580" y="115"/>
<omgdi:waypoint x="750" y="115"/>
<omgdi:waypoint x="810" y="115"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="s_ingetrokken" bpmnElement="Ingetrokken">
<omgdc:Bounds x="435" y="135" width="30" height="30"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="s_endIngetrokken" bpmnElement="endIngetrokken">
<omgdc:Bounds x="435" y="220" width="30" height="30"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="e_flow4" bpmnElement="flow4">
<omgdi:waypoint x="450" y="165"/>
<omgdi:waypoint x="450" y="220"/>
<omgdi:waypoint x="705" y="165"/>
<omgdi:waypoint x="705" y="250"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="s_escaleerTimer" bpmnElement="EscaleerTimer">
<omgdc:Bounds x="480" y="70" width="30" height="30"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="s_escaleerBeoordeling" bpmnElement="EscaleerBeoordeling">
<omgdc:Bounds x="580" y="20" width="120" height="60"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="s_endEscaleren" bpmnElement="endEscaleren">
<omgdc:Bounds x="760" y="35" width="30" height="30"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="e_flow5" bpmnElement="flow5">
<omgdi:waypoint x="495" y="70"/>
<omgdi:waypoint x="495" y="50"/>
<omgdi:waypoint x="580" y="50"/>
<omgdi:waypoint x="715" y="70"/>
<omgdi:waypoint x="715" y="50"/>
<omgdi:waypoint x="800" y="50"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="e_flow6" bpmnElement="flow6">
<omgdi:waypoint x="700" y="50"/>
<omgdi:waypoint x="760" y="50"/>
<omgdi:waypoint x="920" y="50"/>
<omgdi:waypoint x="970" y="50"/>
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>