Compare commits

..

5 Commits

Author SHA1 Message Date
4a7dc07786 docs(workflow): ADR-0016 + demo note for diploma-eligibility DMN (S-13, refs #14)
Some checks failed
CI / lint (pull_request) Successful in 1m18s
CI / build (pull_request) Successful in 58s
CI / unit (pull_request) Successful in 1m6s
CI / frontend (pull_request) Successful in 2m36s
CI / mutation (pull_request) Successful in 5m14s
CI / verify-stack (pull_request) Failing after 4m23s
Record the DMN-as-businessRuleTask decision (from proposal #100) and add the S-13
demo walkthrough showing a foreign diploma routing through CBGV-advies.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 12:05:21 +02:00
5e1975ae84 test(workflow): diploma routing — acceptance + live foreign→CBGV path (S-13, refs #14)
Gherkin scenarios assert the submit carries the diploma origin (domestic/foreign)
into the process. verify-domain now submits a foreign registration and asserts it
parks at CBGVAdvies before Beoordelen, completes CBGV, then advances to Beoordelen
— exercising both DMN branches through the engine (the domestic DIRECT path is the
first registration already parking straight at Beoordelen).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 12:04:12 +02:00
dc9cf41dc8 feat(workflow): diploma-eligibility DMN + CBGV-advies routing (S-13, refs #14)
Add workflows/diploma-eligibility.dmn (origin → route) and wire the registratie
BPMN: a businessRuleTask evaluates it inline and an exclusive gateway routes a
foreign diploma through a new CBGVAdvies user task before Beoordelen, a domestic
one straight there (ADR-0016). Deploy the DMN to Flowable's DMN engine and stage
both workflow files into the fl-bpmn volume.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 12:01:37 +02:00
6955e0ff98 feat(workflow): emit diplomaOrigin start variable + accept it on submit (S-13, refs #14)
The Workflow Client now posts diplomaOrigin alongside registrationId when starting
the process, and the domain submit endpoint accepts an optional diplomaOrigin
(defaulting to domestic) so a foreign submission can be driven end-to-end. Adds the
aggregate + submit-forwarding tests now that the plumbing is in place.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 11:55:45 +02:00
1969297c97 test(workflow): start carries diploma origin as a process variable (S-13, refs #14)
Add DiplomaOrigin (Binnenlands/Buitenlands) to the Registration aggregate and
submit command, and thread it through the process-start port so the workflow's
DMN can route on it (ADR proposal #100). Failing Workflow Client test asserts the
diplomaOrigin start variable; the client takes the origin but does not emit it yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 11:54:28 +02:00
21 changed files with 456 additions and 57 deletions

View File

@@ -0,0 +1,66 @@
# ADR-0016: Diploma eligibility is a DMN evaluated inline as a BPMN businessRuleTask
- **Status:** Accepted
- **Date:** 2026-07-17
- **Deciders:** Respellion engineering
- **Relates to:** S-13 (#14); proposal #100. Builds on ADR-0009 (external-task worker / Workflow
Client), ADR-0014/0015 (the boundary-event and routing constructs on the registratie process).
## Context
S-13 adds flow 4: a foreign diploma must get an extra CBGV-advies assessment before beoordeling
(PRD §5). The eligibility decision — domestic goes straight to beoordeling, foreign routes through
CBGV-advies — needs a home. The Flowable REST app bundles a DMN engine, and the same
`repository/deployments` machinery that deploys `registratie.bpmn` can deploy a `.dmn`. §8.2 makes
the Workflow Client the only code that talks to Flowable; the PRD frames the workflow as "BPMN + DMN
governing the registration workflow" (Flowable as a peer orchestration module).
The issue's wording ("a DMN decision table evaluated by the Domain Service via Workflow Client")
suggests the domain reaches into Flowable's DMN API to evaluate the decision and feeds the result
back. That is one option; it is not the only one, and it is not the cleanest.
## Decision
**The diploma-eligibility DMN is deployed to Flowable and evaluated inline by the registratie process
as a `businessRuleTask`; an exclusive gateway routes on its output. The domain's only new job is to
carry the diploma origin and pass it into the process as a start variable.**
- **The decision lives in the workflow.** `workflows/diploma-eligibility.dmn` maps `diplomaOrigin`
`route` (`Buitenlands``CBGV_ADVIES`, otherwise `DIRECT`). A `businessRuleTask`
(`flowable:type="dmn"`, `decisionTableReferenceKey=diploma-eligibility`) runs it between
`OpenZaakAanmaken` and `Beoordelen`, and an exclusive gateway sends `CBGV_ADVIES` through a new
`CBGVAdvies` user task before `Beoordelen`, `DIRECT` straight there.
- **The domain carries the input, not the decision.** The `Registration` aggregate gains a
`DiplomaOrigin` (Binnenlands/Buitenlands); `SubmitRegistration` passes it to
`StartRegistrationProcessAsync`, which sets it as the `diplomaOrigin` start variable. The domain
never evaluates the DMN and never learns the route — that is the process's concern.
- **Deployed like the BPMN.** The DMN is version-controlled in `workflows/` and deployed to the DMN
engine by the same `flowable-init` step (via `dmn-api/dmn-repository/deployments`), staged into the
`fl-bpmn` volume alongside the BPMN.
## Consequences
**Positive**
- The eligibility rule is a first-class, inspectable workflow artefact (matching the PRD's BPMN+DMN
framing); business users can read/adjust the decision table without touching domain code.
- §8.2 stays clean: the Workflow Client remains the only code talking to Flowable, and the decision
runs inside the process the client already started — no domain→Flowable round-trip for a decision.
- The domain change is minimal and additive: one value on the aggregate, one start variable.
**Negative / costs**
- Deviates from #14's literal "evaluated by the Domain Service via Workflow Client" wording (noted on
the issue). The outcome — DMN decides eligibility, foreign diplomas get the CBGV step — is unchanged.
- The DMN and its `businessRuleTask` wiring are validated live (verify-domain drives a foreign
registration through CBGV-advies and a domestic one straight to beoordeling, exercising both
branches), not in unit tests — consistent with ADR-0009/0014/0015. The domain unit/acceptance tests
cover only that the origin is carried into the process.
## Alternatives considered
- **Domain evaluates the DMN via the Workflow Client** (the issue's wording). Rejected: it couples
the domain to Flowable for a decision and splits the routing across two places (domain computes,
BPMN branches), for no benefit over letting the engine that owns the process own the decision.
- **Eligibility rules in domain C#.** Rejected: it moves a governable business decision out of the
DMN the PRD calls for, and hard-codes what the reference app is meant to demonstrate as data.

View File

@@ -343,3 +343,42 @@ candidate group behandelaar → teamlead (§8.2).
> Both branches (escalate after 14 days; no-op when completed in time) are covered by the > Both branches (escalate after 14 days; no-op when completed in time) are covered by the
> `Een beoordeling escaleren` acceptance scenarios and the Workflow Client unit tests; the timer firing > `Een beoordeling escaleren` acceptance scenarios and the Workflow Client unit tests; the timer firing
> and reassignment are asserted live by the verify-domain check. > and reassignment are asserted live by the verify-domain check.
## S-13 — Diploma-eligibility: foreign diplomas route through CBGV-advies (#14, ADR-0016)
A registration's diploma origin decides its route. A DMN `businessRuleTask` in the registratie
process evaluates the `diploma-eligibility` decision on the `diplomaOrigin` start variable: a
**foreign** (Buitenlands) diploma is routed through an extra **CBGV-advies** user task before
beoordeling; a **domestic** (Binnenlands) one goes straight to beoordeling. The decision lives in the
DMN, not in code — a beheerder can read and adjust the decision table directly.
The self-service portal's eIDAS→foreign wiring is a later slice; for now the origin is submitted to
the domain directly, so the demo drives it through the domain endpoint:
```bash
# 1. Submit a foreign-diploma registration to the domain (note the returned Location/reference):
DOM=http://localhost:8080 # domain service
curl -s -i -X POST "$DOM/registrations" -H 'Content-Type: application/json' \
-d '{"bsn":"123456782","diplomaOrigin":"Buitenlands"}' | grep -i '^location:'
#
# 2. Once the zaak is opened, the process parks at the CBGV-advies task (NOT Beoordelen). In Flowable:
FL=http://localhost:8090/flowable-rest/service
curl -s -u rest-admin:test -X POST "$FL/query/tasks" -H 'Content-Type: application/json' \
-d '{"processDefinitionKey":"registratie","taskDefinitionKey":"CBGVAdvies"}' | python3 -m json.tool
#
# 3. Complete the CBGV-advies task; the case then advances to the regular Beoordelen task:
TID=$(curl -s -u rest-admin:test -X POST "$FL/query/tasks" -H 'Content-Type: application/json' \
-d '{"processDefinitionKey":"registratie","taskDefinitionKey":"CBGVAdvies"}' \
| python3 -c 'import sys,json;print(json.load(sys.stdin)["data"][0]["id"])')
curl -s -u rest-admin:test -X POST "$FL/runtime/tasks/$TID" \
-H 'Content-Type: application/json' -d '{"action":"complete"}'
# A domestic submission (default, or "Binnenlands") skips CBGV-advies and parks straight at Beoordelen.
```
**The path:** domain sets the `diplomaOrigin` start variable → registratie process DMN
`businessRuleTask` sets `route` → exclusive gateway → foreign: `CBGVAdvies` user task → `Beoordelen`;
domestic: `Beoordelen` directly (§8.2, ADR-0016).
> The domestic/foreign paths are covered by the `Een diploma op herkomst routeren` acceptance
> scenarios and unit tests (the origin is carried into the process); the DMN decision and the
> foreign→CBGV routing are asserted live by the verify-domain check.

View File

@@ -259,7 +259,7 @@ services:
flowable-init: flowable-init:
image: docker.io/curlimages/curl:latest image: docker.io/curlimages/curl:latest
restart: "no" restart: "no"
# registratie.bpmn is streamed into this external volume by infra/seed-config.sh. # registratie.bpmn + diploma-eligibility.dmn are streamed into this external volume by infra/seed-config.sh.
volumes: volumes:
- fl-bpmn:/work:ro - fl-bpmn:/work:ro
command: command:
@@ -267,7 +267,15 @@ services:
- -c - -c
- | - |
base=http://flowable-rest:8080/flowable-rest/service/repository/deployments base=http://flowable-rest:8080/flowable-rest/service/repository/deployments
dmnbase=http://flowable-rest:8080/flowable-rest/dmn-api/dmn-repository/deployments
until curl -sf -u rest-admin:test "$$base" >/dev/null 2>&1; do echo "waiting for flowable-rest..."; sleep 3; done until curl -sf -u rest-admin:test "$$base" >/dev/null 2>&1; do echo "waiting for flowable-rest..."; sleep 3; done
# Deploy the diploma-eligibility DMN to the DMN engine so the registratie process's
# businessRuleTask can resolve it by key (S-13, ADR-0016).
if curl -s -u rest-admin:test "$$dmnbase?name=diploma-eligibility" | grep -q '"name":"diploma-eligibility"'; then
echo "diploma-eligibility already deployed; skip"
else
curl -sf -u rest-admin:test -F 'file=@/work/diploma-eligibility.dmn;filename=diploma-eligibility.dmn' "$$dmnbase" >/dev/null && echo "deployed diploma-eligibility"
fi
if curl -s -u rest-admin:test "$$base?name=registratie" | grep -q '"name":"registratie"'; then if curl -s -u rest-admin:test "$$base?name=registratie" | grep -q '"name":"registratie"'; then
echo "registratie already deployed; skip" echo "registratie already deployed; skip"
else else

View File

@@ -35,12 +35,12 @@ services:
condition: service_healthy condition: service_healthy
networks: [cg] networks: [cg]
# Deploys workflows/registratie.bpmn via the REST API once flowable-rest is up. # Deploys workflows/registratie.bpmn + diploma-eligibility.dmn via the REST API once flowable-rest is up.
# Idempotent: skips if a deployment named "registratie" already exists. # Idempotent: skips each if a deployment of that name already exists.
flowable-init: flowable-init:
image: docker.io/curlimages/curl:latest image: docker.io/curlimages/curl:latest
restart: "no" restart: "no"
# registratie.bpmn is streamed into this external volume by infra/seed-config.sh. # registratie.bpmn + diploma-eligibility.dmn are streamed into this external volume by infra/seed-config.sh.
volumes: volumes:
- fl-bpmn:/work:ro - fl-bpmn:/work:ro
command: command:
@@ -48,7 +48,15 @@ services:
- -c - -c
- | - |
base=http://flowable-rest:8080/flowable-rest/service/repository/deployments base=http://flowable-rest:8080/flowable-rest/service/repository/deployments
dmnbase=http://flowable-rest:8080/flowable-rest/dmn-api/dmn-repository/deployments
until curl -sf -u rest-admin:test "$$base" >/dev/null 2>&1; do echo "waiting for flowable-rest..."; sleep 3; done until curl -sf -u rest-admin:test "$$base" >/dev/null 2>&1; do echo "waiting for flowable-rest..."; sleep 3; done
# Deploy the diploma-eligibility DMN to the DMN engine so the registratie process's
# businessRuleTask can resolve it by key (S-13, ADR-0016).
if curl -s -u rest-admin:test "$$dmnbase?name=diploma-eligibility" | grep -q '"name":"diploma-eligibility"'; then
echo "diploma-eligibility already deployed; skip"
else
curl -sf -u rest-admin:test -F 'file=@/work/diploma-eligibility.dmn;filename=diploma-eligibility.dmn' "$$dmnbase" >/dev/null && echo "deployed diploma-eligibility"
fi
if curl -s -u rest-admin:test "$$base?name=registratie" | grep -q '"name":"registratie"'; then if curl -s -u rest-admin:test "$$base?name=registratie" | grep -q '"name":"registratie"'; then
echo "registratie already deployed; skip" echo "registratie already deployed; skip"
else else

View File

@@ -158,6 +158,49 @@ done
[ -n "$gone" ] || { echo "FAIL — Beoordelen task for $reg_id2 still active after withdrawal" >&2; docker logs "$dom" 2>&1 | tail -15 >&2; exit 1; } [ -n "$gone" ] || { echo "FAIL — Beoordelen task for $reg_id2 still active after withdrawal" >&2; docker logs "$dom" 2>&1 | tail -15 >&2; exit 1; }
echo "OK — withdrawal cancelled the Beoordelen task; the registratie process ended (ingetrokken)" echo "OK — withdrawal cancelled the Beoordelen task; the registratie process ended (ingetrokken)"
# ── S-13: diploma-eligibility routing. A registration with a FOREIGN diploma must route through the
# extra CBGVAdvies user task before Beoordelen (the DMN businessRuleTask sets route=CBGV_ADVIES and the
# gateway branches, ADR-0016). The domestic DIRECT path is already proven by the first registration
# above, which parked straight at Beoordelen. ──────────────────────────────────────────────────────
cbgv_query='{"processDefinitionKey":"registratie","taskDefinitionKey":"CBGVAdvies","includeProcessVariables":true}'
echo ">> submitting a registration with a foreign diploma"
locf="$(docker run --rm --network "$net" curlimages/curl:latest \
-fsS -D - -o /dev/null -X POST "http://$dom_ip:8080/registrations" \
-H 'Content-Type: application/json' -d '{"bsn":"123456782","diplomaOrigin":"Buitenlands"}' \
| sed -n 's/\r$//; s/^[Ll]ocation: //p' | head -1)"
[ -n "$locf" ] || { echo "FAIL — foreign POST /registrations returned no Location" >&2; exit 1; }
reg_idf="${locf##*/}"
echo ">> foreign registration $reg_idf"
echo ">> polling Flowable for its CBGV-advies task (foreign diplomas route here first)"
cbgv_task=""
for _ in $(seq 1 30); do
resp="$(flcurl -X POST "$fl_base/query/tasks" -H 'Content-Type: application/json' -d "$cbgv_query" 2>/dev/null || true)"
cbgv_task="$(printf '%s' "$resp" | task_for_reg "$reg_idf")"
[ -n "$cbgv_task" ] && break
sleep 2
done
[ -n "$cbgv_task" ] || { echo "FAIL — no CBGVAdvies task appeared for the foreign registration $reg_idf" >&2; docker logs "$dom" 2>&1 | tail -15 >&2; exit 1; }
echo ">> CBGVAdvies task $cbgv_task is waiting"
echo ">> asserting it has NOT reached Beoordelen yet (still awaiting CBGV-advies)"
resp="$(flcurl -X POST "$fl_base/query/tasks" -H 'Content-Type: application/json' -d "$query")"
early="$(printf '%s' "$resp" | task_for_reg "$reg_idf")"
[ -z "$early" ] || { echo "FAIL — foreign registration reached Beoordelen ($early) before CBGV-advies" >&2; exit 1; }
echo ">> completing the CBGV-advies task"
flcurl -X POST "$fl_base/runtime/tasks/$cbgv_task" -H 'Content-Type: application/json' -d '{"action":"complete"}' >/dev/null
echo ">> asserting it now advances to Beoordelen"
onward=""
for _ in $(seq 1 15); do
resp="$(flcurl -X POST "$fl_base/query/tasks" -H 'Content-Type: application/json' -d "$query" 2>/dev/null || true)"
[ -n "$(printf '%s' "$resp" | task_for_reg "$reg_idf")" ] && { onward=1; break; }
sleep 2
done
[ -n "$onward" ] || { echo "FAIL — foreign registration did not reach Beoordelen after CBGV-advies" >&2; exit 1; }
echo "OK — foreign diploma routed through CBGV-advies, then on to Beoordelen (DMN + gateway)"
# ── S-14: escalation. A third registration parks at Beoordelen. We fire its 14-day boundary timer # ── S-14: escalation. A third registration parks at Beoordelen. We fire its 14-day boundary timer
# early via Flowable's management API (the timer job is moved to executable and run), which routes a # early via Flowable's management API (the timer job is moved to executable and run), which routes a
# parallel token to the BeoordelingEscaleren external task. The domain's escalation worker acquires # parallel token to the BeoordelingEscaleren external task. The domain's escalation worker acquires

View File

@@ -40,7 +40,7 @@ for key in "$@"; do
oz) populate rr-oz-config "$here/openzaak/setup_configuration/." ;; oz) populate rr-oz-config "$here/openzaak/setup_configuration/." ;;
nrc) populate rr-nrc-config "$here/opennotificaties/setup_configuration/." ;; nrc) populate rr-nrc-config "$here/opennotificaties/setup_configuration/." ;;
kc) populate rr-kc-realms "$here/keycloak/realms/." ;; kc) populate rr-kc-realms "$here/keycloak/realms/." ;;
fl) populate rr-fl-bpmn "$here/../workflows/registratie.bpmn" ;; fl) populate rr-fl-bpmn "$here/../workflows/." ;; # registratie.bpmn + diploma-eligibility.dmn
*) echo "unknown seed key: $key" >&2; exit 2 ;; *) echo "unknown seed key: $key" >&2; exit 2 ;;
esac esac
done done

View File

@@ -48,7 +48,12 @@ app.MapGet("/health", () => "Healthy");
// a location to read the registration's progress (ADR-0009, eventual consistency). // a location to read the registration's progress (ADR-0009, eventual consistency).
app.MapPost("/registrations", async (SubmitRegistrationRequest body, SubmitRegistration submit, CancellationToken ct) => app.MapPost("/registrations", async (SubmitRegistrationRequest body, SubmitRegistration submit, CancellationToken ct) =>
{ {
var id = await submit.HandleAsync(new SubmitRegistrationCommand(body.Bsn), ct); // Diploma origin defaults to domestic; a foreign (eIDAS) submission passes "Buitenlands" so the
// workflow's DMN routes it through CBGV-advies (S-13). An unknown value is a bad request.
if (!Enum.TryParse<DiplomaOrigin>(body.DiplomaOrigin, ignoreCase: true, out var origin) && body.DiplomaOrigin is not null)
return Results.BadRequest(new { error = $"Unknown diplomaOrigin '{body.DiplomaOrigin}'. Expected 'Binnenlands' or 'Buitenlands'." });
var id = await submit.HandleAsync(new SubmitRegistrationCommand(body.Bsn, origin), ct);
return Results.Accepted($"/registrations/{id}", new RegistrationResponse(id.ToString(), RegistrationStatus.Ingediend.ToString(), null)); return Results.Accepted($"/registrations/{id}", new RegistrationResponse(id.ToString(), RegistrationStatus.Ingediend.ToString(), null));
}); });
@@ -117,7 +122,7 @@ app.MapGet("/registrations/{id}", async (string id, IRegistrationStore store, Ca
await app.RunAsync(); await app.RunAsync();
public sealed record SubmitRegistrationRequest(string Bsn); public sealed record SubmitRegistrationRequest(string Bsn, string? DiplomaOrigin = null);
public sealed record DecideRequest(string Besluit); public sealed record DecideRequest(string Besluit);

View File

@@ -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

View File

@@ -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);

View 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,
}

View File

@@ -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>

View File

@@ -24,15 +24,20 @@ public sealed class FlowableWorkflowClient(HttpClient http, FlowableOptions opti
private const string BehandelaarGroup = "behandelaar"; private const string BehandelaarGroup = "behandelaar";
private const string TeamleadGroup = "teamlead"; private const string TeamleadGroup = "teamlead";
private const string RegistrationIdVariable = "registrationId"; private const string RegistrationIdVariable = "registrationId";
private const string DiplomaOriginVariable = "diplomaOrigin";
private const string ZaakUrlVariable = "zaakUrl"; private const string ZaakUrlVariable = "zaakUrl";
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,
[new Variable(RegistrationIdVariable, "string", registrationId.ToString())]); [
new Variable(RegistrationIdVariable, "string", registrationId.ToString()),
new Variable(DiplomaOriginVariable, "string", diplomaOrigin.ToString()),
]);
var created = await PostAsync<StartProcessRequest, ProcessInstance>( var created = await PostAsync<StartProcessRequest, ProcessInstance>(
"service/runtime/process-instances", request, ct) "service/runtime/process-instances", request, ct)

View File

@@ -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);
} }

View File

@@ -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);
} }

View File

@@ -16,6 +16,15 @@ public class RegistrationTests
Assert.Null(registration.ProcessInstanceId); 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] [Theory]
[InlineData("")] [InlineData("")]
[InlineData(" ")] [InlineData(" ")]

View File

@@ -25,6 +25,19 @@ public class SubmitRegistrationTests
Assert.Equal(2, store.SaveCount); 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] [Fact]
public async Task Rejects_a_null_command_without_touching_the_store_or_workflow() public async Task Rejects_a_null_command_without_touching_the_store_or_workflow()
{ {

View File

@@ -0,0 +1,19 @@
# language: en
# Drives S-13 (#14). A registration's diploma origin decides its route: a domestic (Binnenlands)
# diploma goes straight to beoordeling, a foreign (Buitenlands) one is routed through an extra
# CBGV-advies step (PRD flow 4). The decision itself is a DMN evaluated inside the workflow
# (ADR-0016); the domain's part — verified here — is carrying the origin into the process so the DMN
# can route on it. The DMN evaluation and the CBGV routing are verified live (verify-domain).
Feature: Een diploma op herkomst routeren
Als register wil ik een aanvraag met een buitenlands diploma extra laten toetsen
zodat een CBGV-advies wordt ingewonnen voordat een behandelaar beoordeelt.
Scenario: Een binnenlands diploma start de registratie als binnenlands
Given a zorgprofessional with a "Binnenlands" diploma
When they submit their registration
Then the registratie process is started carrying a "Binnenlands" diploma
Scenario: Een buitenlands diploma start de registratie als buitenlands
Given a zorgprofessional with a "Buitenlands" diploma
When they submit their registration
Then the registratie process is started carrying a "Buitenlands" diploma

View File

@@ -0,0 +1,36 @@
using Acceptance.Support;
using Big.Application;
using Big.Domain;
using Reqnroll;
using Xunit;
namespace Acceptance.Steps;
/// <summary>Bindings for <c>EenDiplomaRouteren.feature</c> (S-13). Drives the submit use case against
/// in-memory ports and asserts the registratie process is started carrying the diploma origin — the
/// domain's contribution to flow 4. The DMN evaluation and the foreign→CBGV-advies routing it drives
/// are verified live (verify-domain); one instance per scenario.</summary>
[Binding]
[Scope(Feature = "Een diploma op herkomst routeren")]
public sealed class EenDiplomaRouterenSteps
{
private readonly InMemoryRegistrationStore _store = new();
private readonly InMemoryWorkflowClient _workflow = new();
private DiplomaOrigin _origin;
[Given("a zorgprofessional with a \"(.*)\" diploma")]
public void GivenAZorgprofessionalWithADiploma(string origin)
=> _origin = Enum.Parse<DiplomaOrigin>(origin, ignoreCase: true);
[When("they submit their registration")]
public async Task WhenTheySubmitTheirRegistration()
=> await new SubmitRegistration(_store, _workflow).HandleAsync(
new SubmitRegistrationCommand("123456782", _origin));
[Then("the registratie process is started carrying a \"(.*)\" diploma")]
public void ThenTheProcessIsStartedCarryingTheDiploma(string expected)
{
Assert.NotNull(_workflow.StartedFor);
Assert.Equal(Enum.Parse<DiplomaOrigin>(expected, ignoreCase: true), _workflow.StartedWithOrigin);
}
}

View File

@@ -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);
} }

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Diploma-eligibility decision (S-13, ADR-0016). Evaluated inline by the registratie process as a
BPMN businessRuleTask: given the diploma's origin, it sets the `route` the process should take.
A foreign (Buitenlands) diploma routes through the extra CBGV-advies assessment step; a domestic
one (or anything else) goes DIRECT to beoordeling. FIRST hit policy: the foreign rule wins, and
the empty-input catch-all is the default. -->
<definitions xmlns="https://www.omg.org/spec/DMN/20191111/MODEL/"
xmlns:flowable="http://flowable.org/dmn"
id="diplomaEligibilityDefinitions"
name="Diploma eligibility"
namespace="http://respellion.nl/big/dmn">
<decision id="diploma-eligibility" name="Diploma eligibility">
<decisionTable id="dt-diploma-eligibility" hitPolicy="FIRST">
<input id="in-origin" label="Diploma origin">
<inputExpression id="ie-origin" typeRef="string">
<text>diplomaOrigin</text>
</inputExpression>
</input>
<output id="out-route" label="Route" name="route" typeRef="string"/>
<rule id="rule-foreign">
<inputEntry id="rule-foreign-in">
<text>"Buitenlands"</text>
</inputEntry>
<outputEntry id="rule-foreign-out">
<text>"CBGV_ADVIES"</text>
</outputEntry>
</rule>
<rule id="rule-default">
<inputEntry id="rule-default-in">
<text></text>
</inputEntry>
<outputEntry id="rule-default-out">
<text>"DIRECT"</text>
</outputEntry>
</rule>
</decisionTable>
</decision>
</definitions>

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" <definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:flowable="http://flowable.org/bpmn" 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:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC"
xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" 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 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 (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 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
businessRuleTask 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"/> <message id="Message_Ingetrokken" name="RegistratieIngetrokken"/>
<process id="registratie" name="Registratie ontvangen" isExecutable="true"> <process id="registratie" name="Registratie ontvangen" isExecutable="true">
@@ -33,7 +38,35 @@
flowable:type="external-worker" flowable:type="external-worker"
flowable:topic="OpenZaakAanmaken"/> 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). Flowable 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. -->
<businessRuleTask id="DiplomaEligibiliteit" name="Diploma-eligibiliteit bepalen" flowable:type="dmn">
<extensionElements>
<flowable:field name="decisionTableReferenceKey">
<flowable:string><![CDATA[diploma-eligibility]]></flowable:string>
</flowable:field>
</extensionElements>
</businessRuleTask>
<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"/> <userTask id="Beoordelen" name="Beoordelen" flowable:candidateGroups="behandelaar"/>
@@ -77,53 +110,79 @@
<omgdc:Bounds x="100" y="100" width="30" height="30"/> <omgdc:Bounds x="100" y="100" width="30" height="30"/>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="s_task" bpmnElement="OpenZaakAanmaken"> <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>
<bpmndi:BPMNShape id="s_beoordelen" bpmnElement="Beoordelen"> <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>
<bpmndi:BPMNShape id="s_end" bpmnElement="end"> <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:BPMNShape>
<bpmndi:BPMNEdge id="e_flow1" bpmnElement="flow1"> <bpmndi:BPMNEdge id="e_flow1" bpmnElement="flow1">
<omgdi:waypoint x="130" y="115"/> <omgdi:waypoint x="130" y="115"/>
<omgdi:waypoint x="200" y="115"/> <omgdi:waypoint x="180" y="115"/>
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="e_flow2" bpmnElement="flow2"> <bpmndi:BPMNEdge id="e_flow2" bpmnElement="flow2">
<omgdi:waypoint x="320" y="115"/> <omgdi:waypoint x="300" y="115"/>
<omgdi:waypoint x="390" 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>
<bpmndi:BPMNEdge id="e_flow3" bpmnElement="flow3"> <bpmndi:BPMNEdge id="e_flow3" bpmnElement="flow3">
<omgdi:waypoint x="510" y="115"/> <omgdi:waypoint x="750" y="115"/>
<omgdi:waypoint x="580" y="115"/> <omgdi:waypoint x="810" y="115"/>
</bpmndi:BPMNEdge> </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"> <bpmndi:BPMNEdge id="e_flow4" bpmnElement="flow4">
<omgdi:waypoint x="450" y="165"/> <omgdi:waypoint x="705" y="165"/>
<omgdi:waypoint x="450" y="220"/> <omgdi:waypoint x="705" y="250"/>
</bpmndi:BPMNEdge> </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"> <bpmndi:BPMNEdge id="e_flow5" bpmnElement="flow5">
<omgdi:waypoint x="495" y="70"/> <omgdi:waypoint x="715" y="70"/>
<omgdi:waypoint x="495" y="50"/> <omgdi:waypoint x="715" y="50"/>
<omgdi:waypoint x="580" y="50"/> <omgdi:waypoint x="800" y="50"/>
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="e_flow6" bpmnElement="flow6"> <bpmndi:BPMNEdge id="e_flow6" bpmnElement="flow6">
<omgdi:waypoint x="700" y="50"/> <omgdi:waypoint x="920" y="50"/>
<omgdi:waypoint x="760" y="50"/> <omgdi:waypoint x="970" y="50"/>
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
</bpmndi:BPMNPlane> </bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram> </bpmndi:BPMNDiagram>