diff --git a/.github/workflows/azure-backend-acc.yml b/.github/workflows/azure-backend-acc.yml index c25bf1d..b4c2191 100644 --- a/.github/workflows/azure-backend-acc.yml +++ b/.github/workflows/azure-backend-acc.yml @@ -57,10 +57,17 @@ jobs: # Copy package.json to deploy root (NOT inside dist) cp package.json deploy/ + # Copy SHACL shape files — read at runtime from /shapes + # (SHAPES_ROOT resolves to deploy/shapes via dist/services/../../shapes). + # tsc does not emit these .ttl assets, so copy them explicitly. + cp -r shapes deploy/ + # Verify structure echo "Deployment structure:" ls -la deploy/ ls -la deploy/dist/ + echo "SHACL shapes:" + ls -R deploy/shapes/ # Install production dependencies in deploy folder cd deploy diff --git a/.gitignore b/.gitignore index 044db93..e35e761 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,9 @@ dist-ssr *.d.ts *.d.ts.map +# Hand-authored ambient declarations (not generated output) — keep tracked +!packages/backend/src/types/shacl-rdf.d.ts + # But keep the source JSON !testData.json @@ -42,6 +45,12 @@ dist-ssr *.sw? .claudesync +# Claude Code local config & per-machine memory (machine-specific, not portable) +.claude/ + +# Local env overrides (committed envs are .env.development / .acceptance / .production) +.env + # flatten script & dir flatten_directory.sh flattened diff --git a/dso-integration-phase-plan.md b/dso-integration-phase-plan.md new file mode 100644 index 0000000..4822341 --- /dev/null +++ b/dso-integration-phase-plan.md @@ -0,0 +1,287 @@ +# DSO Integration — Phase Plan updated + +## End goal + +A **DSO-driven AWB process bundle**: given a location and a werkzaamheid, LDE produces a deployable Operaton package — BPMN subprocess + DMN + form schema + document template — all seeded from authoritative DSO source data rather than authored by hand. + +--- + +## The data flow + +``` +Werkzaamheid (what someone wants to do) + ↓ linked to +Activiteit (the legal activity at that location) + ↓ has +Regelbeheerobjecten (RTR detail call) + ├── indieningsvereisten → functioneleStructuurRef → STTR file → Form scaffold (form-js JSON) + ├── conclusie → functioneleStructuurRef → STTR file → DMN decision model (.dmn) + └── maatregelen → functioneleStructuurRef → STTR file → Document template scaffold + ↓ governed by +BPMN subprocess (the AWB procedural shell, linked via ronl:dsoActiviteitUrn) +``` + +### The `functioneleStructuurRef` is the pivot + +Every `regelBeheerobject` returned by the RTR activity detail call carries a `functioneleStructuurRef`. This URI is the key that links the RTR taxonomy to the STTR files in the Uitvoeren Gegevens API. + +Two formats exist: + +- **Zoekinterface** (werkzaamheid-level): `http://toepasbare-regels.omgevingswet.overheid.nl/werkzaamheden/id/concept/GebouwPlaatsen` +- **RTR detail** (regelBeheerobject-level): `http://toepasbare-regels.omgevingswet.overheid.nl/00000001005024249000/id/concept/Conclusienl.imow-gm0995.activiteit.HoutopstandVellen` + +The authority-scoped RTR detail format is the one used for `GET /toepasbareRegels?functioneleStructuurRef=...`. + +--- + +## APIs + +### Integrated + +| API | Version | Used for | +|---|---|---| +| RTR CRUD (raadplegen) | v2 | Activity list, detail, OIN-based browse via `_zoek` with `bestuursorgaan.oin` | +| Catalogus opvragen | v3 | Concepts search (Stelselcatalogus) | +| Zoekinterface | v2.2.3 | Werkzaamheden search + autocomplete, `functioneleStructuurRef` retrieval | +| Opvragen Werkzaamheden | v1 | Werkzaamheid version history | +| Uitvoeren Gegevens | v1 | STTR metadata + download by `functioneleStructuurRef`, DMN extraction, form scaffold | + +### Pending + +| API | Key capability | +|---|---| +| Samengestelde RTR services v2 | Rule type completeness check for werkzaamheid + location | + +--- + +## STTR file format — confirmed from HoutopstandVellen (Lelystad) + +The STTR files produced by the Sogelink STTR Builder have `` as their root element. There is no outer `` envelope. The DSO domain-specific content lives inside ``. + +### conclusie STTR (identifier 105946) + +Root: `` + +Content: +- `dmn:extensionElements` + - `inter:regelgroepen` — named groups of questions + - `uitv:uitvoeringsregels` — questionnaire questions (used to produce the DMN inputs) + - `sttrbuilder:dependencyInformation` — decision paths and outcome descriptions +- `dmn:inputData` elements — one per `uitvoeringsregel`, typed (boolean, string, number) +- `dmn:decision` elements — full decision tables implementing the check logic + +The file is **structurally** a DMN but is **not deployable as-extracted** — the Sogelink STTR Builder emits DMN 1.2 with `` elements lacking ids, FEEL-unsafe variable names, and no history TTL, which Operaton refuses. "Extract DMN" returns the full `` element fully normalized for Operaton (DMN 1.3 + input ids + FEEL-safe names + output typeRefs + `camunda:historyTimeToLive` — see Phase 4.1), so it is deploy-ready as handed off. + +### indieningsvereisten STTR (identifier 105947) + +Root: `` + +Content: +- `dmn:extensionElements` + - `inter:regelgroepen` — named groups (e.g. Kappen, Eigendom, Algemene bijlagen) + - `uitv:uitvoeringsregels` — questionnaire: `uitv:vraag` (question) or `uitv:bijlage` (attachment) + - `sttrbuilder:dependencyInformation > sttrbuilder:formDependencyInformation` — conditional visibility rules + +The questionnaire is extracted from `uitv:uitvoeringsregels`, not from the decision tables. + +### Form scaffold mapping + +| `uitv:gegevensType` | `inter:inputType` | form-js type | +|---|---|---| +| `boolean` | — | `checkbox` | +| `list` + `uitv:opties` | — | `select` with option values | +| `number` | — | `number` | +| `string` | `textarea` | `textarea` | +| `string` | `text` or absent | `textfield` | +| `uitv:bijlage` | — | `textfield` with `[Bijlage]` label prefix | +| `uitv:geoVerwijzing` | — | skipped (not representable in form-js) | + +--- + +## Known API quirks + +- **Uitvoeren Gegevens identifier type:** `identifier` is a number (e.g. `105946`), not a string. `begindatum` is lowercase `d`. +- **RTR typering casing:** the RTR API returns `"Conclusie"` and `"Indieningsvereisten"` (capitalised), not lowercase. All comparisons must be case-insensitive. +- **Opvragen Werkzaamheden `_expandScope`:** the spec documents `logischeRelaties` as valid but the runtime rejects it. Current workaround: call without expand — version history only, no keywords or logical relations. +- **RTR `_wijzigingen`:** is a delta sync endpoint, not a browse endpoint. Use `POST /activiteiten/_zoek` with `bestuursorgaan.oin` for OIN-based browsing. +- **Date format:** `dd-MM-yyyy` throughout. +- **STTR download:** the Uitvoeren Gegevens API path is `/toepasbareRegels/{identifier}/sttrBestand` (not `/sttr`). + +--- + +## Phase plan + +### Phase 1 — Navigate ✅ Done (v1.5.0 – v1.5.3) + +- DSO Explorer panel: Concepts, Works, and Activities tabs +- Concepts: full-text search across the Stelselcatalogus +- Works: werkzaamheden search + autocomplete, version history, `functioneleStructuurRef` per result +- Activities: RTR list with OIN presets, date filtering, activity detail panel with child/parent navigation, rule types present badges +- DSO environment toggle (pre / prod) in Settings +- `ronl:dsoActiviteitUrn` moddleExtension on `bpmn:Process` with live RTR verification + +### Phase 2 — Locate + +**Step 2a — Applicable Rules panel in Activity Detail ✅ Done (v1.9.3)** + +- For each `regelBeheerobject` with a `functioneleStructuurRef`, calls `GET /toepasbareRegels?functioneleStructuurRef=...` against the Uitvoeren Gegevens API +- Shows per rule entry: validity date (`begindatum`), STTR version (`sttrVersie`), numeric identifier +- Action buttons per entry (see Phase 4) +- Supported for `Conclusie` and `Indieningsvereisten`; `Maatregelen` displayed when present, no action buttons yet + +**Step 2d — Activities tab name search (location-scoped) ✅ Done** + +- Fixing a location preset (Lelystad / Flevoland) loads that authority's full activity set in one `activiteiten/_zoek` call (`pageSize=200`; the API caps `size` to the actual count — Lelystad 136, Flevoland 50) +- A name search box appears only when a location is fixed, live-filtering the loaded list by `omschrijving` (case-insensitive substring) — purely client-side, no extra API calls +- Solves the problem of finding e.g. `nl.imow-gm0995.activiteit.HoutopstandVellen` ("Boom kappen of houtopstand vellen") without walking the activity hierarchy +- Pagination arrows hidden in OIN mode (single full load); footer shows `N of M activities` when filtering +- The unscoped `/activiteiten` endpoint (date-only, large, paginated) is deliberately not wired to this search + +**Step 2b — Works tab → Applicable Rules shortcut ⏳ Pending** + +- Each werkzaamheid result carries its own `functioneleStructuurRef` — wire a "View applicable rules" action that queries the Uitvoeren Gegevens API without navigating through the activity hierarchy + +**Step 2c — Rule type completeness check ⏳ Pending** + +- `POST /regelbeheerobjectedtypen` (Samengestelde services) for a werkzaamheid + location: confirm which rule types are available before attempting generation + +### Phase 3 — Map + +- ✅ `ronl:dsoActiviteitUrn` on `bpmn:Process` — persisted in BPMN XML +- ✅ `DsoActiviteitSelector` in BPMN Modeler footer: paste URN, verify live, save +- ✅ BPMN shell/subprocess auto-detection on import and startup (v1.9.2) +- ✅ `TreeFellingPermitSubProcess` linked to `nl.imow-gm0995.activiteit.HoutopstandVellen` +- ⏳ Indieningsvereisten checklist in BPMN properties panel +- ⏳ Rule type coverage badges on subprocess element (✓ Form ✓ Decision ✓ Document) + +### Phase 4 — Generate + +**Step 4.1 — DMN from conclusie STTR ✅ Done (v1.9.3)** + +- Backend route `GET /v1/dso/toepasbare-regels/:id/dmn` +- Extracts `...` from the STTR and returns it as a standalone `.dmn` file +- ↓ Extract DMN button in the Applicable Rules panel (Conclusie entries only) + +**Operaton deploy + eval normalization (`normalizeDmnForOperaton` in `dso.service.ts`)** — verified against `operaton.open-regels.nl` (engine `1.0.0`). A raw STTR DMN both fails to *deploy* (`ENGINE-22004 Unable to transform DMN resource`) and, once deployable, fails to *evaluate*. Fixes and the LDE/CPSV split: + +- ✅ **#1 DMN 1.2 → 1.3 (LDE):** the engine only transforms DMN 1.3, so the four spec namespaces `…/20180521/…` → `…/20191111/…` (incl. http→https). DSO target namespace untouched. +- ✅ **#2 Missing input ids (LDE):** STTR `` / `` have no `id` (rejected by the engine); a stable id is injected where absent. +- ✅ **#3 `camunda:historyTimeToLive` (LDE):** this Operaton enforces HTTL at deploy. `ensureHistoryTimeToLive` declares the `camunda` namespace and stamps `historyTimeToLive="180"` (matching the LDE BPMN-template convention) on each ``. Design principle: **LDE hands off a deploy-ready DMN** — the CPSV Editor deploys it as-is, no patching. (Decisions that already declare HTTL are left untouched.) +- ✅ **#4 FEEL-safe variable names (LDE):** the STTR names variables with hyphenated GUIDs (`uitv__`) and spaces (`Boom kappen … _cross`). FEEL reads `-` as subtraction and spaces as separators, so the DMN deploys but **fails to evaluate** (`Exception while evaluating decision`). `sanitizeFeelNames` renames every `` name to a FEEL-safe identifier and rewrites the exact `` references; rule logic and output value literals are untouched. This is also the root cause of the validator's INT-007 warnings. +- ✅ **BIZ-004 output typeRef (LDE):** untyped `` columns get the type of their decision's result `` (default `string`). +- Ruled out (not causes of deploy failure): `outputLabel` on decisionTable. +- Verified: STTR 105946 → after #1–#4 + BIZ-004 the LDE output deploys **directly** (no deployer patching) — all 7 decisions — **and** the root decision evaluates with HTTP 200 (previously a 500 FEEL exception); a minimal `cleanVar` vs `uitv__…-…` test isolated hyphens as the eval breaker. +- **"Import into LDE" — decided: publish via the CPSV Editor → TriplyDB.** Unlike forms, DMNs have no local asset store: the DMN picker (`DmnTemplateSelector`) is populated from **TriplyDB via SPARQL** (`sparqlService.getAllDmns`) and DMN XML is fetched from **Operaton** by identifier; there is no `upsertDmn`/`POST /v1/dmns`. Rather than duplicate a publish pipeline in LDE, the extracted DMN is handed to the **CPSV Editor** (`ttl-editor`, separate codebase), which already (a) deploys DMN XML to Operaton (`/engine-rest/deployment/create`) and (b) publishes the DMN's RDF to the same TriplyDB graph LDE reads. Once published, the DMN appears in LDE's picker automatically — no LDE-side store required. + - **RDF contract** a DMN must satisfy to appear in LDE (`sparql.service.ts` `getAllDmns` + `ttl-editor` `ttlGenerator.generateDmnSection`): a node `a cprmv:DecisionModel` with `dct:identifier` (→ `camunda:decisionRef`) and `dct:title` required; `cprmv:implements `, `cprmv:deploymentId`, `cprmv:deployedAt` expected. Publishing is service-centric — the DMN rides along with a `cpsv:PublicService` definition. + - **DSO → CPSV-AP mapping:** activity `omschrijving` → PublicService title; `bestuursorgaan` (via `authorityLabel`) → competent authority; primary decision key in the extracted DMN → `dct:identifier`; `functioneleStructuurRef` → provenance. + +**Step 4.1b — DSO → CPSV Editor handoff deep-link ✅ Done (LDE side)** + +- "Publish via CPSV Editor" button on Conclusie entries in the Applicable Rules panel, alongside ↓ Extract DMN +- Opens the CPSV Editor with a deep-link carrying identifiers + DSO metadata; the DMN XML is **not** in the URL — the CPSV Editor fetches it from the shared LDE backend +- **Deep-link contract** (the interface the CPSV Editor chat must implement): + ``` + /?dsoImport=dmn + &dmnId= + &env= + &activityName= + &authority= + &activityUrn= + &fsRef= + ``` + - CPSV Editor fetches DMN XML from: `GET /v1/dso/toepasbare-regels//dmn?env=` (same backend both apps already share via `REACT_APP_BACKEND_URL` / `VITE_API_BASE_URL`) + - `VITE_CPSV_EDITOR_URL` configured per environment (dev `http://localhost:3002`, acc `https://acc.cpsv-editor.open-regels.nl`, prod `https://cpsv-editor.open-regels.nl`) +- **Pending (CPSV Editor chat):** consume the `dsoImport=dmn` params — fetch the DMN, prefill DMNTab + Service/Organization tabs from the DSO metadata, then deploy + publish through the existing pipeline. + +**Step 4.2 — Form scaffold from indieningsvereisten STTR ✅ Done (v1.9.3)** + +- Backend route `GET /v1/dso/toepasbare-regels/:id/form-scaffold` +- Parses `uitv:uitvoeringsregels` from `dmn:extensionElements`, maps questions to form-js field types +- ↓ Form scaffold button in the Applicable Rules panel (Indieningsvereisten entries only) +- Output is a ready-to-use form-js JSON schema +- ✅ **"Import into LDE" button** — saves the scaffold straight into the LDE form store via `FormService.saveForm` (localStorage + `POST /v1/assets/forms`), stamped with the execution-platform metadata the editor/Operaton deploy expect. Appears in the Form Editor as a `wip` draft named ` — Submission requirements`, tagged with the authority as organization. Unlike the DMN side, forms already have a local asset store, so no backend changes were needed. + +**Step 4.3 — Document template from maatregelen STTR ⏳ Pending** + +- Each `` element → beschikking document zone: `maatregeltekst` → label, `toelichting` → body +- Requires a `maatregelen` activity — none confirmed in the current test set + +**Step 4.4 — BPMN subprocess scaffold ⏳ Pending** + +- Generate complete subprocess XML wired to imported DMN, form, and document template via `ronl:` moddleExtensions + +### Phase 5 — Deploy ⏳ Pending + +- Wire subprocess into AWB shell as call activity +- Deploy bundle via existing LDE mechanism: BPMN + DMN + form + document template +- Process runnable in Operaton with DSO-authoritative rule content + +--- + +## Current state — v1.9.3 + +| Capability | Status | +|---|---| +| DSO Concepts tab | ✅ Live | +| DSO Works tab — search + autocomplete + version history | ✅ Live | +| DSO Activities tab — list + OIN presets + date filter | ✅ Live | +| DSO Activity Detail panel | ✅ Live | +| DSO environment toggle (pre / prod) | ✅ Live | +| `ronl:dsoActiviteitUrn` on BPMN subprocess with live verification | ✅ Live | +| BPMN shell/subprocess auto-detection on import and startup | ✅ Live | +| Applicable Rules panel (Phase 2a) | ✅ Live | +| Activities tab name search (location-scoped, Phase 2d) | ✅ Live | +| ↓ STTR download (conclusie + indieningsvereisten) | ✅ Live | +| ↓ Extract DMN (conclusie) | ✅ Live | +| ↓ Form scaffold (indieningsvereisten) | ✅ Live | +| Werkzaamheid keywords + logical relations | ⏳ Pending (`_expandScope` enum) | +| Works tab → Applicable Rules shortcut (Phase 2b) | ⏳ Pending | +| Rule type completeness check (Phase 2c) | ⏳ Pending | +| Import form scaffold into LDE from STTR | ✅ Live | +| DMN → CPSV Editor publish handoff deep-link (Phase 4.1b, LDE side) | ✅ Live | +| DMN handoff consumed in CPSV Editor (fetch + prefill + publish) | ⏳ Pending (CPSV Editor chat) | +| Maatregelen → document template scaffold (Phase 4.3) | ⏳ Pending | +| BPMN subprocess scaffold (Phase 4.4) | ⏳ Pending | +| Deploy bundle to Operaton (Phase 5) | ⏳ Pending | + +--- + +## Reference: HoutopstandVellen — confirmed working example + +**Activity:** Boom kappen of houtopstand vellen · Gemeente Lelystad (GM0995) +**URN:** `nl.imow-gm0995.activiteit.HoutopstandVellen` +**OIN:** `00000001005024249000` +**Environment:** pre-production and production +**BPMN link:** `TreeFellingPermitSubProcess.bpmn` via `ronl:dsoActiviteitUrn` + +| Rule type | `functioneleStructuurRef` | Uitvoeren Gegevens identifier | +|---|---|---| +| Conclusie | `http://toepasbare-regels.omgevingswet.overheid.nl/00000001005024249000/id/concept/Conclusienl.imow-gm0995.activiteit.HoutopstandVellen` | 105946 | +| Indieningsvereisten | `http://toepasbare-regels.omgevingswet.overheid.nl/00000001005024249000/id/concept/IndieningsvereistenVergunningnl.imow-gm0995.activiteit.HoutopstandVellen` | 105947 | + +Generated artifacts committed to `examples/organizations/flevoland/STTR/`: + +| File | Content | +|---|---| +| `sttr-105946.xml` | Raw STTR (conclusie) | +| `sttr-105947.xml` | Raw STTR (indieningsvereisten) | +| `decision-105946.dmn` | Extracted DMN — ready for import into LDE / Operaton | +| `form-scaffold-105947.json` | form-js schema — 10 fields (select, textarea, textfield, number, checkbox, bijlage) | + +--- + +## Reference: other confirmed test activities + +### "Bed & Breakfast starten" (Lelystad, production) + +- **URN:** `nl.imow-gm0995.activiteit.a42ec23b8e4d464b8d32a1e88ac6d4cd` +- **Rule types:** Conclusie ✅ + Indieningsvereisten ✅ +- **Note:** `toonbaar: false` — not visible in the public Omgevingsloket but fully queryable via the API +- **Conclusie `functioneleStructuurRef`:** `http://toepasbare-regels.omgevingswet.overheid.nl/00000001005024249000/id/concept/Conclusienl.imow-gm0995.activiteit.a42ec23b8e4d464b8d32a1e88ac6d4cd` +- **Indieningsvereisten `functioneleStructuurRef`:** `http://toepasbare-regels.omgevingswet.overheid.nl/00000001005024249000/id/concept/IndieningsvereistenVergunningnl.imow-gm0995.activiteit.a42ec23b8e4d464b8d32a1e88ac6d4cd` + +### "Boom kappen" (Groningen, pre-production) + +- **URN:** `nl.imow-gm0014.activiteit.1d52a3b09a7a4b2f846ae1e171f6678d` +- **Authority:** gemeente GM0014 +- **Rule types:** Conclusie ✅ diff --git a/examples/organizations/flevoland/STTR/Indieningsvereisten.xml b/examples/organizations/flevoland/STTR/Indieningsvereisten.xml new file mode 100644 index 0000000..37ea567 --- /dev/null +++ b/examples/organizations/flevoland/STTR/Indieningsvereisten.xml @@ -0,0 +1,1102 @@ + + + + + + Kappen + 1 + + + Eigendom + 2 + + + Algemene bijlagen + 3 + + + + + + true + true + 10 + + list + + + enkelAntwoord + + 1 + + + + 2 + + + + + + false + + + + + true + true + 20 + + string + + textarea + + + false + + + + + true + false + 30 + + string + + text + + + false + + + + + true + true + 40 + + number + + + + + false + + + + + true + false + 50 + + string + + textarea + + + false + + + + + true + false + 60 + + boolean + + + + false + + + + + true + false + 70 + + boolean + + + + false + + + + + true + false + 80 + + + + + + false + + + + + true + true + 90 + + + + + + + false + + + + + true + true + 100 + + + + + + + false + + + + + 1.0.3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + _5acd1773-b81c-436f-a91e-d37546ad5e24 + + + + + _06480753-bdb0-4d23-8fd7-8941fad930f2 + + + + + _c1720883-89cf-47e5-97f0-36ef4d5fab05 + + + + + _2ebda717-baaf-4b56-8cc8-e8761d7ab632 + + + + + _94af6398-3d9e-450b-bb64-12b1728fee00 + + + + + _acbdb84c-54b5-4bd9-98b4-bf7e676bf3d4 + + + + + _82778bb0-6319-4d43-985e-d3d4c4ea2958 + + + + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + + + false + + + - + + + - + + + - + + + - + + + - + + + - + + + false + + + + + - + + + false + + + - + + + - + + + - + + + - + + + - + + + false + + + + + - + + + - + + + false + + + - + + + - + + + - + + + - + + + false + + + + + - + + + - + + + - + + + false + + + - + + + - + + + - + + + false + + + + + - + + + - + + + - + + + - + + + false + + + - + + + - + + + false + + + + + - + + + - + + + - + + + - + + + - + + + false + + + - + + + false + + + + + - + + + - + + + - + + + - + + + - + + + - + + + false + + + false + + + + + + + + + + + + + + + + + + + true + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + if ?=null then false else contains(?,"Anders") + + + false + + + false + + + + + if ?=null then false else not(contains(?,"Anders")) + + + - + + + true + + + + + + + + + + + + + + + + + + + true + + + true + + + + + false + + + false + + + + + + + + + + + + + + + + + + + not(null) + + + true + + + + + + + + + + + + + + + + + + + true + + + true + + + + + false + + + false + + + + + + + + + + + + + + + + + + + not(null) + + + true + + + + + + + + + + + + + + + + + + + true + + + true + + + + + false + + + false + + + + + + + + + + + + + + + + + + + not(null) + + + true + + + + + + + + + + + + + + + + + + + true + + + true + + + + + false + + + false + + + + + + + + + + + + + + + + + + + not(null) + + + true + + + + + + + + + + + + + + + + + + + true + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + false + + + false + + + false + + + + + true + + + - + + + true + + + + + + + + + + + + + + + + + + + true + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + false + + + false + + + + + false + + + - + + + true + + + + + + + + + + + + + + + + + + + true + + + true + + + + + false + + + false + + + + + + + + + + + + + + + + + + + not(null) + + + true + + + + + + + + + + + + + + + + + + + true + + + true + + + + + false + + + false + + + + + + + + + + + + + + + + + + + not(null) + + + true + + + + + + + + + + + + + + + + + + + true + + + true + + + + + false + + + false + + + + + + + + + + + + + + + + + + + not(null) + + + true + + + + + diff --git a/examples/organizations/flevoland/STTR/conclusie.xml b/examples/organizations/flevoland/STTR/conclusie.xml new file mode 100644 index 0000000..93531a0 --- /dev/null +++ b/examples/organizations/flevoland/STTR/conclusie.xml @@ -0,0 +1,787 @@ + + + + + + Boom kappen of houtopstand vellen_Gemeente Lelystad + 1 + + + + + + 10 + + boolean + + + + + + true + + + + + 20 + + boolean + + + + + true + + + + + 30 + + + + + + false + + + + + 40 + + boolean + + + + false + + + + + 50 + + boolean + + + + false + + + + + 1.0.3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + deels + nee + + + + + + + + + + + + + + + + + + + + deels + nee + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + if ?=null then false else contains(?,"Niet van toepassing pad 0_") + + + not(null) + + + + + + + + not(null) + + + if ?=null then false else contains(?,"Vergunningplicht pad 1_") + + + + + + + + + + + + + not(null) + + + if ?=null then false else contains(?,"Vergunningplicht pad 2_") + + + + + + + + + + + + + not(null) + + + if ?=null then false else contains(?,"Vergunningplicht pad 3_") + + + + + + + + + + + + + "no hit" + + + "no hit" + + + + + + + + "Toestemmingsvrij" + + + + + + + + + + + + + + + + + + + + + + + + + + + "no hit" + + + "no hit" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + - + + + + + + + + - + + + + + + - + + + + + + + + - + + + - + + + + + + + + + + + "no hit" + + + "no hit" + + + "no hit" + + + "no hit" + + + + + + + + + + + + + uitv__4483fe58-57b7-4e71-bf19-15783f0f9d92 + + + + + + false + + + + + + + + true + + + "no hit" + + + + + + + + + + + + + + + + uitv__4483fe58-57b7-4e71-bf19-15783f0f9d92 + + + + + uitv__864933e7-4ea9-45a2-ae17-d8b1a4df34d7 + + + + + + true + + + true + + + + + + + + false + + + - + + + "no hit" + + + + + - + + + false + + + "no hit" + + + + + + + + + + + + + + + + + + + + + + uitv__4483fe58-57b7-4e71-bf19-15783f0f9d92 + + + + + uitv__864933e7-4ea9-45a2-ae17-d8b1a4df34d7 + + + + + uitv__7b6ed60e-184f-4d93-a186-828dc9182890 + + + + + uitv__648edf88-a9ce-4726-b043-a3e18fa434c5 + + + + + + true + + + false + + + "deels","nee" + + + false + + + + + + + + false + + + - + + + - + + + - + + + "no hit" + + + + + - + + + true + + + - + + + - + + + "no hit" + + + + + - + + + - + + + + + + - + + + "no hit" + + + + + - + + + - + + + - + + + true + + + "no hit" + + + + + + + + + + + + + + + + + + + + + + + + + uitv__4483fe58-57b7-4e71-bf19-15783f0f9d92 + + + + + uitv__864933e7-4ea9-45a2-ae17-d8b1a4df34d7 + + + + + uitv__7b6ed60e-184f-4d93-a186-828dc9182890 + + + + + uitv__648edf88-a9ce-4726-b043-a3e18fa434c5 + + + + + uitv__8155d4e0-7003-44ca-8f42-c97afa52f0c7 + + + + + + true + + + false + + + "deels","nee" + + + true + + + false + + + + + + + + false + + + - + + + - + + + - + + + - + + + "no hit" + + + + + - + + + true + + + - + + + - + + + - + + + "no hit" + + + + + - + + + - + + + + + + - + + + - + + + "no hit" + + + + + - + + + - + + + - + + + false + + + - + + + "no hit" + + + + + - + + + - + + + - + + + - + + + true + + + "no hit" + + + + + diff --git a/examples/organizations/flevoland/STTR/decision-105946.dmn b/examples/organizations/flevoland/STTR/decision-105946.dmn new file mode 100644 index 0000000..7967c7c --- /dev/null +++ b/examples/organizations/flevoland/STTR/decision-105946.dmn @@ -0,0 +1,787 @@ + + + + + + Boom kappen of houtopstand vellen_Gemeente Lelystad + 1 + + + + + + 10 + + boolean + Wilt u een boom of beplanting weghalen? + + + Met boom of beplanting wordt iedere soort houtopstand bedoeld. + **Wat is een houtopstand?** +Een houtopstand is een zelfstandige boom of een groep van bomen, boomvormers, struiken, hakhout of griend. + +Een boomvormer is een houtig, opgaand gewas met ontwikkeling van één of meer hoofdtakken. Een boomvormer kan uitgroeien tot een boom, een meerstammige boom of een boomachtige struik. + +Een griend is een vochtige akker waarop wilgenhout wordt verbouwd. + true + + + + + 20 + + boolean + Gaat het om een aangewezen bijzondere boom of plant? + + + Op deze kaart staan bomen en planten die door de gemeente aangewezen zijn als bijzonder: + +[Bijzondere bomen in Lelystad | Gemeente Lelystad](https://www.lelystad.nl/bijzonderebomen) + true + + + + + 30 + + + Gaat het om een boom of houtopstand binnen de bebouwingscontour houtkap? + + + false + + + + + 40 + + boolean + Staat de boom of beplanting op een kavel van maximaal 5.000 vierkante meter? + + + false + + + + + 50 + + boolean + Staat de boom of beplanting op een (particulier) privé terrein? + + + false + + + + + 1.0.3 + + + + + + + + + + + Niet van toepassing + NietVanToepassing + + + nee + + + + + Vergunningplicht + Vergunningplicht + + + ja + + + ja + + + + + Vergunningplicht + Vergunningplicht + + + ja + + + nee + + + + deels + nee + + + + nee + + + + + Vergunningplicht + Vergunningplicht + + + ja + + + nee + + + + deels + nee + + + + ja + + + nee + + + + + + + Op basis van uw antwoorden valt uw activiteit onder de vergunningsplicht. Dit betekent dat u een omgevingsvergunning dient aan te vragen volgens de geldende regels van de Omgevingswet. Zorg ervoor dat u de aanvraag compleet indient met alle benodigde documenten en tekeningen. + + + Op basis van uw antwoorden is de conclusie dat uw activiteit toestemmingsvrij is. Dit houdt in dat u geen omgevingsvergunning nodig heeft, mits u de voorschriften uit het omgevingsplan naleeft. Bewaar alle relevante documenten, zodat u deze later kunt raadplegen indien nodig, en neem bij vragen gerust contact op met de gemeente Lelystad. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Boom_kappen_of_houtopstand_vellen_Niet_van_toepassing_cross + + + + + Boom_kappen_of_houtopstand_vellen_Vergunningplicht_cross + + + + + + if ?=null then false else contains(?,"Niet van toepassing pad 0_") + + + not(null) + + + "NietVanToepassing" + + + + + not(null) + + + if ?=null then false else contains(?,"Vergunningplicht pad 1_") + + + + + Op basis van uw antwoorden valt uw activiteit onder de vergunningsplicht. Dit betekent dat u een omgevingsvergunning dient aan te vragen volgens de geldende regels van de Omgevingswet. Zorg ervoor dat u de aanvraag compleet indient met alle benodigde documenten en tekeningen. + + + "Vergunningplicht" + + + + + not(null) + + + if ?=null then false else contains(?,"Vergunningplicht pad 2_") + + + + + Op basis van uw antwoorden valt uw activiteit onder de vergunningsplicht. Dit betekent dat u een omgevingsvergunning dient aan te vragen volgens de geldende regels van de Omgevingswet. Zorg ervoor dat u de aanvraag compleet indient met alle benodigde documenten en tekeningen. + + + "Vergunningplicht" + + + + + not(null) + + + if ?=null then false else contains(?,"Vergunningplicht pad 3_") + + + + + Op basis van uw antwoorden valt uw activiteit onder de vergunningsplicht. Dit betekent dat u een omgevingsvergunning dient aan te vragen volgens de geldende regels van de Omgevingswet. Zorg ervoor dat u de aanvraag compleet indient met alle benodigde documenten en tekeningen. + + + "Vergunningplicht" + + + + + "no hit" + + + "no hit" + + + + + Op basis van uw antwoorden is de conclusie dat uw activiteit toestemmingsvrij is. Dit houdt in dat u geen omgevingsvergunning nodig heeft, mits u de voorschriften uit het omgevingsplan naleeft. Bewaar alle relevante documenten, zodat u deze later kunt raadplegen indien nodig, en neem bij vragen gerust contact op met de gemeente Lelystad. + + + "Toestemmingsvrij" + + + + + + + + + + + + + _6d45be8c_8010_4d11_8775_487a28b88087_Niet_van_toepassing + + + + + + "Niet van toepassing" + + + "Niet van toepassing pad 0_" + + + + + "no hit" + + + "no hit" + + + + + + + + + + + + + + + + + + + _2bc68e71_981f_4476_aaaf_f10980dab348_Vergunningplicht + + + + + _45d6b9e0_518b_4bcc_b39a_b50cc3251539_Vergunningplicht + + + + + _8f6e3ddc_bba0_4b6c_bb24_6614f061332c_Vergunningplicht + + + + + + "Vergunningplicht" + + + - + + + - + + + "Vergunningplicht pad 1_" + + + + + - + + + "Vergunningplicht" + + + - + + + "Vergunningplicht pad 2_" + + + + + - + + + - + + + "Vergunningplicht" + + + "Vergunningplicht pad 3_" + + + + + "no hit" + + + "no hit" + + + "no hit" + + + "no hit" + + + + + + + + + + + + + uitv__4483fe58_57b7_4e71_bf19_15783f0f9d92 + + + + + + false + + + "Niet van toepassing" + + + + + true + + + "no hit" + + + + + + + + + + + + + + + + uitv__4483fe58_57b7_4e71_bf19_15783f0f9d92 + + + + + uitv__864933e7_4ea9_45a2_ae17_d8b1a4df34d7 + + + + + + true + + + true + + + "Vergunningplicht" + + + + + false + + + - + + + "no hit" + + + + + - + + + false + + + "no hit" + + + + + + + + + + + + + + + + + + + + + + uitv__4483fe58_57b7_4e71_bf19_15783f0f9d92 + + + + + uitv__864933e7_4ea9_45a2_ae17_d8b1a4df34d7 + + + + + uitv__7b6ed60e_184f_4d93_a186_828dc9182890 + + + + + uitv__648edf88_a9ce_4726_b043_a3e18fa434c5 + + + + + + true + + + false + + + "deels","nee" + + + false + + + "Vergunningplicht" + + + + + false + + + - + + + - + + + - + + + "no hit" + + + + + - + + + true + + + - + + + - + + + "no hit" + + + + + - + + + - + + + "ja" + + + - + + + "no hit" + + + + + - + + + - + + + - + + + true + + + "no hit" + + + + + + + + + + + + + + + + + + + + + + + + + uitv__4483fe58_57b7_4e71_bf19_15783f0f9d92 + + + + + uitv__864933e7_4ea9_45a2_ae17_d8b1a4df34d7 + + + + + uitv__7b6ed60e_184f_4d93_a186_828dc9182890 + + + + + uitv__648edf88_a9ce_4726_b043_a3e18fa434c5 + + + + + uitv__8155d4e0_7003_44ca_8f42_c97afa52f0c7 + + + + + + true + + + false + + + "deels","nee" + + + true + + + false + + + "Vergunningplicht" + + + + + false + + + - + + + - + + + - + + + - + + + "no hit" + + + + + - + + + true + + + - + + + - + + + - + + + "no hit" + + + + + - + + + - + + + "ja" + + + - + + + - + + + "no hit" + + + + + - + + + - + + + - + + + false + + + - + + + "no hit" + + + + + - + + + - + + + - + + + - + + + true + + + "no hit" + + + + + diff --git a/examples/organizations/flevoland/STTR/form-scaffold-105947.json b/examples/organizations/flevoland/STTR/form-scaffold-105947.json new file mode 100644 index 0000000..15ac5fc --- /dev/null +++ b/examples/organizations/flevoland/STTR/form-scaffold-105947.json @@ -0,0 +1,107 @@ +{ + "schemaVersion": 17, + "id": "form-105947", + "components": [ + { + "id": "uitv__5acd1773-b81c-436f-a91e-d37546ad5e24", + "type": "select", + "label": "Wat wilt u gaan doen?", + "key": "5acd1773_b81c_436f_a91e_d37546ad5e24", + "values": [ + { + "label": "Kappen", + "value": "Kappen" + }, + { + "label": "Anders", + "value": "Anders" + } + ], + "validate": { + "required": false + } + }, + { + "id": "uitv__625483ff-2dfd-4e63-8c52-2773cf169211", + "type": "textarea", + "label": "Beschrijf wat u wilt gaan doen.", + "key": "625483ff_2dfd_4e63_8c52_2773cf169211", + "validate": { + "required": false + } + }, + { + "id": "uitv__06480753-bdb0-4d23-8fd7-8941fad930f2", + "type": "textfield", + "label": "Waarom wilt u de houtopstand onderhouden of weghalen?", + "key": "06480753_bdb0_4d23_8fd7_8941fad930f2", + "validate": { + "required": false + } + }, + { + "id": "uitv__c1720883-89cf-47e5-97f0-36ef4d5fab05", + "type": "number", + "label": "Om hoeveel bomen gaat het?", + "key": "c1720883_89cf_47e5_97f0_36ef4d5fab05", + "validate": { + "required": false + } + }, + { + "id": "uitv__2ebda717-baaf-4b56-8cc8-e8761d7ab632", + "type": "textarea", + "label": "Beschrijf om welke soort(en) het gaat.", + "key": "2ebda717_baaf_4b56_8cc8_e8761d7ab632", + "validate": { + "required": false + } + }, + { + "id": "uitv__94af6398-3d9e-450b-bb64-12b1728fee00", + "type": "checkbox", + "label": "Bent u de eigenaar van de boom of houtopstand?", + "key": "94af6398_3d9e_450b_bb64_12b1728fee00", + "validate": { + "required": false + } + }, + { + "id": "uitv__5f545924-6269-4f1b-b4e7-26c457e7fd31", + "type": "checkbox", + "label": "Stemt de eigenaar van de boom in met de kap?", + "key": "5f545924_6269_4f1b_b4e7_26c457e7fd31", + "validate": { + "required": false + } + }, + { + "id": "uitv__9d5e992e-4e3f-429b-930c-0e9d69e997de", + "type": "textfield", + "label": "[Bijlage] Toestemming van de eigenaar", + "key": "9d5e992e_4e3f_429b_930c_0e9d69e997de", + "validate": { + "required": false + } + }, + { + "id": "uitv__acbdb84c-54b5-4bd9-98b4-bf7e676bf3d4", + "type": "textfield", + "label": "[Bijlage] Situatietekening kappen", + "key": "acbdb84c_54b5_4bd9_98b4_bf7e676bf3d4", + "validate": { + "required": false + } + }, + { + "id": "uitv__82778bb0-6319-4d43-985e-d3d4c4ea2958", + "type": "textfield", + "label": "[Bijlage] Gegevens houtopstanden", + "key": "82778bb0_6319_4d43_985e_d3d4c4ea2958", + "validate": { + "required": false + } + } + ], + "type": "default" +} \ No newline at end of file diff --git a/examples/organizations/flevoland/STTR/sttr-105946.xml b/examples/organizations/flevoland/STTR/sttr-105946.xml new file mode 100644 index 0000000..93531a0 --- /dev/null +++ b/examples/organizations/flevoland/STTR/sttr-105946.xml @@ -0,0 +1,787 @@ + + + + + + Boom kappen of houtopstand vellen_Gemeente Lelystad + 1 + + + + + + 10 + + boolean + + + + + + true + + + + + 20 + + boolean + + + + + true + + + + + 30 + + + + + + false + + + + + 40 + + boolean + + + + false + + + + + 50 + + boolean + + + + false + + + + + 1.0.3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + deels + nee + + + + + + + + + + + + + + + + + + + + deels + nee + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + if ?=null then false else contains(?,"Niet van toepassing pad 0_") + + + not(null) + + + + + + + + not(null) + + + if ?=null then false else contains(?,"Vergunningplicht pad 1_") + + + + + + + + + + + + + not(null) + + + if ?=null then false else contains(?,"Vergunningplicht pad 2_") + + + + + + + + + + + + + not(null) + + + if ?=null then false else contains(?,"Vergunningplicht pad 3_") + + + + + + + + + + + + + "no hit" + + + "no hit" + + + + + + + + "Toestemmingsvrij" + + + + + + + + + + + + + + + + + + + + + + + + + + + "no hit" + + + "no hit" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + - + + + + + + + + - + + + + + + - + + + + + + + + - + + + - + + + + + + + + + + + "no hit" + + + "no hit" + + + "no hit" + + + "no hit" + + + + + + + + + + + + + uitv__4483fe58-57b7-4e71-bf19-15783f0f9d92 + + + + + + false + + + + + + + + true + + + "no hit" + + + + + + + + + + + + + + + + uitv__4483fe58-57b7-4e71-bf19-15783f0f9d92 + + + + + uitv__864933e7-4ea9-45a2-ae17-d8b1a4df34d7 + + + + + + true + + + true + + + + + + + + false + + + - + + + "no hit" + + + + + - + + + false + + + "no hit" + + + + + + + + + + + + + + + + + + + + + + uitv__4483fe58-57b7-4e71-bf19-15783f0f9d92 + + + + + uitv__864933e7-4ea9-45a2-ae17-d8b1a4df34d7 + + + + + uitv__7b6ed60e-184f-4d93-a186-828dc9182890 + + + + + uitv__648edf88-a9ce-4726-b043-a3e18fa434c5 + + + + + + true + + + false + + + "deels","nee" + + + false + + + + + + + + false + + + - + + + - + + + - + + + "no hit" + + + + + - + + + true + + + - + + + - + + + "no hit" + + + + + - + + + - + + + + + + - + + + "no hit" + + + + + - + + + - + + + - + + + true + + + "no hit" + + + + + + + + + + + + + + + + + + + + + + + + + uitv__4483fe58-57b7-4e71-bf19-15783f0f9d92 + + + + + uitv__864933e7-4ea9-45a2-ae17-d8b1a4df34d7 + + + + + uitv__7b6ed60e-184f-4d93-a186-828dc9182890 + + + + + uitv__648edf88-a9ce-4726-b043-a3e18fa434c5 + + + + + uitv__8155d4e0-7003-44ca-8f42-c97afa52f0c7 + + + + + + true + + + false + + + "deels","nee" + + + true + + + false + + + + + + + + false + + + - + + + - + + + - + + + - + + + "no hit" + + + + + - + + + true + + + - + + + - + + + - + + + "no hit" + + + + + - + + + - + + + + + + - + + + - + + + "no hit" + + + + + - + + + - + + + - + + + false + + + - + + + "no hit" + + + + + - + + + - + + + - + + + - + + + true + + + "no hit" + + + + + diff --git a/examples/organizations/flevoland/STTR/sttr-105947.xml b/examples/organizations/flevoland/STTR/sttr-105947.xml new file mode 100644 index 0000000..37ea567 --- /dev/null +++ b/examples/organizations/flevoland/STTR/sttr-105947.xml @@ -0,0 +1,1102 @@ + + + + + + Kappen + 1 + + + Eigendom + 2 + + + Algemene bijlagen + 3 + + + + + + true + true + 10 + + list + + + enkelAntwoord + + 1 + + + + 2 + + + + + + false + + + + + true + true + 20 + + string + + textarea + + + false + + + + + true + false + 30 + + string + + text + + + false + + + + + true + true + 40 + + number + + + + + false + + + + + true + false + 50 + + string + + textarea + + + false + + + + + true + false + 60 + + boolean + + + + false + + + + + true + false + 70 + + boolean + + + + false + + + + + true + false + 80 + + + + + + false + + + + + true + true + 90 + + + + + + + false + + + + + true + true + 100 + + + + + + + false + + + + + 1.0.3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + _5acd1773-b81c-436f-a91e-d37546ad5e24 + + + + + _06480753-bdb0-4d23-8fd7-8941fad930f2 + + + + + _c1720883-89cf-47e5-97f0-36ef4d5fab05 + + + + + _2ebda717-baaf-4b56-8cc8-e8761d7ab632 + + + + + _94af6398-3d9e-450b-bb64-12b1728fee00 + + + + + _acbdb84c-54b5-4bd9-98b4-bf7e676bf3d4 + + + + + _82778bb0-6319-4d43-985e-d3d4c4ea2958 + + + + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + + + false + + + - + + + - + + + - + + + - + + + - + + + - + + + false + + + + + - + + + false + + + - + + + - + + + - + + + - + + + - + + + false + + + + + - + + + - + + + false + + + - + + + - + + + - + + + - + + + false + + + + + - + + + - + + + - + + + false + + + - + + + - + + + - + + + false + + + + + - + + + - + + + - + + + - + + + false + + + - + + + - + + + false + + + + + - + + + - + + + - + + + - + + + - + + + false + + + - + + + false + + + + + - + + + - + + + - + + + - + + + - + + + - + + + false + + + false + + + + + + + + + + + + + + + + + + + true + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + if ?=null then false else contains(?,"Anders") + + + false + + + false + + + + + if ?=null then false else not(contains(?,"Anders")) + + + - + + + true + + + + + + + + + + + + + + + + + + + true + + + true + + + + + false + + + false + + + + + + + + + + + + + + + + + + + not(null) + + + true + + + + + + + + + + + + + + + + + + + true + + + true + + + + + false + + + false + + + + + + + + + + + + + + + + + + + not(null) + + + true + + + + + + + + + + + + + + + + + + + true + + + true + + + + + false + + + false + + + + + + + + + + + + + + + + + + + not(null) + + + true + + + + + + + + + + + + + + + + + + + true + + + true + + + + + false + + + false + + + + + + + + + + + + + + + + + + + not(null) + + + true + + + + + + + + + + + + + + + + + + + true + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + false + + + false + + + false + + + + + true + + + - + + + true + + + + + + + + + + + + + + + + + + + true + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + false + + + false + + + + + false + + + - + + + true + + + + + + + + + + + + + + + + + + + true + + + true + + + + + false + + + false + + + + + + + + + + + + + + + + + + + not(null) + + + true + + + + + + + + + + + + + + + + + + + true + + + true + + + + + false + + + false + + + + + + + + + + + + + + + + + + + not(null) + + + true + + + + + + + + + + + + + + + + + + + true + + + true + + + + + false + + + false + + + + + + + + + + + + + + + + + + + not(null) + + + true + + + + + diff --git a/examples/organizations/flevoland/rip-phase1/RipPhase1Process.bpmn b/examples/organizations/flevoland/rip-phase1/RipPhase1Process.bpmn index 88aa701..c0db905 100644 --- a/examples/organizations/flevoland/rip-phase1/RipPhase1Process.bpmn +++ b/examples/organizations/flevoland/rip-phase1/RipPhase1Process.bpmn @@ -94,6 +94,15 @@ execution.setVariable("candidateGroups", roleResult.get("candidateGroups")); execution.setVariable("assignedRoles", roleResult.get("assignedRoles")); + + // Derive the portfolio lead/owner role (Infra-board "ROL" ownership + // signal — distinct from the task candidateGroups above). Maps the intake + // projectType to a rip-model portfolio role key; defaults to Projectleider, + // which leads a RIP project unless a control-track type owns it instead. + // NOTE: confirm this projectType -> leadRole mapping with the business. + var pt = String(execution.getVariable("projectType") || ""); + var leadRole = (pt === "contractbeheer") ? "manager-pb" : "projectleider"; + execution.setVariable("leadRole", leadRole); diff --git a/examples/organizations/flevoland/thuisbatterij/RechtEnHoogteSubsidieThuisbatterij.dmn b/examples/organizations/flevoland/thuisbatterij/RechtEnHoogteSubsidieThuisbatterij.dmn new file mode 100644 index 0000000..81926eb --- /dev/null +++ b/examples/organizations/flevoland/thuisbatterij/RechtEnHoogteSubsidieThuisbatterij.dmn @@ -0,0 +1,717 @@ + + + + + + + + + + + basisHoogteSubsidie + + + + + + <= beschikbaarSubsidiePlafond + + + basisHoogteSubsidie + + + + + > beschikbaarSubsidiePlafond + + + beschikbaarSubsidiePlafond + + + + + + + + + + + + + gemaakteKosten + + + + + + >= minimaleNoodzakelijkeKosten / subsidiePercentage + + + if subsidiePercentage * gemaakteKosten > subsidieMaximum then subsidieMaximum else if subsidiePercentage * gemaakteKosten < subsidieMinimum then subsidieMinimum else subsidiePercentage * gemaakteKosten + + + + + < minimaleNoodzakelijkeKosten / subsidiePercentage + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + aanvraagDatum + + + + + aanvragerType + + + + + reedsGesubsidieerdEigenaren + + + + + reedsGesubsidieerdHuurders + + + + + 12 januari 2026 t/m 30 september 2026: apart plafond voor eigenaren. + + date(aanvraagDatum) >= date("2026-01-12") and date(aanvraagDatum) <= date("2026-09-30") + + + "eigenaar" + + + - + + + - + + + if plafondEigenaren - reedsGesubsidieerdEigenaren > 0 then plafondEigenaren - reedsGesubsidieerdEigenaren else 0 + + + + 12 januari 2026 t/m 30 september 2026: apart plafond voor huurders. + + date(aanvraagDatum) >= date("2026-01-12") and date(aanvraagDatum) <= date("2026-09-30") + + + "huurder" + + + - + + + - + + + if plafondHuurders - reedsGesubsidieerdHuurders > 0 then plafondHuurders - reedsGesubsidieerdHuurders else 0 + + + + Vanaf 1 oktober 2026: wat over is van beide plafonds samen, geen onderscheid. Voor 2026 is het hele budget opgesplitst naar huurder en verhuurders. + + date(aanvraagDatum) >= date("2026-10-01") and date(aanvraagDatum) <= date("2026-12-31") + + + - + + + - + + + - + + + if plafondEigenaren + plafondHuurders - reedsGesubsidieerdEigenaren - reedsGesubsidieerdHuurders > 0 then plafondEigenaren + plafondHuurders - reedsGesubsidieerdEigenaren - reedsGesubsidieerdHuurders else 0 + + + + 1 januari 2027 t/m 30 september 2027: apart plafond voor eigenaren. NB: in deze berekening wordt nog geen 2026 overgebleven budget overgeheveld. + + date("2027-01-01") <= date(aanvraagDatum) and date(aanvraagDatum) <= date("2027-09-30") + + + "eigenaar" + + + - + + + - + + + if plafondEigenaren - reedsGesubsidieerdEigenaren > 0 then plafondEigenaren - reedsGesubsidieerdEigenaren else 0 + + + + 1 januari 2027 t/m 30 september 2027: apart plafond voor huurders. + + date(aanvraagDatum) >= date("2027-01-01") and date(aanvraagDatum) <= date("2027-09-30") + + + "huurder" + + + - + + + - + + + if plafondHuurders - reedsGesubsidieerdHuurders > 0 then plafondHuurders - reedsGesubsidieerdHuurders else 0 + + + + Vanaf 1 oktober 2027: wat over is van beide plafonds samen, geen onderscheid. NB": het totale budget voor 2027 is meer dan wat tm september mag worden uitgegeven! + + date(aanvraagDatum) >= date("2027-10-01") and date(aanvraagDatum) <= date("2027-12-31") + + + - + + + - + + + - + + + if 1000000 - reedsGesubsidieerdEigenaren - reedsGesubsidieerdHuurders > 0 then 1000000 - reedsGesubsidieerdEigenaren - reedsGesubsidieerdHuurders else 0 + + + + Buiten de gemodelleerde aanvraagperiodes of onbekend type: geen beschikbaar plafond. + + - + + + - + + + - + + + - + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0.25 + + + 750 + + + 1250 + + + 750 + + + + + + + + + + + + date(aanvraagDatum).year + + + + + + + 2026 + + + 437500 + + + 437500 + + + + + 2027 + + + 437500 + + + 437500 + + + + + + + + + + + + + + + + + + + + + + + + aanvragerFailliet + + + + + provincieWoning + + + + + relatieTotWoning + + + + + toestemmingEigenaar + + + + + rekeningNaamKomtOvereen + + + + + + De aanvrager heeft geen recht op subsidie als de aanvrager failliet is. + + true + + + - + + + - + + + - + + + - + + + false + + + "Aanvrager is failliet" + + + + De woning moet in Flevoland liggen. + + false + + + not("Flevoland") + + + - + + + - + + + - + + + false + + + "Woning ligt niet in Flevoland" + + + + De aanvrager moet eigenaar of huurder van de woning zijn. + + false + + + "Flevoland" + + + not("eigenaar", "huurder") + + + - + + + - + + + false + + + "Aanvrager is geen eigenaar of huurder" + + + + Als de aanvrager huurder is, is toestemming van de eigenaar vereist. + + false + + + "Flevoland" + + + "huurder" + + + false + + + - + + + false + + + "Huurder heeft geen toestemming van eigenaar" + + + + De aanvrager moet ook de naam op de rekening van de energiemaatschappij zijn. + + + + + + + + - + + + - + + + false + + + false + + + "Aanvrager staat niet op de energierekening" + + + + Eigenaar voldoet aan alle voorwaarden. + + false + + + "Flevoland" + + + "eigenaar" + + + - + + + true + + + true + + + "Aanvrager heeft recht op subsidie" + + + + Huurder voldoet aan alle voorwaarden, inclusief toestemming van de eigenaar. + + false + + + "Flevoland" + + + "huurder" + + + true + + + true + + + true + + + "Aanvrager heeft recht op subsidie" + + + + Vangnetregel: niet alle voorwaarden zijn vervuld of gegevens ontbreken. + + - + + + - + + + - + + + - + + + - + + + false + + + "Niet alle voorwaarden zijn vervuld" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/organizations/flevoland/thuisbatterij/dmn_mcdc_pipeline_generator_repaired_embedded_v4.py b/examples/organizations/flevoland/thuisbatterij/dmn_mcdc_pipeline_generator_repaired_embedded_v4.py new file mode 100644 index 0000000..564e471 --- /dev/null +++ b/examples/organizations/flevoland/thuisbatterij/dmn_mcdc_pipeline_generator_repaired_embedded_v4.py @@ -0,0 +1,3641 @@ +#!/usr/bin/env python3 +""" +Generate MC/DC-style and boundary-focused JSON test cases for every decision + table in a DMN file. + +The output is designed for DMN/REST-style test runners. Each generated case has: +- decisionId / decisionName: the DMN decision table the case targets +- name: readable test-case name +- expected: comma-separated expected output values +- requestBody.variables: typed variables to submit +- coverage: why the case was selected + +Algorithm summary: +1. Parse all DMN decision tables. +2. For every decision table, derive the variables required by that table. +3. Build boundary-focused domains from FEEL unary tests, date ranges, + string equality rules, numeric thresholds, and known subsidy-style variables. +4. Evaluate candidates directly against each decision table. +5. Select MC/DC-style pairs for atomic conditions. +6. Add explicit boundary/domain coverage so every derived boundary value is + represented in at least one test for that table. + +Supported FEEL subset: +- string and numeric literals +- date("YYYY-MM-DD"), date(variable), date(variable).year +- equality and comparisons +- `and` clauses +- nested `if ... then ... else ...` output expressions +- simple arithmetic + +The core DMN/JSON/Postman generation uses only the Python standard library. +Excel generation uses `openpyxl` so the workbook can include local-friendly +formatting, run-link cells, and charts. For complex FEEL models, extend the +evaluator or replace `safe_eval` with your production DMN engine while keeping +the domain and MC/DC selection logic. +""" + +from __future__ import annotations + +import argparse +import base64 +import copy +import uuid +import html +import os +import shutil +import tempfile +import zipfile +from collections import Counter, defaultdict +import itertools +import json +import math +import re +import sys +import xml.etree.ElementTree as ET +from dataclasses import dataclass, field +from datetime import date, timedelta +from functools import lru_cache +from pathlib import Path +from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple + + +DMN_NS = {"dmn": "https://www.omg.org/spec/DMN/20191111/MODEL/"} +IDENT_RE = re.compile(r"\b[A-Za-z_][A-Za-z0-9_]*\b") +COMPARISON_RE = re.compile(r"^(<=|>=|<|>|=|!=)\s*(.+)$") + + +@dataclass +class DmnInput: + id: str + label: str + expression: str + type_ref: Optional[str] = None + + +@dataclass +class DmnOutput: + id: str + label: str + name: str + type_ref: Optional[str] = None + + +@dataclass +class DmnRule: + id: str + input_entries: List[str] + output_entries: List[str] + description: str = "" + + +@dataclass +class DmnDecision: + id: str + name: str + table_id: str + variable_name: Optional[str] + hit_policy: str + inputs: List[DmnInput] + outputs: List[DmnOutput] + rules: List[DmnRule] + + +@dataclass +class RuleTrace: + rule_id: str + matched: bool + atoms: Dict[str, bool] + + +@dataclass +class TableEvaluation: + selected_rule_id: Optional[str] + selected_rule_index: Optional[int] + outputs: Dict[str, Any] + rule_traces: Dict[str, RuleTrace] = field(default_factory=dict) + + +@dataclass +class Candidate: + decision_id: str + inputs: Dict[str, Any] + evaluation: TableEvaluation + + +# --------------------------------------------------------------------------- +# XML parsing +# --------------------------------------------------------------------------- + + +def text_of(elem: Optional[ET.Element]) -> str: + if elem is None or elem.text is None: + return "" + return elem.text.strip() + + +def parse_dmn(path: Path) -> Dict[str, DmnDecision]: + root = ET.parse(path).getroot() + decisions: Dict[str, DmnDecision] = {} + + for decision_el in root.findall("dmn:decision", DMN_NS): + decision_id = decision_el.get("id") or "" + variable_el = decision_el.find("dmn:variable", DMN_NS) + variable_name = variable_el.get("name") if variable_el is not None else None + + dt = decision_el.find("dmn:decisionTable", DMN_NS) + if dt is None: + continue + + inputs: List[DmnInput] = [] + for inp in dt.findall("dmn:input", DMN_NS): + ie = inp.find("dmn:inputExpression", DMN_NS) + inputs.append( + DmnInput( + id=inp.get("id") or "", + label=inp.get("label") or "", + expression=text_of(ie.find("dmn:text", DMN_NS)) if ie is not None else "", + type_ref=ie.get("typeRef") if ie is not None else None, + ) + ) + + outputs: List[DmnOutput] = [] + for out in dt.findall("dmn:output", DMN_NS): + outputs.append( + DmnOutput( + id=out.get("id") or "", + label=out.get("label") or "", + name=out.get("name") or out.get("label") or out.get("id") or "", + type_ref=out.get("typeRef"), + ) + ) + + rules: List[DmnRule] = [] + for rule_el in dt.findall("dmn:rule", DMN_NS): + input_entries = [text_of(e.find("dmn:text", DMN_NS)) for e in rule_el.findall("dmn:inputEntry", DMN_NS)] + output_entries = [text_of(e.find("dmn:text", DMN_NS)) for e in rule_el.findall("dmn:outputEntry", DMN_NS)] + description = text_of(rule_el.find("dmn:description", DMN_NS)) + + # Ignore empty placeholder rows created by DMN modelers. + if not any(x.strip() for x in input_entries + output_entries): + continue + + rules.append( + DmnRule( + id=rule_el.get("id") or "", + input_entries=input_entries, + output_entries=output_entries, + description=description, + ) + ) + + decisions[decision_id] = DmnDecision( + id=decision_id, + name=decision_el.get("name") or decision_id, + table_id=dt.get("id") or "", + variable_name=variable_name, + hit_policy=(dt.get("hitPolicy") or "UNIQUE").upper(), + inputs=inputs, + outputs=outputs, + rules=rules, + ) + + if not decisions: + raise ValueError("No DMN decision table found.") + return decisions + + +# --------------------------------------------------------------------------- +# FEEL subset evaluator +# --------------------------------------------------------------------------- + + +def date_func(value: Any) -> date: + if isinstance(value, date): + return value + if isinstance(value, str): + return date.fromisoformat(value) + raise ValueError(f"Cannot convert {value!r} to date") + + +def split_top_level_keyword(expr: str, keyword: str, start: int = 0) -> int: + depth = 0 + in_string = False + i = start + while i < len(expr): + ch = expr[i] + if ch == '"': + in_string = not in_string + i += 1 + continue + if in_string: + i += 1 + continue + if ch == "(": + depth += 1 + elif ch == ")": + depth -= 1 + elif depth == 0: + before_ok = i == 0 or not (expr[i - 1].isalnum() or expr[i - 1] == "_") + after_idx = i + len(keyword) + after_ok = after_idx >= len(expr) or not (expr[after_idx].isalnum() or expr[after_idx] == "_") + if before_ok and after_ok and expr.startswith(keyword, i): + return i + i += 1 + return -1 + + +def split_top_level_and(expr: str) -> List[str]: + parts: List[str] = [] + start = 0 + while True: + idx = split_top_level_keyword(expr, "and", start) + if idx == -1: + parts.append(expr[start:].strip()) + return [p for p in parts if p] + parts.append(expr[start:idx].strip()) + start = idx + 3 + + +@lru_cache(maxsize=None) +def translate_feel_expr(expr: str) -> str: + expr = (expr or "").strip() + if not expr: + return "None" + + if expr.startswith("if "): + then_idx = split_top_level_keyword(expr, "then", 3) + else_idx = split_top_level_keyword(expr, "else", then_idx + 4 if then_idx != -1 else 0) + if then_idx == -1 or else_idx == -1: + raise ValueError(f"Cannot parse FEEL if-expression: {expr}") + condition = expr[3:then_idx].strip() + then_part = expr[then_idx + 4:else_idx].strip() + else_part = expr[else_idx + 4:].strip() + return f"({translate_feel_expr(then_part)} if {translate_condition_expr(condition)} else {translate_feel_expr(else_part)})" + + return translate_condition_expr(expr) + + +@lru_cache(maxsize=None) +def translate_condition_expr(expr: str) -> str: + expr = (expr or "").strip() + expr = re.sub(r"\bdate\s*\(", "date_func(", expr) + expr = re.sub(r"(?=!])=(?!=)", "==", expr) + expr = re.sub(r"\btrue\b", "True", expr, flags=re.IGNORECASE) + expr = re.sub(r"\bfalse\b", "False", expr, flags=re.IGNORECASE) + expr = re.sub(r"\bnull\b", "None", expr, flags=re.IGNORECASE) + return expr + + +@lru_cache(maxsize=None) +def compiled_feel_expr(expr: str) -> Tuple[str, Any]: + py_expr = translate_feel_expr(expr) + return py_expr, compile(py_expr, "", "eval") + + +def safe_eval(expr: str, ctx: Dict[str, Any]) -> Any: + py_expr, code = compiled_feel_expr(expr) + safe_globals = {"__builtins__": {}, "date_func": date_func, "min": min, "max": max, "abs": abs, "math": math} + try: + return eval(code, safe_globals, dict(ctx)) + except Exception as e: + raise ValueError(f"Failed to evaluate FEEL {expr!r} translated as {py_expr!r}: {e}") from e + + +def atom_label(input_expr: str, entry: str) -> str: + entry = (entry or "").strip() + if not entry or entry == "-": + return "" + m = COMPARISON_RE.match(entry) + if m: + op, rhs = m.groups() + op = "==" if op == "=" else op + return f"{input_expr} {op} {rhs.strip()}" + if re.match(r'^\s*".*"\s*$', entry): + return f"{input_expr} == {entry}" + if re.match(r"^\s*-?\d+(\.\d+)?\s*$", entry): + return f"{input_expr} == {entry}" + return entry + + +def eval_entry_against_input(input_expr: str, input_value: Any, entry: str, ctx: Dict[str, Any]) -> bool: + entry = (entry or "").strip() + if entry in ("", "-"): + return True + + m = COMPARISON_RE.match(entry) + if m: + op, rhs_expr = m.groups() + rhs = safe_eval(rhs_expr, ctx) + if op in ("=", "=="): + return input_value == rhs + if op == "!=": + return input_value != rhs + if op == "<=": + return input_value <= rhs + if op == ">=": + return input_value >= rhs + if op == "<": + return input_value < rhs + if op == ">": + return input_value > rhs + + if re.match(r'^\s*".*"\s*$', entry) or re.match(r"^\s*-?\d+(\.\d+)?\s*$", entry): + return input_value == safe_eval(entry, ctx) + + return bool(safe_eval(entry, ctx)) + + +def eval_rule_atoms(decision: DmnDecision, rule: DmnRule, ctx: Dict[str, Any]) -> Tuple[bool, Dict[str, bool]]: + atoms: Dict[str, bool] = {} + rule_matches = True + + for inp, entry in zip(decision.inputs, rule.input_entries): + entry = (entry or "").strip() + input_value = safe_eval(inp.expression, ctx) + if not entry or entry == "-": + continue + + parts = split_top_level_and(entry) + entry_match = True + for part in parts: + label = atom_label(inp.expression, part) + value = eval_entry_against_input(inp.expression, input_value, part, ctx) + atoms[label] = bool(value) + entry_match = entry_match and bool(value) + rule_matches = rule_matches and entry_match + + return bool(rule_matches), atoms + + +def evaluate_decision_table_direct(decision: DmnDecision, ctx: Dict[str, Any]) -> TableEvaluation: + selected_rule: Optional[DmnRule] = None + selected_rule_index: Optional[int] = None + rule_traces: Dict[str, RuleTrace] = {} + + for idx, rule in enumerate(decision.rules, start=1): + matched, atoms = eval_rule_atoms(decision, rule, ctx) + rule_traces[rule.id] = RuleTrace(rule_id=rule.id, matched=matched, atoms=atoms) + if matched and selected_rule is None: + selected_rule = rule + selected_rule_index = idx + if decision.hit_policy == "FIRST": + # Still keep evaluating remaining rules for traceability? For a + # true FIRST table, selection stops. We continue only atom traces + # in the generic evaluator by not breaking; selected stays first. + pass + + outputs: Dict[str, Any] = {} + if selected_rule is not None: + for out, out_expr in zip(decision.outputs, selected_rule.output_entries): + outputs[out.name] = safe_eval(out_expr, ctx) if out_expr.strip() else None + + return TableEvaluation( + selected_rule_id=selected_rule.id if selected_rule else None, + selected_rule_index=selected_rule_index, + outputs=outputs, + rule_traces=rule_traces, + ) + + +# --------------------------------------------------------------------------- +# Domain extraction +# --------------------------------------------------------------------------- + + +def identifiers(expr: str) -> List[str]: + expr = re.sub(r'"[^"]*"', '""', expr or "") + keywords = {"if", "then", "else", "and", "or", "not", "date", "true", "false", "null", "year", "min", "max"} + return [x for x in IDENT_RE.findall(expr or "") if x not in keywords] + + +def decision_text(decision: DmnDecision) -> str: + return "\n".join( + [ + decision.name, + "\n".join(i.expression for i in decision.inputs), + "\n".join(e for r in decision.rules for e in (r.input_entries + r.output_entries)), + ] + ) + + +def all_model_text(decisions: Dict[str, DmnDecision]) -> str: + return "\n".join(decision_text(d) for d in decisions.values()) + + +def variables_required_by_decision(decision: DmnDecision, constant_defaults: Dict[str, Any]) -> List[str]: + vars_needed: set[str] = set() + for inp in decision.inputs: + vars_needed.update(identifiers(inp.expression)) + for rule in decision.rules: + for expr in rule.input_entries + rule.output_entries: + vars_needed.update(identifiers(expr)) + + # Do not ask the caller to supply a variable produced by this same table. + own_outputs = {o.name for o in decision.outputs if o.name} + vars_needed -= own_outputs + + # Keep constants if this table references them; direct table tests are self-contained. + return sorted(vars_needed) + + +def compute_constant_defaults(decisions: Dict[str, DmnDecision]) -> Dict[str, Any]: + defaults: Dict[str, Any] = {} + for decision in decisions.values(): + if decision.inputs: + continue + try: + evaluation = evaluate_decision_table_direct(decision, dict(defaults)) + defaults.update(evaluation.outputs) + except Exception: + continue + return defaults + + +def extract_dates_from_text(text: str) -> List[str]: + values: set[str] = set() + for raw in re.findall(r'date\s*\(\s*"(\d{4}-\d{2}-\d{2})"\s*\)', text): + d = date.fromisoformat(raw) + for delta in (-1, 0, 1): + values.add((d + timedelta(days=delta)).isoformat()) + if values: + years = sorted({date.fromisoformat(v).year for v in values}) + values.add(f"{min(years) - 1}-12-31") + values.add(f"{max(years) + 1}-01-01") + return sorted(values) + + +def dates_for_year_literals(text: str) -> List[str]: + years = sorted({int(x) for x in re.findall(r"(? List[str]: + values: set[str] = set() + for inp_idx, inp in enumerate(decision.inputs): + if inp.expression != var_name: + continue + for rule in decision.rules: + if inp_idx >= len(rule.input_entries): + continue + for lit in re.findall(r'"([^"]*)"', rule.input_entries[inp_idx] or ""): + if not re.fullmatch(r"\d{4}-\d{2}-\d{2}", lit): + values.add(lit) + if values: + values.add("onbekend") + return sorted(values) + + +def extract_numeric_boundaries(text: str) -> List[float]: + raw_numbers = [float(x) for x in re.findall(r"(? 0) + for a in positives: + for b in positives: + if b == 0: + continue + q = a / b + if 1 <= q <= 10_000_000 and abs(q - round(q, 6)) < 1e-9: + nums.add(round(q, 6)) + + boundaries: set[float] = set() + for n in nums: + if abs(n) < 1: + boundaries.add(n) + else: + for delta in (-1, 0, 1): + boundaries.add(n + delta) + + return sorted(x for x in boundaries if -1_000_000 <= x <= 10_000_000) + + +def numeric_domain_for_variable(var_name: str, numeric_boundaries: Sequence[float], constant_defaults: Dict[str, Any]) -> List[Any]: + if var_name in constant_defaults and isinstance(constant_defaults[var_name], (int, float)): + return [as_number(constant_defaults[var_name])] + + lower = var_name.lower() + if "percentage" in lower: + return [0.25] + if "minimum" in lower or "minimale" in lower: + return [750] + if "maximum" in lower: + return [1250] + if "gemaaktekosten" in lower or "kosten" in lower: + preferred = [0, 2999, 3000, 3001, 4999, 5000, 5001, 10000] + elif "basishoogtesubsidie" in lower or "beschikbaarsubsidieplafond" in lower or "hoogtesubsidie" in lower: + preferred = [0, 1, 749, 750, 751, 1249, 1250, 1251, 437499, 437500, 437501] + elif "reeds" in lower or "gesubsidieerd" in lower: + preferred = [0, 1, 437499, 437500, 437501, 874999, 875000, 875001, 999999, 1000000, 1000001] + elif "plafond" in lower or "budget" in lower: + preferred = [437500, 875000, 1000000] + else: + preferred = [0, 1] + list(numeric_boundaries[:12]) + + return sorted(dict.fromkeys(as_number(x) for x in preferred)) + + +def as_number(value: Any) -> Any: + if isinstance(value, float) and value.is_integer(): + return int(value) + return value + + +def is_date_variable(var_name: str, text: str) -> bool: + return ( + "datum" in var_name.lower() + or f"date({var_name})" in text.replace(" ", "") + or f"date( {var_name} )" in text + ) + + +def domain_for_variable(decision: DmnDecision, var_name: str, constant_defaults: Dict[str, Any], model_text: str) -> List[Any]: + text = decision_text(decision) + compact_text = text.replace(" ", "") + + if var_name in constant_defaults and not is_date_variable(var_name, compact_text): + value = constant_defaults[var_name] + if isinstance(value, str): + return [value] + if isinstance(value, (int, float)): + return [as_number(value)] + + strings = string_literals_for_var(decision, var_name) + if strings: + return strings + + if is_date_variable(var_name, compact_text): + date_values = extract_dates_from_text(text) + if ".year" in text or "year" in text: + date_values = sorted(set(date_values) | set(dates_for_year_literals(text))) + return date_values or [date.today().isoformat()] + + numeric_boundaries = extract_numeric_boundaries(model_text) + return numeric_domain_for_variable(var_name, numeric_boundaries, constant_defaults) + + +def domains_for_decision(decision: DmnDecision, constant_defaults: Dict[str, Any], model_text: str) -> Dict[str, List[Any]]: + domains: Dict[str, List[Any]] = {} + for var_name in variables_required_by_decision(decision, constant_defaults): + domains[var_name] = domain_for_variable(decision, var_name, constant_defaults, model_text) + return domains + + +def candidate_inputs(domains: Dict[str, List[Any]], max_candidates: int) -> Iterable[Dict[str, Any]]: + keys = list(domains.keys()) + if not keys: + yield {} + return + values = [domains[k] for k in keys] + for idx, combo in enumerate(itertools.product(*values)): + if idx >= max_candidates: + return + yield dict(zip(keys, combo)) + + +# --------------------------------------------------------------------------- +# Selection: MC/DC pairs + boundary representatives +# --------------------------------------------------------------------------- + + +def input_distance(a: Dict[str, Any], b: Dict[str, Any]) -> int: + return sum(1 for k in set(a) | set(b) if a.get(k) != b.get(k)) + + +def jsonable(value: Any) -> Any: + if isinstance(value, date): + return value.isoformat() + if isinstance(value, float): + if value.is_integer(): + return int(value) + return value + if isinstance(value, dict): + return {k: jsonable(v) for k, v in value.items()} + if isinstance(value, list): + return [jsonable(v) for v in value] + return value + + +def select_mcdc_pairs_for_decision(decision: DmnDecision, candidates: List[Candidate]) -> Tuple[List[Tuple[Candidate, Candidate, str]], List[str]]: + pairs: List[Tuple[Candidate, Candidate, str]] = [] + uncovered: List[str] = [] + seen: set[str] = set() + + for rule_index, rule in enumerate(decision.rules, start=1): + atom_names: List[str] = [] + for c in candidates: + rt = c.evaluation.rule_traces.get(rule.id) + if rt and rt.atoms: + atom_names = list(rt.atoms.keys()) + break + if not atom_names: + continue + + for target_atom in atom_names: + buckets: Dict[Tuple[Tuple[Tuple[str, bool], ...], bool, bool], List[Candidate]] = {} + for c in candidates: + rt = c.evaluation.rule_traces.get(rule.id) + if not rt or target_atom not in rt.atoms: + continue + other_key = tuple((atom, bool(rt.atoms.get(atom))) for atom in atom_names if atom != target_atom) + key = (other_key, bool(rt.atoms[target_atom]), bool(rt.matched)) + cell = buckets.setdefault(key, []) + if len(cell) < 100: + cell.append(c) + + best: Optional[Tuple[int, Candidate, Candidate]] = None + for other_key in sorted({k[0] for k in buckets}): + false_false = buckets.get((other_key, False, False), []) + true_true = buckets.get((other_key, True, True), []) + false_true = buckets.get((other_key, False, True), []) + true_false = buckets.get((other_key, True, False), []) + for left, right in ((false_false, true_true), (false_true, true_false)): + if not left or not right: + continue + for a in left: + for b in right: + score = input_distance(a.inputs, b.inputs) + if jsonable(a.evaluation.outputs) != jsonable(b.evaluation.outputs): + score -= 1 + if best is None or score < best[0]: + best = (score, a, b) + + reason = f"MC/DC: rule {rule_index} {rule.id}, condition [{target_atom}]" + if best: + _, a, b = best + key = json.dumps([a.inputs, b.inputs, reason], sort_keys=True, default=str) + if key not in seen: + pairs.append((a, b, reason)) + seen.add(key) + else: + uncovered.append(f"{decision.name}: {reason}") + + return pairs, uncovered + + +def output_sort_value(candidate: Candidate) -> Tuple[int, float, str]: + nums = [float(v) for v in candidate.evaluation.outputs.values() if isinstance(v, (int, float))] + max_abs = max((abs(v) for v in nums), default=0.0) + has_output = 0 if candidate.evaluation.outputs else 1 + return (has_output, -max_abs, json.dumps(candidate.inputs, sort_keys=True, default=str)) + + +def choose_boundary_representatives(domains: Dict[str, List[Any]], candidates: List[Candidate]) -> List[Tuple[Candidate, str]]: + reps: List[Tuple[Candidate, str]] = [] + for var_name, values in domains.items(): + for value in values: + matching = [c for c in candidates if c.inputs.get(var_name) == value] + if not matching: + continue + matching.sort(key=output_sort_value) + reps.append((matching[0], f"Boundary/domain value: {var_name}={value!r}")) + return reps + + +def choose_rule_representatives(decision: DmnDecision, candidates: List[Candidate]) -> List[Tuple[Candidate, str]]: + reps: List[Tuple[Candidate, str]] = [] + for idx, rule in enumerate(decision.rules, start=1): + matching = [c for c in candidates if c.evaluation.selected_rule_id == rule.id] + if not matching: + continue + matching.sort(key=output_sort_value) + reps.append((matching[0], f"Representative selected rule {idx}: {rule.id}")) + return reps + + +def choose_output_extreme_representatives(decision: DmnDecision, candidates: List[Candidate]) -> List[Tuple[Candidate, str]]: + reps: List[Tuple[Candidate, str]] = [] + for out in decision.outputs: + numeric_candidates = [c for c in candidates if isinstance(c.evaluation.outputs.get(out.name), (int, float))] + if not numeric_candidates: + continue + by_value = sorted(numeric_candidates, key=lambda c: float(c.evaluation.outputs[out.name])) + for label, c in [("minimum", by_value[0]), ("maximum", by_value[-1])]: + reps.append((c, f"Output {label}: {out.name}={c.evaluation.outputs[out.name]!r}")) + zeroish = sorted(numeric_candidates, key=lambda c: abs(float(c.evaluation.outputs[out.name]))) + if zeroish: + reps.append((zeroish[0], f"Output near zero boundary: {out.name}={zeroish[0].evaluation.outputs[out.name]!r}")) + return reps + + +# --------------------------------------------------------------------------- +# Output formatting +# --------------------------------------------------------------------------- + + +def variable_type(value: Any) -> str: + if isinstance(value, bool): + return "Boolean" + if isinstance(value, (int, float)): + return "Double" + return "String" + + +def typed_variables(inputs: Dict[str, Any]) -> Dict[str, Dict[str, Any]]: + return { + k: {"value": jsonable(v), "type": variable_type(v)} + for k, v in inputs.items() + } + + +def format_value_for_expected(value: Any, type_ref: Optional[str] = None) -> str: + if value is None: + return "null" + if isinstance(value, float): + return f"{value:.10g}" if not value.is_integer() else f"{value:.1f}" + if isinstance(value, int) and type_ref in {"double", "number"}: + return f"{float(value):.1f}" + if isinstance(value, int): + return str(value) + return str(value) + + +def expected_string(decision: DmnDecision, evaluation: TableEvaluation) -> str: + if not evaluation.outputs: + if decision.outputs: + return ", ".join(f"{out.name}=null" for out in decision.outputs) + return "noOutputs=true" + parts = [] + for out in decision.outputs: + parts.append(f"{out.name}={format_value_for_expected(evaluation.outputs.get(out.name), out.type_ref)}") + return ", ".join(parts) + + +def case_name(index: int, decision: DmnDecision, candidate: Candidate, primary_reason: str) -> str: + reason = primary_reason.split(":", 1)[0] + selected = candidate.evaluation.selected_rule_id or "no matching rule" + return f"TC_{index:03d} {decision.name} - {reason} - {selected}" + + +def build_outputs( + dmn_path: Path, + decisions: Dict[str, DmnDecision], + all_domains: Dict[str, Dict[str, List[Any]]], + all_candidates: Dict[str, List[Candidate]], + max_cases_per_decision: Optional[int] = None, +) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]: + all_cases: List[Dict[str, Any]] = [] + coverage_summary: Dict[str, Any] = {} + uncovered_all: List[str] = [] + + for decision in decisions.values(): + candidates = all_candidates[decision.id] + domains = all_domains[decision.id] + by_key: Dict[str, Dict[str, Any]] = {} + + def add_candidate(c: Candidate, reason: str) -> None: + key = json.dumps(c.inputs, sort_keys=True, default=str) + if key not in by_key: + by_key[key] = { + "candidate": c, + "reasons": [], + } + if reason not in by_key[key]["reasons"]: + by_key[key]["reasons"].append(reason) + + pairs, uncovered = select_mcdc_pairs_for_decision(decision, candidates) + uncovered_all.extend(uncovered) + for a, b, reason in pairs: + add_candidate(a, reason + " baseline") + add_candidate(b, reason + " flipped") + + for c, reason in choose_boundary_representatives(domains, candidates): + add_candidate(c, reason) + for c, reason in choose_rule_representatives(decision, candidates): + add_candidate(c, reason) + for c, reason in choose_output_extreme_representatives(decision, candidates): + add_candidate(c, reason) + + selected_items = list(by_key.values()) + selected_items.sort(key=lambda item: json.dumps(item["candidate"].inputs, sort_keys=True, default=str)) + if max_cases_per_decision: + selected_items = selected_items[:max_cases_per_decision] + + decision_case_count = 0 + for item in selected_items: + c: Candidate = item["candidate"] + reasons: List[str] = item["reasons"] + case_index = len(all_cases) + 1 + all_cases.append( + { + "name": case_name(case_index, decision, c, reasons[0]), + "decisionId": decision.id, + "decisionName": decision.name, + "decisionTableId": decision.table_id, + "evaluationMode": "direct-table-inputs", + "expected": expected_string(decision, c.evaluation), + "requestBody": {"variables": typed_variables(c.inputs)}, + "coverage": { + "selectedRuleId": c.evaluation.selected_rule_id, + "selectedRuleIndex": c.evaluation.selected_rule_index, + "reasons": reasons, + }, + } + ) + decision_case_count += 1 + + atom_count = 0 + for rule in decision.rules: + for c in candidates[:1_000]: + rt = c.evaluation.rule_traces.get(rule.id) + if rt: + atom_count += len(rt.atoms) + break + + coverage_summary[decision.id] = { + "decisionName": decision.name, + "decisionTableId": decision.table_id, + "hitPolicy": decision.hit_policy, + "candidateCountEvaluated": len(candidates), + "selectedTestCaseCount": decision_case_count, + "inputDomainsUsed": jsonable(domains), + "rules": [ + { + "ruleIndex": idx, + "ruleId": rule.id, + "description": rule.description, + "inputEntries": rule.input_entries, + "outputEntries": rule.output_entries, + } + for idx, rule in enumerate(decision.rules, start=1) + ], + "uncoveredConditions": uncovered, + } + + analysis = { + "metadata": { + "sourceDmn": str(dmn_path), + "algorithm": "Per-decision-table boundary domains + MC/DC-style condition-pair selection", + "note": ( + "Each decision table is tested in direct-table-input mode. Non-boolean DMN entries " + "such as date ranges, comparisons, and string equality tests are treated as atomic " + "boolean predicates for MC/DC pair selection. Boundary/domain representatives are " + "added so all generated input ranges appear in at least one case per table." + ), + "decisionCount": len(decisions), + "selectedTestCaseCount": len(all_cases), + }, + "decisions": coverage_summary, + "uncoveredConditions": uncovered_all, + } + return all_cases, analysis + + +def generate( + dmn_path: Path, + max_candidates_per_decision: int = 100_000, + max_cases_per_decision: Optional[int] = None, +) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]: + decisions = parse_dmn(dmn_path) + model_text = all_model_text(decisions) + constant_defaults = compute_constant_defaults(decisions) + + all_domains: Dict[str, Dict[str, List[Any]]] = {} + all_candidates: Dict[str, List[Candidate]] = {} + + for decision in decisions.values(): + domains = domains_for_decision(decision, constant_defaults, model_text) + all_domains[decision.id] = domains + candidates: List[Candidate] = [] + errors = 0 + for inputs in candidate_inputs(domains, max_candidates_per_decision): + try: + # Direct-table mode: the request body contains all variables + # required to evaluate the selected table. Defaults from zero-input + # constant tables are also added when referenced. + evaluation = evaluate_decision_table_direct(decision, dict(inputs)) + candidates.append(Candidate(decision_id=decision.id, inputs=dict(inputs), evaluation=evaluation)) + except Exception as e: + errors += 1 + if errors <= 3: + print(f"Skipping {decision.id} candidate {inputs}: {e}", file=sys.stderr) + if not candidates: + raise RuntimeError(f"No evaluable candidates generated for decision {decision.id}") + all_candidates[decision.id] = candidates + + return build_outputs(dmn_path, decisions, all_domains, all_candidates, max_cases_per_decision=max_cases_per_decision) + + + +# --------------------------------------------------------------------------- +# Postman collection generation +# --------------------------------------------------------------------------- + +JsonObj = Dict[str, Any] + + +def load_json(path: str | Path) -> Any: + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + + +def dump_json(data: Any, path: str | Path) -> None: + with open(path, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + f.write("\n") + + +def decision_key_from_item(item: JsonObj) -> Optional[str]: + """Extract `/decision-definition/key/{key}/...` from a Postman item.""" + url = item.get("request", {}).get("url", {}) + path = url.get("path", []) + if isinstance(path, list) and "key" in path: + idx = path.index("key") + if idx + 1 < len(path): + return path[idx + 1] + raw = url.get("raw", "") if isinstance(url, dict) else "" + m = re.search(r"/decision-definition/key/([^/]+)/", raw) + return m.group(1) if m else None + + +def iter_postman_items(items: Iterable[JsonObj]) -> Iterable[JsonObj]: + """Yield all request items, including nested folder items.""" + for item in items: + if "item" in item: + yield from iter_postman_items(item.get("item", [])) + elif "request" in item: + yield item + + +def choose_template_items(base_collection: JsonObj, needed_keys: Iterable[str]) -> Dict[str, JsonObj]: + """Pick one canonical request item per decision key from a Postman collection.""" + candidates: Dict[str, List[JsonObj]] = defaultdict(list) + for item in iter_postman_items(base_collection.get("item", [])): + key = decision_key_from_item(item) + if key: + candidates[key].append(item) + + selected: Dict[str, JsonObj] = {} + for key in needed_keys: + options = candidates.get(key, []) + if not options: + continue + + def score(item: JsonObj) -> Tuple[int, int, int]: + name = item.get("name", "").lower() + no_test = 1 if ("test" not in name and "experiment" not in name) else 0 + has_examples = 1 if item.get("response") else 0 + shorter_name = -len(name) + return (no_test, has_examples, shorter_name) + + selected[key] = max(options, key=score) + return selected + + +def infer_postman_value_type(value: Any) -> str: + if value is None: + return "Null" + if isinstance(value, bool): + return "Boolean" + if isinstance(value, int) and not isinstance(value, bool): + return "Integer" + if isinstance(value, float): + return "Double" + return "String" + + +def parse_expected_string(expected: str) -> JsonObj: + """Convert 'a=1.0, b=text' into an Operaton/Camunda-style response row.""" + result: JsonObj = {} + if not expected: + return result + parts = [p.strip() for p in re.split(r",\s*(?=[A-Za-z_][A-Za-z0-9_]*\s*=)", expected)] + for part in parts: + if "=" not in part: + continue + key, raw_value = part.split("=", 1) + key = key.strip() + raw_value = raw_value.strip() + if raw_value.lower() == "null": + value = None + elif raw_value.lower() == "true": + value = True + elif raw_value.lower() == "false": + value = False + else: + try: + value = float(raw_value) + except ValueError: + value = raw_value.strip('"') + result[key] = {"type": infer_postman_value_type(value), "value": value} + return result + + +def make_original_request(template_request: JsonObj, request_body: JsonObj) -> JsonObj: + original = copy.deepcopy(template_request) + original.pop("auth", None) # parent request retains auth; examples stay compact + original["body"] = { + "mode": "raw", + "raw": json.dumps(request_body, ensure_ascii=False, indent=2), + "options": {"raw": {"language": "json"}}, + } + return original + + +def make_postman_example(test_case: JsonObj, template_request: JsonObj) -> JsonObj: + expected_row = parse_expected_string(test_case.get("expected", "")) + coverage = test_case.get("coverage", {}) or {} + reasons = coverage.get("reasons", []) or [] + description_lines = [ + f"Decision: {test_case.get('decisionName') or test_case.get('decisionId')}", + f"Decision table: {test_case.get('decisionTableId', '')}", + f"Expected: {test_case.get('expected', '')}", + ] + if reasons: + description_lines.append("Coverage reasons:") + description_lines.extend(f"- {reason}" for reason in reasons) + + return { + "name": test_case.get("name", "MC/DC test case"), + "originalRequest": make_original_request(template_request, test_case.get("requestBody", {})), + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [{"key": "Content-Type", "value": "application/json"}], + "cookie": [], + "body": json.dumps([expected_row], ensure_ascii=False, indent=2), + "description": "\n".join(description_lines), + } + + +def engine_rest_base_url(base_url: str) -> str: + """Return a base URL that ends with /engine-rest exactly once.""" + base = base_url.rstrip("/") + if base.endswith("/engine-rest"): + return base + return f"{base}/engine-rest" + + +def postman_url_for_decision(base_url: str, decision_id: str, tenant_id: str) -> JsonObj: + raw = f"{engine_rest_base_url(base_url)}/decision-definition/key/{decision_id}/tenant-id/{tenant_id}/evaluate" + m = re.match(r"^(https?)://([^/]+)(/.*)$", raw) + if not m: + return {"raw": raw} + protocol, host, path = m.groups() + return {"raw": raw, "protocol": protocol, "host": host.split("."), "path": [p for p in path.strip("/").split("/") if p]} + + +def make_synthetic_template_item(decision_id: str, base_url: str, tenant_id: str, request_body: JsonObj) -> JsonObj: + return { + "name": decision_id, + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": json.dumps(request_body, ensure_ascii=False, indent=2), + "options": {"raw": {"language": "json"}}, + }, + "url": postman_url_for_decision(base_url, decision_id, tenant_id), + }, + "response": [], + } + + +def make_decision_item(decision_id: str, cases: List[JsonObj], template_item: JsonObj) -> JsonObj: + item = copy.deepcopy(template_item) + decision_name = cases[0].get("decisionName") or decision_id + item["name"] = f"{decision_id} - MC/DC examples ({len(cases)})" + item["description"] = ( + f"Generated MC/DC and boundary-value examples for `{decision_name}`. " + f"Each example contains the request body for one generated test case and " + f"an expected Operaton/Camunda-style decision-evaluation response body." + ) + item["request"]["body"] = { + "mode": "raw", + "raw": json.dumps(cases[0].get("requestBody", {}), ensure_ascii=False, indent=2), + "options": {"raw": {"language": "json"}}, + } + item["response"] = [make_postman_example(tc, item["request"]) for tc in cases] + item.pop("event", None) + return item + + +def make_summary_item(test_cases: List[JsonObj]) -> JsonObj: + counts = Counter(tc.get("decisionId", "UNKNOWN") for tc in test_cases) + body = { + "summary": "Generated MC/DC examples grouped under the corresponding DMN decision-evaluate request.", + "totalTestCases": len(test_cases), + "testCasesPerDecision": [[decision_id, count] for decision_id, count in sorted(counts.items())], + } + return { + "name": "MC/DC generation summary", + "request": { + "method": "GET", + "header": [], + "url": {"raw": "about:blank", "host": ["about:blank"]}, + "description": json.dumps(body, ensure_ascii=False, indent=2), + }, + "response": [], + } + + +def generate_postman_collection( + test_cases: List[JsonObj], + postman_template: Optional[Path], + base_url: str, + tenant_id: str, +) -> JsonObj: + by_decision: Dict[str, List[JsonObj]] = defaultdict(list) + for tc in test_cases: + by_decision[tc.get("decisionId", "UNKNOWN")].append(tc) + + if postman_template: + base_collection = load_json(postman_template) + templates = choose_template_items(base_collection, by_decision.keys()) + else: + base_collection = { + "info": { + "_postman_id": str(uuid.uuid4()), + "name": "DMN MC/DC generated collection", + "description": "Generated from DMN and MC/DC boundary test cases.", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + }, + "item": [], + } + templates = {} + + # Synthesize any request missing from the optional template. + for key, cases in by_decision.items(): + if key not in templates: + templates[key] = make_synthetic_template_item(key, base_url, tenant_id, cases[0].get("requestBody", {})) + + output = copy.deepcopy(base_collection) + output["info"] = copy.deepcopy(base_collection.get("info", {})) + output["info"]["_postman_id"] = str(uuid.uuid4()) + output["info"]["schema"] = output["info"].get("schema", "https://schema.getpostman.com/json/collection/v2.1.0/collection.json") + base_name = output["info"].get("name", "Postman collection") + output["info"]["name"] = f"{base_name} - MC/DC examples" + base_description = output["info"].get("description", "") + output["info"]["description"] = ( + f"{base_description}\n\nGenerated collection with {len(test_cases)} MC/DC and boundary-value test cases " + f"grouped as Postman examples under their corresponding DMN decision calls." + ).strip() + + utility_items: List[JsonObj] = [] + if postman_template: + for item in base_collection.get("item", []): + key = decision_key_from_item(item) if "request" in item else None + if key is None and "request" in item: + utility_items.append(copy.deepcopy(item)) + + preferred_order = [ + "BehaalbareHoogteSubsidie", + "BerekenBasisHoogteSubsidie", + "BerekenBeschikbaarSubsidiePlafond", + "SubsidieConstantenThuisbatterij", + "jaarGebondenBudget", + ] + ordered_keys = [key for key in preferred_order if key in by_decision] + [ + key for key in sorted(by_decision.keys()) if key not in preferred_order + ] + folder = { + "name": "DMN decision calls with generated MC/DC examples", + "description": "One request per DMN decision key. Test cases are attached as Postman examples.", + "item": [make_decision_item(key, by_decision[key], templates[key]) for key in ordered_keys], + } + output["item"] = [make_summary_item(test_cases), folder] + utility_items + return output + + +# --------------------------------------------------------------------------- +# Excel workbook generation +# --------------------------------------------------------------------------- + + +def excel_col_name(index: int) -> str: + """1-based column number to Excel column letters.""" + letters = "" + while index: + index, rem = divmod(index - 1, 26) + letters = chr(65 + rem) + letters + return letters + + +def write_matrix(sheet: Any, start_row: int, start_col: int, rows: List[List[Any]]) -> None: + """Write a 2D matrix using 1-based row/col coordinates.""" + if not rows: + return + row_count = len(rows) + col_count = max(len(r) for r in rows) + padded = [r + [None] * (col_count - len(r)) for r in rows] + end_col = excel_col_name(start_col + col_count - 1) + rng = f"{excel_col_name(start_col)}{start_row}:{end_col}{start_row + row_count - 1}" + sheet.get_range(rng).values = padded + + +def add_table_if_possible(sheet: Any, address: str, name: str) -> None: + try: + sheet.tables.add(address, True, name) + except Exception: + # The workbook remains useful without table objects if the runtime lacks this API. + pass + + +def style_basic_sheet(sheet: Any, used_range: str, header_range: str = "A1:Z1") -> None: + try: + sheet.freeze_panes.freeze_rows(1) + except Exception: + pass + try: + sheet.get_range(header_range).format = { + "fill": "#0F766E", + "font": {"bold": True, "color": "#FFFFFF"}, + "horizontal_alignment": "center", + "vertical_alignment": "center", + } + except Exception: + pass + try: + sheet.get_range(used_range).format.wrap_text = True + sheet.get_range(used_range).format.autofit_columns() + sheet.get_range(used_range).format.autofit_rows() + except Exception: + pass + + +def generate_excel_workbook( + test_cases: List[JsonObj], + analysis: JsonObj, + output_path: Path, + *, + base_url: str = "https://operaton.open-regels.nl", + tenant_id: str = "46", + postman_path: Optional[Path] = None, +) -> None: + """Create a local-friendly Excel workbook with dashboard, run links and chart. + + The workbook intentionally uses openpyxl rather than artifact_tool so it can + be generated on a normal Windows/macOS/Linux Python installation: + + python -m pip install openpyxl + + The "buttons" are styled Excel cells with hyperlinks. Standard .xlsx files + cannot execute POST requests without macros/VBA, so these cells point to the + generated Postman collection and decision endpoints. Use Postman/Newman for + real automated execution. + """ + try: + from openpyxl import Workbook as OpenPyxlWorkbook + from openpyxl.chart import PieChart, Reference + from openpyxl.styles import Alignment, Border, Font, PatternFill, Side + from openpyxl.worksheet.table import Table, TableStyleInfo + from openpyxl.utils import get_column_letter + except ModuleNotFoundError as e: # pragma: no cover - local dependency guidance + raise RuntimeError( + "Excel generation requires openpyxl. Install it with: " + "python -m pip install openpyxl. Alternatively run with --skip-excel." + ) from e + + counts = Counter(tc.get("decisionId", "UNKNOWN") for tc in test_cases) + uncovered = analysis.get("uncoveredConditions", []) or [] + decisions = analysis.get("decisions") or {} + output_path = Path(output_path) + postman_path = Path(postman_path) if postman_path else None + + var_names = sorted({ + name + for tc in test_cases + for name in (tc.get("requestBody", {}).get("variables", {}) or {}).keys() + }) + + def endpoint_for(decision_id: str) -> str: + return postman_url_for_decision(base_url, decision_id, tenant_id).get("raw", "") + + def expected_response_body(expected: str) -> str: + """Small preview of the expected decision response for the workbook.""" + result: JsonObj = {} + for part in [p.strip() for p in str(expected or "").split(",") if p.strip()]: + if "=" not in part: + continue + key, value_text = part.split("=", 1) + key = key.strip() + value_text = value_text.strip() + if not key: + continue + try: + value: Any = float(value_text) + except ValueError: + value = value_text.strip('"') + value_type = "Double" if isinstance(value, float) else "String" + result[key] = {"type": value_type, "value": value} + return json.dumps([result], ensure_ascii=False, indent=2) if result else "" + + def safe_uri(path: Path) -> str: + try: + return path.resolve().as_uri() + except Exception: + return str(path) + + # ------------------------------------------------------------------ + # Prepare sheet data + # ------------------------------------------------------------------ + case_headers = [ + "#", + "Run", + "Name", + "Decision ID", + "Decision Name", + "Decision Table ID", + "Expected", + "Selected Rule ID", + "Selected Rule Index", + "Coverage Reasons", + ] + var_names + ["Endpoint", "Expected Response Preview", "Request Body JSON"] + + case_rows: List[List[Any]] = [case_headers] + for idx, tc in enumerate(test_cases, start=1): + variables = tc.get("requestBody", {}).get("variables", {}) or {} + coverage = tc.get("coverage", {}) or {} + decision_id = tc.get("decisionId", "") + row = [ + idx, + "RUN", + tc.get("name", ""), + decision_id, + tc.get("decisionName", ""), + tc.get("decisionTableId", ""), + tc.get("expected", ""), + coverage.get("selectedRuleId", ""), + coverage.get("selectedRuleIndex", ""), + "\n".join(coverage.get("reasons", []) or []), + ] + for var in var_names: + spec = variables.get(var) + row.append(spec.get("value") if isinstance(spec, dict) else None) + row.extend([ + endpoint_for(decision_id), + expected_response_body(tc.get("expected", "")), + json.dumps(tc.get("requestBody", {}), ensure_ascii=False, indent=2), + ]) + case_rows.append(row) + + domain_rows: List[List[Any]] = [["Decision ID", "Decision Name", "Variable", "Domain Values"]] + for decision_id, details in sorted(decisions.items()): + for var_name, values in sorted((details.get("inputDomainsUsed") or {}).items()): + domain_rows.append([ + decision_id, + details.get("decisionName", ""), + var_name, + json.dumps(values, ensure_ascii=False), + ]) + + rule_rows: List[List[Any]] = [["Decision ID", "Decision Name", "Rule Index", "Rule ID", "Description", "Input Entries", "Output Entries"]] + for decision_id, details in sorted(decisions.items()): + for rule in details.get("rules", []) or []: + rule_rows.append([ + decision_id, + details.get("decisionName", ""), + rule.get("ruleIndex", ""), + rule.get("ruleId", ""), + rule.get("description", ""), + json.dumps(rule.get("inputEntries", []), ensure_ascii=False), + json.dumps(rule.get("outputEntries", []), ensure_ascii=False), + ]) + + wb = OpenPyxlWorkbook() + default_sheet = wb.active + wb.remove(default_sheet) + ws_summary = wb.create_sheet("Summary") + ws_cases = wb.create_sheet("Test Cases") + ws_domains = wb.create_sheet("Input Domains") + ws_rules = wb.create_sheet("Rules") + + # ------------------------------------------------------------------ + # Styling helpers + # ------------------------------------------------------------------ + dark_fill = PatternFill("solid", fgColor="0F766E") + medium_fill = PatternFill("solid", fgColor="14B8A6") + light_fill = PatternFill("solid", fgColor="CCFBF1") + grey_fill = PatternFill("solid", fgColor="F3F4F6") + white_font = Font(color="FFFFFF", bold=True) + title_font = Font(size=16, bold=True, color="0F172A") + subtitle_font = Font(size=11, color="475569") + header_font = Font(bold=True, color="FFFFFF") + button_font = Font(bold=True, color="FFFFFF", underline="single") + thin_border = Border( + left=Side(style="thin", color="D1D5DB"), + right=Side(style="thin", color="D1D5DB"), + top=Side(style="thin", color="D1D5DB"), + bottom=Side(style="thin", color="D1D5DB"), + ) + + def append_rows(ws: Any, rows: List[List[Any]]) -> None: + for row in rows: + ws.append(row) + + def apply_header(ws: Any, row: int = 1) -> None: + for cell in ws[row]: + cell.fill = dark_fill + cell.font = header_font + cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True) + cell.border = thin_border + + def add_table(ws: Any, table_name: str, start_row: int = 1, end_col: Optional[int] = None) -> None: + if ws.max_row < start_row or ws.max_column < 1: + return + max_col = end_col or ws.max_column + ref = f"A{start_row}:{get_column_letter(max_col)}{ws.max_row}" + table = Table(displayName=table_name, ref=ref) + table.tableStyleInfo = TableStyleInfo( + name="TableStyleMedium2", + showFirstColumn=False, + showLastColumn=False, + showRowStripes=True, + showColumnStripes=False, + ) + try: + ws.add_table(table) + except ValueError: + pass + + def autosize(ws: Any, max_width: int = 60) -> None: + for column_cells in ws.columns: + col_letter = get_column_letter(column_cells[0].column) + max_len = 0 + for cell in column_cells: + if cell.value is None: + continue + max_len = max(max_len, min(len(str(cell.value)), max_width)) + ws.column_dimensions[col_letter].width = max(10, min(max_len + 2, max_width)) + for row in ws.iter_rows(): + for cell in row: + cell.alignment = Alignment(vertical="top", wrap_text=True) + + def style_button(cell: Any, label: str, link: Optional[str] = None, fill: Optional[PatternFill] = None) -> None: + cell.value = label + cell.fill = fill or dark_fill + cell.font = button_font + cell.alignment = Alignment(horizontal="center", vertical="center") + cell.border = thin_border + if link: + cell.hyperlink = link + cell.style = "Hyperlink" + cell.fill = fill or dark_fill + cell.font = button_font + + # ------------------------------------------------------------------ + # Summary / dashboard with run buttons and pie chart + # ------------------------------------------------------------------ + ws_summary.merge_cells("A1:F1") + ws_summary["A1"] = "DMN MC/DC Test Generation Summary" + ws_summary["A1"].font = title_font + ws_summary["A1"].alignment = Alignment(horizontal="left") + + ws_summary.merge_cells("A2:F2") + ws_summary["A2"] = "Boundary-focused MC/DC cases generated per DMN decision table. Use Postman/Newman for real POST execution." + ws_summary["A2"].font = subtitle_font + + metric_rows = [ + ("Source DMN", analysis.get("metadata", {}).get("sourceDmn", "")), + ("Algorithm", analysis.get("metadata", {}).get("algorithm", "")), + ("Decision count", analysis.get("metadata", {}).get("decisionCount", len(counts))), + ("Selected test cases", len(test_cases)), + ("Uncovered conditions", len(uncovered)), + ] + ws_summary["A4"] = "Metric" + ws_summary["B4"] = "Value" + for c in ws_summary[4][0:2]: + c.fill = dark_fill + c.font = header_font + c.alignment = Alignment(horizontal="center") + c.border = thin_border + for idx, (metric, value) in enumerate(metric_rows, start=5): + ws_summary.cell(idx, 1, metric) + ws_summary.cell(idx, 2, value) + ws_summary.cell(idx, 1).fill = grey_fill + ws_summary.cell(idx, 1).font = Font(bold=True) + ws_summary.cell(idx, 1).border = thin_border + ws_summary.cell(idx, 2).border = thin_border + + # Button-like cells. They are hyperlinks because standard .xlsx cannot issue POST requests without macros. + postman_uri = safe_uri(postman_path) if postman_path else None + ws_summary.merge_cells("D4:F5") + style_button(ws_summary["D4"], "RUN ALL IN POSTMAN", postman_uri, dark_fill) + ws_summary.merge_cells("D7:F8") + style_button(ws_summary["D7"], "OPEN TEST CASES", "#'Test Cases'!A1", medium_fill) + ws_summary["D10"] = "Newman command" + ws_summary["D10"].fill = grey_fill + ws_summary["D10"].font = Font(bold=True) + ws_summary["E10"] = f"newman run \"{postman_path.name if postman_path else '.json'}\"" + ws_summary["E10"].alignment = Alignment(wrap_text=True) + ws_summary.merge_cells("E10:F10") + ws_summary["D11"] = "Note" + ws_summary["D11"].fill = grey_fill + ws_summary["D11"].font = Font(bold=True) + ws_summary["E11"] = "The RUN cells are clickable links. To execute all POST requests, import/run the generated Postman collection or use Newman." + ws_summary.merge_cells("E11:F11") + ws_summary["E11"].alignment = Alignment(wrap_text=True) + + # Decision summary table and pie chart source data + decision_start = 14 + decision_headers = ["Decision ID", "Decision name", "Hit policy", "Candidate count", "Selected cases"] + for col_idx, value in enumerate(decision_headers, start=1): + cell = ws_summary.cell(decision_start, col_idx, value) + cell.fill = dark_fill + cell.font = header_font + cell.alignment = Alignment(horizontal="center", wrap_text=True) + cell.border = thin_border + row_idx = decision_start + 1 + for decision_id, details in sorted(decisions.items()): + values = [ + decision_id, + details.get("decisionName", ""), + details.get("hitPolicy", ""), + details.get("candidateCountEvaluated", ""), + counts.get(decision_id, 0), + ] + for col_idx, value in enumerate(values, start=1): + cell = ws_summary.cell(row_idx, col_idx, value) + cell.border = thin_border + cell.alignment = Alignment(vertical="top", wrap_text=True) + row_idx += 1 + + add_table(ws_summary, "DecisionSummary", decision_start, end_col=5) + + if counts: + chart = PieChart() + labels = Reference(ws_summary, min_col=1, min_row=decision_start + 1, max_row=row_idx - 1) + data = Reference(ws_summary, min_col=5, min_row=decision_start, max_row=row_idx - 1) + chart.add_data(data, titles_from_data=True) + chart.set_categories(labels) + chart.title = "MC/DC cases by decision" + chart.height = 8 + chart.width = 11 + ws_summary.add_chart(chart, "G4") + + ws_summary.freeze_panes = "A14" + ws_summary.column_dimensions["A"].width = 34 + ws_summary.column_dimensions["B"].width = 28 + ws_summary.column_dimensions["C"].width = 16 + ws_summary.column_dimensions["D"].width = 18 + ws_summary.column_dimensions["E"].width = 36 + ws_summary.column_dimensions["F"].width = 22 + + # ------------------------------------------------------------------ + # Test cases sheet with individual run links + # ------------------------------------------------------------------ + append_rows(ws_cases, case_rows) + apply_header(ws_cases, 1) + add_table(ws_cases, "GeneratedTestCases", 1) + ws_cases.freeze_panes = "A2" + ws_cases.auto_filter.ref = f"A1:{get_column_letter(ws_cases.max_column)}{ws_cases.max_row}" + endpoint_col = case_headers.index("Endpoint") + 1 + for r in range(2, ws_cases.max_row + 1): + decision_id = ws_cases.cell(r, 4).value + run_cell = ws_cases.cell(r, 2) + style_button(run_cell, "RUN", endpoint_for(str(decision_id)), medium_fill) + ws_cases.cell(r, endpoint_col).hyperlink = str(ws_cases.cell(r, endpoint_col).value or "") + ws_cases.cell(r, endpoint_col).style = "Hyperlink" + autosize(ws_cases) + ws_cases.column_dimensions["B"].width = 12 + ws_cases.column_dimensions["C"].width = 46 + ws_cases.column_dimensions["J"].width = 48 + ws_cases.column_dimensions[get_column_letter(ws_cases.max_column)].width = 64 + ws_cases.column_dimensions[get_column_letter(ws_cases.max_column - 1)].width = 48 + + # ------------------------------------------------------------------ + # Supporting sheets + # ------------------------------------------------------------------ + append_rows(ws_domains, domain_rows) + apply_header(ws_domains, 1) + add_table(ws_domains, "InputDomains", 1) + ws_domains.freeze_panes = "A2" + autosize(ws_domains) + ws_domains.column_dimensions["D"].width = 72 + + append_rows(ws_rules, rule_rows) + apply_header(ws_rules, 1) + add_table(ws_rules, "Rules", 1) + ws_rules.freeze_panes = "A2" + autosize(ws_rules) + ws_rules.column_dimensions["F"].width = 56 + ws_rules.column_dimensions["G"].width = 56 + + # Apply light borders and readable row heights across data sheets. + for ws in [ws_cases, ws_domains, ws_rules]: + for row in ws.iter_rows(): + for cell in row: + cell.border = thin_border + cell.alignment = Alignment(vertical="top", wrap_text=True) + ws.sheet_view.showGridLines = False + ws_summary.sheet_view.showGridLines = False + + output_path.parent.mkdir(parents=True, exist_ok=True) + wb.save(output_path) + +# --------------------------------------------------------------------------- +# Macro-enabled Excel workbook generation +# --------------------------------------------------------------------------- + +RUN_ALL_MACRO = "PostmanTestRunner.RunAllTests" +RUN_SELECTED_MACRO = "PostmanTestRunner.RunSelectedTest" + +# Embedded macro-enabled runner workbook. This is the small working template used +# only as the container for vbaProject.bin, workbook relationships, and Excel +# form-control support files. Generated Dashboard/Tests XML replaces the template +# sheets at runtime, so callers do not need to keep a separate grr.bla file next +# to the script. +EMBEDDED_RUNNER_TEMPLATE_B64 = """ +UEsDBBQABgAIAAAAIQDox6z/FAIAAAcLAAATAAgCW0NvbnRlbnRfVHlwZXNdLnhtbCCiBAIooAAC +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADE +ls1y2jAQx++d6Tt4dO3YImmbdjqYHJr02GYm6QMIacEqsqTRKgTevusP6EcA45jSi2WQd3+7f3nX +O75elSZZQkDtbM4ushFLwEqntJ3n7PvDl/QjSzAKq4RxFnK2BmTXk9evxg9rD5iQtcWcFTH6T5yj +LKAUmDkPlnZmLpQi0s8w517IhZgDvxyNrrh0NoKNaax8sMn4Bmbi0cTkdkV/N5FMtWXJ5+a5CpUz +4b3RUkQKlC+t+guSutlMS1BOPpbkOkMfQCgsAGJpMh80EcM9xEiJIeM7mQEM9oO2WWVkWQeGhfb4 +hlLfQ1jSzpCsyP4miCfKYQ+gQu8HtIF9o/MOWkFyJ0L8KkoSl68Mf3JhMXVukR12UmlfYgorCSar +Bc5KIYO7tWJqgPaEthsJDpBqS+T1cnEE8s936vBxV5nUjnvGcfmf4ohUNsDr63ApajcdiWNcG8AT +Z9s47SIXIoC6j1SQ85MH8LvvjjhUU0bI25vhureOOrgyBnMXnEe+uTuGvK23qnMGZzx5gBA1bJvN +nkqTJEkkVrWcLEVqqLXDnhIPr66XSnwM+eUSPzvRt306aO8TfYZ7d17c+/Pirs6L+/AvcVSz6Czy +Zu1Daiw6Cm45FdRafoCMWff4RO97MzJlv8yqoWJXI6Gpqu1ZLkD/r8ZmSpJknR7XurZEmvoGf6ag +GisVqB1sXo+xk58AAAD//wMAUEsDBBQABgAIAAAAIQC1VTAj9AAAAEwCAAALAAgCX3JlbHMvLnJl +bHMgogQCKKAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAArJJNT8MwDIbvSPyHyPfV3ZAQQkt3QUi7IVR+gEncD7WNoyQb3b8nHBBUGoMDR3+9 +fvzK2908jerIIfbiNKyLEhQ7I7Z3rYaX+nF1ByomcpZGcazhxBF21fXV9plHSnkodr2PKqu4qKFL +yd8jRtPxRLEQzy5XGgkTpRyGFj2ZgVrGTVneYviuAdVCU+2thrC3N6Dqk8+bf9eWpukNP4g5TOzS +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 +GDl0cyyx7lMnD/d0re96zu//fqAplno5sUPcUNCZXJfEJPGeqk/tTiVDE3+4s7itfNGO3faiq6du +q7TLaorKfuvdgm+s2CGKdt4+fnNjz77WPZErxj9XvmhDVbQsNN7XdWt8Q9W4/29vLKmJ14y29n++ +qqnta4mp5K3x9nFf97N/FMUfo0vG1i/o706ctW94/Fk73h5su/RnG8oPv3Pv//QtL+tOiCPHVr12 +5KlOe+vfv3zlSImdeHRxzfafRX1F7x/8ZNT/nL9sZey17951yUgp//TK5ZfckjxcO9TTZ5eN19op +8fvLxV4s1498s72qzKq0m27sPtSTOPKJg+WX3Txe31/U8NhNTbFfdo1EY+01F629OfJHkb+dTvV0 +/+2uptq3/ate3vu5Ezcfs75af/i9dN2xwo6iHQ/bnTvqWru6Yl/f5H92+/jWpo5WLGxZoRsKN1Q1 +98T3zMYOpWqe//zuC7buDf901dme4kf9r969f9m7lU9c8NVqe21/v++IXML+5NVP/Li4N9HV4zti +N38yyIXe9wHGivrixZf8GaDWtmtTveOp5xN/U/LJQG+yFOvOP/3Cn/nseGtULjFfv3hqTc+4Wk7+ +xZri+307ipJy0bqur6c99ovA39R9u2rzwkSq9d7JdHzkiVd8f+Tvse5Z56+Vs/97053/evHo9bfs +XrFpwYjv1jNNKz7ri/7+0eU/8dU8svwF3/eaiovOLHm2sH7FZ/0NLSt+/BPfvq4Vy6wbYg3fGHru +w/bYmhVWXXDflV+97bef333lz2cWN684c5td9pzvg9u+vfRYtOR3LbsMK/It1t/vvvBfAf6Bb9ZK +rTxjfTV+U1npmDVw0Z8B+Zzvn8asW79cesayl7wKgtcuvv6sNfmnT10ya/1q1fV+u2xHgWU/HB70 +P7D6q7jrX2mpNfFfrPn5L/xfWvNqzQ77gWeaDl95yfu9h/3+zXXg47/0X1qP1ILNL6x/d8k/Lnh0 +5ScW9XSd7S292V+486uFTxT+ANBXdl3/u/5+/6cu/qH/WPBTjTvs8drezq72xhV/unzl6O5Ez5Gh +vicfTTR/+nPXPvSVS95aYB/tW7D+vvv27Fg0dSbWNvUO/fJvFCwMNa2utMcvrvxPt/9O0PduSeXu +/ZV28vKmhQOLmm8PB3u6j8YS46mdWGxIDr33g6bK5p4/WXVZcyiwqNI+e1+JWPAXy7+7rO2aG5at +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 +eGwvZHJhd2luZ3MvZHJhd2luZzEueG1sUEsBAi0AFAAGAAgAAAAhAJI7gS+lAgAAXAUAABsAAAAA +AAAAAAAAAAAAnS4AAHhsL2RyYXdpbmdzL3ZtbERyYXdpbmcxLnZtbFBLAQItABQABgAIAAAAIQDP +b5agjgQAAFwLAAAUAAAAAAAAAAAAAAAAAHsxAAB4bC9jaGFydHMvY2hhcnQxLnhtbFBLAQItABQA +BgAIAAAAIQDWStfAdQQAAConAAAYAAAAAAAAAAAAAAAAADs2AAB4bC9kcmF3aW5ncy9kcmF3aW5n +Mi54bWxQSwECLQAUAAYACAAAACEA4Tir2SwDAAASFgAAGwAAAAAAAAAAAAAAAADmOgAAeGwvZHJh +d2luZ3Mvdm1sRHJhd2luZzIudm1sUEsBAi0AFAAGAAgAAAAhAPUKwQ/HZgAAAA4BABEAAAAAAAAA +AAAAAAAASz4AAHhsL3ZiYVByb2plY3QuYmluUEsBAi0AFAAGAAgAAAAhAJSy2dgDAQAA9QIAACMA +AAAAAAAAAAAAAAAAQaUAAHhsL3dvcmtzaGVldHMvX3JlbHMvc2hlZXQxLnhtbC5yZWxzUEsBAi0A +FAAGAAgAAAAhANS1WSoLAQAAJQUAACMAAAAAAAAAAAAAAAAAhaYAAHhsL3dvcmtzaGVldHMvX3Jl +bHMvc2hlZXQyLnhtbC5yZWxzUEsBAi0AFAAGAAgAAAAhAA5E9N+8AAAAJQEAACMAAAAAAAAAAAAA +AAAA0acAAHhsL2RyYXdpbmdzL19yZWxzL2RyYXdpbmcxLnhtbC5yZWxzUEsBAi0AFAAGAAgAAAAh +AOKvn3S5AQAANBUAACcAAAAAAAAAAAAAAAAAzqgAAHhsL3ByaW50ZXJTZXR0aW5ncy9wcmludGVy +U2V0dGluZ3MxLmJpblBLAQItABQABgAIAAAAIQBH8JgGnQAAALAAAAAaAAAAAAAAAAAAAAAAAMyq +AAB4bC9jdHJsUHJvcHMvY3RybFByb3AxLnhtbFBLAQItABQABgAIAAAAIQBH8JgGnQAAALAAAAAa +AAAAAAAAAAAAAAAAAKGrAAB4bC9jdHJsUHJvcHMvY3RybFByb3AyLnhtbFBLAQItABQABgAIAAAA +IQBH8JgGnQAAALAAAAAaAAAAAAAAAAAAAAAAAHasAAB4bC9jdHJsUHJvcHMvY3RybFByb3AzLnht +bFBLAQItABQABgAIAAAAIQBH8JgGnQAAALAAAAAaAAAAAAAAAAAAAAAAAEutAAB4bC9jdHJsUHJv +cHMvY3RybFByb3A0LnhtbFBLAQItABQABgAIAAAAIQBH8JgGnQAAALAAAAAaAAAAAAAAAAAAAAAA +ACCuAAB4bC9jdHJsUHJvcHMvY3RybFByb3A1LnhtbFBLAQItABQABgAIAAAAIQBH8JgGnQAAALAA +AAAaAAAAAAAAAAAAAAAAAPWuAAB4bC9jdHJsUHJvcHMvY3RybFByb3A2LnhtbFBLAQItABQABgAI +AAAAIQBH8JgGnQAAALAAAAAaAAAAAAAAAAAAAAAAAMqvAAB4bC9jdHJsUHJvcHMvY3RybFByb3A3 +LnhtbFBLAQItABQABgAIAAAAIQCtMdRdpAAAANoAAAAVAAAAAAAAAAAAAAAAAJ+wAAB4bC9wZXJz +b25zL3BlcnNvbi54bWxQSwECLQAUAAYACAAAACEA4h0v0EsBAABtAgAAEQAAAAAAAAAAAAAAAAB2 +sQAAZG9jUHJvcHMvY29yZS54bWxQSwECLQAUAAYACAAAACEAobepTpsBAABGAwAAEAAAAAAAAAAA +AAAAAAD4swAAZG9jUHJvcHMvYXBwLnhtbFBLBQYAAAAAHQAdAOgHAADJtgAAAAA= +""" + + +def _macro_write_embedded_runner_template(path: Path) -> Path: + """Write the embedded macro runner template to *path* and return it.""" + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(base64.b64decode(EMBEDDED_RUNNER_TEMPLATE_B64)) + return path + + +def _xml_text(value: Any) -> str: + """Escape a value for XML text nodes while preserving valid newlines. + + DMN labels/descriptions can contain characters that are legal in JSON or + copied text but illegal in XML 1.0. Excel repairs the whole worksheet when + such a character is written to sheet XML, so strip those characters before + escaping. + """ + if value is None: + return "" + text = str(value) + text = "".join( + ch + for ch in text + if ch in "\t\n\r" + or "\u0020" <= ch <= "\uD7FF" + or "\uE000" <= ch <= "\uFFFD" + or "\U00010000" <= ch <= "\U0010FFFF" + ) + return html.escape(text, quote=False) + + +def _macro_cell_ref(row: int, col: int) -> str: + return f"{excel_col_name(col)}{row}" + + +def _macro_cell_xml(row: int, col: int, value: Any, style: Optional[int] = None) -> str: + """Return one SpreadsheetML cell using inline strings to avoid shared-string rewrites.""" + if value is None or value == "": + if style is None: + return "" + return f'' + attr_s = f' s="{style}"' if style is not None else "" + if isinstance(value, bool): + return f'{1 if value else 0}' + if isinstance(value, (int, float)) and not isinstance(value, bool): + return f'{value}' + return ( + f'' + f'{_xml_text(value)}' + ) + + +def _macro_row_xml( + row_num: int, + values: Sequence[Any], + style_by_col: Optional[Dict[int, int]] = None, + height: Optional[str] = None, +) -> str: + style_by_col = style_by_col or {} + cells = [_macro_cell_xml(row_num, i, value, style_by_col.get(i)) for i, value in enumerate(values, start=1)] + ht = f' ht="{height}" customHeight="1"' if height else "" + return ( + f'' + + "".join(cells) + + "" + ) + + +def _macro_build_outcome_names(expected: Any) -> str: + if not expected: + return "" + names: List[str] = [] + for part in str(expected).split(","): + if "=" in part: + name = part.split("=", 1)[0].strip() + if name and name not in names: + names.append(name) + return ",".join(names) + + +def _macro_build_variable_types(request_body: JsonObj) -> str: + variables = (request_body or {}).get("variables", {}) if isinstance(request_body, dict) else {} + parts: List[str] = [] + for name, spec in variables.items(): + if isinstance(spec, dict) and spec.get("type"): + parts.append(f"{name}:{spec.get('type')}") + return ";".join(parts) + + +def _macro_build_controls_xml(n: int, run_col_zero_based: int) -> str: + """Build the x14 form-control list for the Tests sheet. + + The returned fragment intentionally does NOT include the outer + mc:AlternateContent wrapper. Excel expects the worksheet tail to be: + + + ... + + Putting directly after pageMargins makes Excel repair/replace + sheet2.xml on some workbooks. + """ + parts = [""] + for i in range(n): + shape_id = 2049 + i + rid = 3 + i + row0 = 4 + i # zero-based row for Excel row 5+i + parts.append( + f'' + f'' + f'' + f'{run_col_zero_based}' + f'0{row0}63500' + f'{run_col_zero_based + 1}6350' + f'{row0}368300' + f'' + ) + parts.append("") + return "".join(parts) + + +def _macro_build_vml(n: int, run_col_zero_based: int) -> str: + parts = [ + ''' + + + + + + + ''' + ] + for i in range(n): + shape_id = 2049 + i + row0 = 4 + i + margin_top = 69.5 + i * 70 + margin_left = 3060 + parts.append( + f''' + + + +
Run
+
+ + + {run_col_zero_based}, 0, {row0}, 10, {run_col_zero_based + 1}, 1, {row0}, 58 + False + False + {RUN_SELECTED_MACRO} + Center + Center + +
''' + ) + parts.append("
") + return "".join(parts) + + +def _macro_build_drawing(n: int, run_col_zero_based: int) -> str: + parts = [ + ''' +''' + ] + for i in range(n): + shape_id = 2049 + i + row0 = 4 + i + cid = f"{{00000000-0008-0000-0100-{i + 1:012X}}}" + parts.append( + f'''{run_col_zero_based}0{row0}63500{run_col_zero_based + 1}6350{row0}368300Run''' + ) + parts.append("") + return "".join(parts) + + +def _macro_build_sheet2_rels(n: int) -> str: + rel_ns = "http://schemas.openxmlformats.org/package/2006/relationships" + parts = [ + f'\n' + '' + '' + ] + for i in range(n): + rid = 3 + i + cp = 2 + i # ctrlProp1 belongs to the Dashboard Run All button. + parts.append( + f'' + ) + parts.append("") + return "".join(parts) + + +def _macro_update_content_types(xml_bytes: bytes, n: int) -> bytes: + root = ET.fromstring(xml_bytes) + ns = "{http://schemas.openxmlformats.org/package/2006/content-types}" + existing = {el.attrib.get("PartName") for el in root.findall(f"{ns}Override")} + required = [ + ("/xl/worksheets/sheet1.xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"), + ("/xl/worksheets/sheet2.xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"), + ("/xl/drawings/drawing2.xml", "application/vnd.openxmlformats-officedocument.drawing+xml"), + ("/xl/vbaProject.bin", "application/vnd.ms-office.vbaProject"), + ] + for cp in range(1, n + 2): + required.append((f"/xl/ctrlProps/ctrlProp{cp}.xml", "application/vnd.ms-excel.controlproperties+xml")) + for part_name, content_type in required: + if part_name not in existing: + ET.SubElement(root, f"{ns}Override", {"PartName": part_name, "ContentType": content_type}) + return ET.tostring(root, encoding="utf-8", xml_declaration=True) + + +def _macro_update_workbook_xml(xml_bytes: bytes, last_col_letter: str, last_row: int) -> bytes: + txt = xml_bytes.decode("utf-8") + sheets_xml = '' + txt = re.sub(r".*?", sheets_xml, txt, flags=re.S) + defined = ( + f'' + ) + if re.search(r".*?", txt, flags=re.S): + txt = re.sub(r".*?", defined, txt, flags=re.S) + elif "", defined + "", 1) + return txt.encode("utf-8") + + +def _macro_update_app_xml(xml_bytes: bytes) -> bytes: + """Best-effort update of document properties; stale app.xml is harmless but confusing.""" + try: + txt = xml_bytes.decode("utf-8") + txt = re.sub(r"\d+", "2", txt, count=1) + txt = re.sub( + r".*?", + 'DashboardTests', + txt, + flags=re.S, + ) + return txt.encode("utf-8") + except Exception: + return xml_bytes + + +def _macro_build_dashboard_xml(test_cases: List[JsonObj], analysis: JsonObj, postman_path: Optional[Path] = None) -> str: + '''Build a dashboard that keeps the macro status cells intact and restores + the original per-DMN decision summary table. + + The VBA runner writes totals to B4:B7 and D5:E7, so those cells must stay + reserved. The generated model metadata and the per-partial-DMN table are + placed below them to avoid breaking Run All / UpdateDashboard. + ''' + counts = Counter(tc.get("decisionId", "UNKNOWN") for tc in test_cases) + decisions = analysis.get("decisions") or {} + metadata = analysis.get("metadata") or {} + uncovered = analysis.get("uncoveredConditions", []) or [] + + def decision_sort_key(item: Tuple[str, Any]) -> Tuple[int, str]: + preferred = [ + "BehaalbareHoogteSubsidie", + "BerekenBasisHoogteSubsidie", + "BerekenBeschikbaarSubsidiePlafond", + "SubsidieConstantenThuisbatterij", + "jaarGebondenBudget", + ] + key = item[0] + try: + return (preferred.index(key), key) + except ValueError: + return (len(preferred), key) + + rows: List[str] = [] + rows.append(_macro_row_xml(1, ["DMN MC/DC Test Generation Summary", None, None, None, None, None, None, None], {1: 2}, height="23.5")) + rows.append(_macro_row_xml( + 2, + [ + "Boundary-focused MC/DC cases generated per DMN decision table. Enable macros, then use Run All Tests or row buttons for real POST execution.", + None, None, None, None, None, None, None, + ], + {1: 3}, + )) + + # Keep these cells compatible with the embedded VBA UpdateDashboard macro. + rows.append(_macro_row_xml(4, ["Total", len(test_cases), None, "RUN ALL TESTS", None, None, "Result", "Count"], {1: 1, 2: 1, 4: 1, 5: 1, 6: 1, 7: 1, 8: 1})) + rows.append(_macro_row_xml(5, ["Passed", 0, None, None, None, None, "PASS", 0], {1: 1, 2: 1, 7: 1, 8: 1})) + rows.append(_macro_row_xml(6, ["Failed", 0, None, None, None, None, "FAIL", 0], {1: 1, 2: 1, 7: 1, 8: 1})) + rows.append(_macro_row_xml(7, ["Not run", len(test_cases), None, "OPEN TESTS SHEET", None, None, "NOT RUN", len(test_cases)], {1: 1, 2: 1, 4: 1, 5: 1, 6: 1, 7: 1, 8: 1})) + + postman_hint = postman_path.name if postman_path else "" + rows.append(_macro_row_xml(9, ["Metric", "Value", None, "Newman command", f"newman run \"{postman_hint}\"", None, None, None], {1: 1, 2: 1, 4: 1})) + metric_rows = [ + ("Source DMN", metadata.get("sourceDmn", "")), + ("Algorithm", metadata.get("algorithm", "")), + ("Decision count", metadata.get("decisionCount", len(decisions))), + ("Selected test cases", len(test_cases)), + ("Uncovered conditions", len(uncovered)), + ] + for row_num, (label, value) in enumerate(metric_rows, start=10): + rows.append(_macro_row_xml(row_num, [label, value, None, None, None, None, None, None], {1: 1})) + + decision_start = 17 + rows.append(_macro_row_xml(decision_start, ["Decision ID", "Decision name", "Hit policy", "Candidate count", "Selected cases", None, None, None], {1: 5, 2: 5, 3: 5, 4: 5, 5: 5})) + current_row = decision_start + 1 + for decision_id, details in sorted(decisions.items(), key=decision_sort_key): + rows.append(_macro_row_xml( + current_row, + [ + decision_id, + details.get("decisionName", ""), + details.get("hitPolicy", ""), + details.get("candidateCountEvaluated", ""), + counts.get(decision_id, details.get("selectedTestCaseCount", 0)), + None, None, None, + ], + {}, + height="32", + )) + current_row += 1 + + if current_row == decision_start + 1: + rows.append(_macro_row_xml(current_row, ["No decision details available", None, None, None, len(test_cases), None, None, None], {})) + current_row += 1 + + last_row = max(current_row - 1, 17) + cols_xml = "".join([ + '', + '', + '', + '', + '', + '', + '', + ]) + merge_xml = ( + '' + '' + '' + '' + '' + '' + '' + ) + auto_filter_xml = f'' if last_row >= decision_start else "" + return f''' +{cols_xml}{''.join(rows)}{auto_filter_xml}{merge_xml}30306050''' + + +def _macro_build_dashboard_vml() -> str: + return f''' + + + + + + + + + + +
Run All Tests
+
+ + 3, 0, 3, 0, 6, 0, 5, 0 + False + False + {RUN_ALL_MACRO} + Center + Center + +
''' + + +def _macro_build_dashboard_drawing() -> str: + shape_id = 1025 + cid = "{00000000-0008-0000-0000-000000000001}" + return f''' +30306050Run All Tests''' + + +def _macro_build_tests_xml(headers: List[str], test_rows: List[List[Any]]) -> str: + n_tests = len(test_rows) + last_col = len(headers) + last_col_letter = excel_col_name(last_col) + last_row = 4 + n_tests + run_col_zero_based = headers.index("Run") + + rows = [ + _macro_row_xml(1, ["Tests"] + [None] * (last_col - 1), {1: 4}, height="21"), + _macro_row_xml( + 2, + [ + "Enable macros. Use Run All Tests on the Dashboard or a row Run button; the macro sends POST requests using the Generated JSON Body column.", + ] + + [None] * (last_col - 1), + {1: 3}, + ), + _macro_row_xml(4, headers, {i: 5 for i in range(1, last_col + 1)}), + ] + + variable_headers = set(headers[3:headers.index("Expected")]) + style_data: Dict[int, int] = {} + for i, header in enumerate(headers, start=1): + if header in variable_headers: + style_data[i] = 6 + elif header in {"URL", "Expected", "Actual", "Generated JSON Body", "Coverage Reasons"}: + style_data[i] = 7 + for r_idx, values in enumerate(test_rows, start=5): + rows.append(_macro_row_xml(r_idx, values, style_data, height="70")) + + col_widths: Dict[int, float] = { + 1: 52, + 2: 10, + 3: 80, + headers.index("Expected") + 1: 32, + headers.index("Actual") + 1: 32, + headers.index("Status") + 1: 12, + headers.index("Run") + 1: 12, + headers.index("Username") + 1: 12, + headers.index("Password") + 1: 22, + headers.index("Outcome Names") + 1: 28, + headers.index("Variable Types") + 1: 44, + headers.index("Generated JSON Body") + 1: 70, + last_col: 60, + } + for col in range(4, headers.index("Expected") + 1): + col_widths.setdefault(col, 18) + for col in range(headers.index("Decision ID") + 1, last_col + 1): + col_widths.setdefault(col, 28) + cols_xml = "".join( + f'' for col, width in sorted(col_widths.items()) + ) + controls_xml = _macro_build_controls_xml(n_tests, run_col_zero_based) + return f''' +{cols_xml}{''.join(rows)}{controls_xml}''' + + +def generate_excel_workbook( + test_cases: List[JsonObj], + analysis: JsonObj, + output_path: Path, + *, + base_url: str = "https://operaton.open-regels.nl", + tenant_id: str = "46", + postman_path: Optional[Path] = None, + runner_template_path: Optional[Path] = None, + username: str = "demo", + password: str = "cqa4fpd2jhz*tph5PVC", +) -> None: + """Create the repaired macro-enabled Excel runner workbook. + + This writes an .xlsm by using the embedded macro runner as the VBA/control + container, then replacing Dashboard and Tests with the generated MC/DC cases. + No external Excel template is required. The Tests sheet XML uses Excel-safe control ordering. Pass runner_template_path only when + you intentionally want to override the embedded runner. + + The macro names are fully qualified so Excel resolves the buttons: + PostmanTestRunner.RunAllTests and PostmanTestRunner.RunSelectedTest. + """ + output_path = Path(output_path) + if output_path.suffix.lower() != ".xlsm": + output_path = output_path.with_suffix(".xlsm") + + template_tmp: Optional[str] = None + if runner_template_path is None: + template_tmp = tempfile.mkdtemp(prefix="mcdc_embedded_runner_") + runner_template_path = _macro_write_embedded_runner_template(Path(template_tmp) / "embedded_runner_template.xlsm") + else: + runner_template_path = Path(runner_template_path) + if not runner_template_path.exists(): + raise FileNotFoundError( + f"Macro-enabled runner template override not found: {runner_template_path}. " + "Omit --excel-runner-template to use the embedded runner." + ) + + with zipfile.ZipFile(runner_template_path) as template_zip: + names = set(template_zip.namelist()) + if "xl/vbaProject.bin" not in names: + raise ValueError(f"Runner template {runner_template_path} does not contain xl/vbaProject.bin macros.") + required_parts = { + "xl/workbook.xml", + "[Content_Types].xml", + "xl/worksheets/sheet1.xml", + "xl/worksheets/sheet2.xml", + "xl/worksheets/_rels/sheet2.xml.rels", + } + missing = sorted(required_parts - names) + if missing: + raise ValueError(f"Runner template {runner_template_path} is missing required parts: {', '.join(missing)}") + + var_names = sorted({ + name + for tc in test_cases + for name in (tc.get("requestBody", {}).get("variables", {}) or {}).keys() + }) + headers = [ + "Name", + "Method", + "URL", + *var_names, + "Expected", + "Actual", + "Status", + "Run", + "Username", + "Password", + "Outcome Names", + "Variable Types", + "Generated JSON Body", + "Decision ID", + "Selected Rule ID", + "Selected Rule Index", + "Coverage Reasons", + ] + + def endpoint_for(decision_id: str) -> str: + return postman_url_for_decision(base_url, decision_id, tenant_id).get("raw", "") + + test_rows: List[List[Any]] = [] + for tc in test_cases: + variables = tc.get("requestBody", {}).get("variables", {}) or {} + coverage = tc.get("coverage", {}) or {} + decision_id = str(tc.get("decisionId", "")) + request_body = tc.get("requestBody", {}) or {} + row = [ + tc.get("name", ""), + "POST", + endpoint_for(decision_id), + ] + for var in var_names: + spec = variables.get(var) + row.append(spec.get("value") if isinstance(spec, dict) else None) + expected = tc.get("expected", "") + row.extend( + [ + expected, + "", + "NOT RUN", + "Run", + username, + password, + _macro_build_outcome_names(expected), + _macro_build_variable_types(request_body), + json.dumps(request_body, ensure_ascii=False, indent=2), + decision_id, + coverage.get("selectedRuleId", ""), + coverage.get("selectedRuleIndex", ""), + "\n".join(coverage.get("reasons", []) or []), + ] + ) + test_rows.append(row) + + n_tests = len(test_rows) + last_row = 4 + n_tests + last_col_letter = excel_col_name(len(headers)) + run_col_zero_based = headers.index("Run") + + tmp = tempfile.mkdtemp(prefix="mcdc_xlsm_") + try: + with zipfile.ZipFile(runner_template_path) as zin: + zin.extractall(tmp) + + Path(tmp, "xl/worksheets").mkdir(parents=True, exist_ok=True) + Path(tmp, "xl/drawings").mkdir(parents=True, exist_ok=True) + Path(tmp, "xl/worksheets/_rels").mkdir(parents=True, exist_ok=True) + Path(tmp, "xl/ctrlProps").mkdir(parents=True, exist_ok=True) + + Path(tmp, "xl/worksheets/sheet1.xml").write_text(_macro_build_dashboard_xml(test_cases, analysis, postman_path), encoding="utf-8") + Path(tmp, "xl/worksheets/sheet2.xml").write_text(_macro_build_tests_xml(headers, test_rows), encoding="utf-8") + Path(tmp, "xl/drawings/vmlDrawing1.vml").write_text(_macro_build_dashboard_vml(), encoding="utf-8") + Path(tmp, "xl/drawings/drawing1.xml").write_text(_macro_build_dashboard_drawing(), encoding="utf-8") + Path(tmp, "xl/drawings/vmlDrawing2.vml").write_text(_macro_build_vml(n_tests, run_col_zero_based), encoding="utf-8") + Path(tmp, "xl/drawings/drawing2.xml").write_text(_macro_build_drawing(n_tests, run_col_zero_based), encoding="utf-8") + Path(tmp, "xl/worksheets/_rels/sheet2.xml.rels").write_text(_macro_build_sheet2_rels(n_tests), encoding="utf-8") + + ctrl_xml = ( + '\n' + '' + ) + for cp in range(2, n_tests + 2): + Path(tmp, "xl/ctrlProps", f"ctrlProp{cp}.xml").write_text(ctrl_xml, encoding="utf-8") + + content_types_path = Path(tmp, "[Content_Types].xml") + content_types_path.write_bytes(_macro_update_content_types(content_types_path.read_bytes(), n_tests)) + workbook_path = Path(tmp, "xl/workbook.xml") + workbook_path.write_bytes(_macro_update_workbook_xml(workbook_path.read_bytes(), last_col_letter, last_row)) + app_path = Path(tmp, "docProps/app.xml") + if app_path.exists(): + app_path.write_bytes(_macro_update_app_xml(app_path.read_bytes())) + + output_path.parent.mkdir(parents=True, exist_ok=True) + if output_path.exists(): + output_path.unlink() + with zipfile.ZipFile(output_path, "w", compression=zipfile.ZIP_DEFLATED) as zout: + for root_dir, _dirs, files in os.walk(tmp): + for filename in files: + full = os.path.join(root_dir, filename) + arc = os.path.relpath(full, tmp).replace(os.sep, "/") + zout.write(full, arc) + finally: + shutil.rmtree(tmp) + if template_tmp is not None: + shutil.rmtree(template_tmp) + + + +# --------------------------------------------------------------------------- +# V4 Excel dashboard overrides: decision pie chart + dynamic per-decision run counts +# --------------------------------------------------------------------------- + + +def _macro_formula_cell_xml( + row: int, + col: int, + formula: str, + style: Optional[int] = None, + cached_value: Optional[Any] = None, +) -> str: + """Return a formula cell with an optional cached value for first-open display.""" + attr_s = f' s="{style}"' if style is not None else "" + formula_text = html.escape(formula.lstrip("="), quote=False) + value_xml = "" + if cached_value is not None and cached_value != "": + if isinstance(cached_value, bool): + value_xml = f"{1 if cached_value else 0}" + elif isinstance(cached_value, (int, float)) and not isinstance(cached_value, bool): + value_xml = f"{cached_value}" + else: + value_xml = f"{_xml_text(cached_value)}" + return f'{formula_text}{value_xml}' + + +def _macro_row_from_cells_xml( + row_num: int, + cells_xml: Sequence[str], + spans_end: int, + height: Optional[str] = None, +) -> str: + ht = f' ht="{height}" customHeight="1"' if height else "" + return ( + f'' + + "".join(cells_xml) + + "" + ) + + +def _macro_update_content_types(xml_bytes: bytes, n: int) -> bytes: + root = ET.fromstring(xml_bytes) + ns = "{http://schemas.openxmlformats.org/package/2006/content-types}" + existing = {el.attrib.get("PartName") for el in root.findall(f"{ns}Override")} + required = [ + ("/xl/worksheets/sheet1.xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"), + ("/xl/worksheets/sheet2.xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"), + ("/xl/drawings/drawing1.xml", "application/vnd.openxmlformats-officedocument.drawing+xml"), + ("/xl/drawings/drawing2.xml", "application/vnd.openxmlformats-officedocument.drawing+xml"), + ("/xl/charts/chart1.xml", "application/vnd.openxmlformats-officedocument.drawingml.chart+xml"), + ("/xl/vbaProject.bin", "application/vnd.ms-office.vbaProject"), + ] + for cp in range(1, n + 2): + required.append((f"/xl/ctrlProps/ctrlProp{cp}.xml", "application/vnd.ms-excel.controlproperties+xml")) + for part_name, content_type in required: + if part_name not in existing: + ET.SubElement(root, f"{ns}Override", {"PartName": part_name, "ContentType": content_type}) + return ET.tostring(root, encoding="utf-8", xml_declaration=True) + + +def _macro_update_workbook_xml(xml_bytes: bytes, last_col_letter: str, last_row: int) -> bytes: + txt = xml_bytes.decode("utf-8") + sheets_xml = '' + txt = re.sub(r".*?", sheets_xml, txt, flags=re.S) + defined = ( + f'' + ) + if re.search(r".*?", txt, flags=re.S): + txt = re.sub(r".*?", defined, txt, flags=re.S) + elif "", defined + "", 1) + + if " str: + tag = match.group(0) + tag = re.sub(r'\s+(calcMode|fullCalcOnLoad|forceFullCalc)="[^"]*"', '', tag) + return tag[:-2] + ' calcMode="auto" fullCalcOnLoad="1" forceFullCalc="1"/>' if tag.endswith('/>') else tag + txt = re.sub(r"]*/>", _calc_repl, txt, count=1) + return txt.encode("utf-8") + + +def _macro_decision_sort_key(item: Tuple[str, Any]) -> Tuple[int, str]: + preferred = [ + "BehaalbareHoogteSubsidie", + "BerekenBasisHoogteSubsidie", + "BerekenBeschikbaarSubsidiePlafond", + "SubsidieConstantenThuisbatterij", + "jaarGebondenBudget", + ] + key = item[0] + try: + return (preferred.index(key), key) + except ValueError: + return (len(preferred), key) + + +def _macro_build_dashboard_xml_v4( + test_cases: List[JsonObj], + analysis: JsonObj, + headers: List[str], + postman_path: Optional[Path] = None, +) -> str: + counts = Counter(tc.get("decisionId", "UNKNOWN") for tc in test_cases) + decisions = analysis.get("decisions") or {} + metadata = analysis.get("metadata") or {} + uncovered = analysis.get("uncoveredConditions", []) or [] + n_tests = len(test_cases) + tests_last_row = 4 + n_tests + status_col = excel_col_name(headers.index("Status") + 1) + decision_col = excel_col_name(headers.index("Decision ID") + 1) + status_range = f"Tests!${status_col}$5:${status_col}${tests_last_row}" + decision_range = f"Tests!${decision_col}$5:${decision_col}${tests_last_row}" + name_range = f"Tests!$A$5:$A${tests_last_row}" + + rows: List[str] = [] + spans_end = 9 + rows.append(_macro_row_xml(1, ["DMN MC/DC Test Generation Summary"] + [None] * (spans_end - 1), {1: 2}, height="23.5")) + rows.append(_macro_row_xml( + 2, + ["Boundary-focused MC/DC cases generated per DMN decision table. Enable macros, then use Run All Tests or row buttons for real POST execution."] + [None] * (spans_end - 1), + {1: 3}, + )) + + # Preserve the runner's original dashboard addresses: B4:B7 and D5:E7. + rows.append(_macro_row_from_cells_xml(4, [ + _macro_cell_xml(4, 1, "Total", 1), + _macro_formula_cell_xml(4, 2, f"COUNTA({name_range})", 1, n_tests), + _macro_cell_xml(4, 4, "Result", 1), + _macro_cell_xml(4, 5, "Count", 1), + ], spans_end)) + rows.append(_macro_row_from_cells_xml(5, [ + _macro_cell_xml(5, 1, "Passed", 1), + _macro_formula_cell_xml(5, 2, f"COUNTIF({status_range},\"PASS\")", 1, 0), + _macro_cell_xml(5, 4, "PASS", 1), + _macro_formula_cell_xml(5, 5, "B5", 1, 0), + ], spans_end)) + rows.append(_macro_row_from_cells_xml(6, [ + _macro_cell_xml(6, 1, "Failed/Error", 1), + _macro_formula_cell_xml(6, 2, f"COUNTIFS({status_range},\"<>PASS\",{status_range},\"<>NOT RUN\",{status_range},\"<>\")", 1, 0), + _macro_cell_xml(6, 4, "FAIL", 1), + _macro_formula_cell_xml(6, 5, "B6", 1, 0), + ], spans_end)) + rows.append(_macro_row_from_cells_xml(7, [ + _macro_cell_xml(7, 1, "Not run", 1), + _macro_formula_cell_xml(7, 2, f"COUNTIF({status_range},\"NOT RUN\")", 1, n_tests), + _macro_cell_xml(7, 4, "NOT RUN", 1), + _macro_formula_cell_xml(7, 5, "B7", 1, n_tests), + ], spans_end)) + + rows.append(_macro_row_xml(9, ["RUN ALL TESTS", None, None, "OPEN TESTS SHEET", None, None, None, None, None], {1: 1, 2: 1, 4: 1, 5: 1}, height="28")) + postman_hint = postman_path.name if postman_path else "" + rows.append(_macro_row_xml(11, ["Metric", "Value", None, "Newman command", f"newman run \"{postman_hint}\"", None, None, None, None], {1: 1, 2: 1, 4: 1}, height="18")) + metric_rows = [ + ("Source DMN", metadata.get("sourceDmn", "")), + ("Algorithm", metadata.get("algorithm", "")), + ("Decision count", metadata.get("decisionCount", len(decisions))), + ("Selected test cases", n_tests), + ("Uncovered conditions", len(uncovered)), + ] + for row_num, (label, value) in enumerate(metric_rows, start=12): + if label == "Selected test cases": + rows.append(_macro_row_from_cells_xml(row_num, [ + _macro_cell_xml(row_num, 1, label, 1), + _macro_formula_cell_xml(row_num, 2, "B4", None, n_tests), + ], spans_end)) + else: + rows.append(_macro_row_xml(row_num, [label, value, None, None, None, None, None, None, None], {1: 1})) + + decision_start = 20 + decision_headers = ["Decision ID", "Decision name", "Hit policy", "Candidate count", "Total cases", "Run", "Passed", "Failed/Error", "Not run"] + rows.append(_macro_row_xml(decision_start, decision_headers, {i: 5 for i in range(1, len(decision_headers) + 1)})) + current_row = decision_start + 1 + for decision_id, details in sorted(decisions.items(), key=_macro_decision_sort_key): + total_cache = counts.get(decision_id, details.get("selectedTestCaseCount", 0)) + row_ref = current_row + rows.append(_macro_row_from_cells_xml(row_ref, [ + _macro_cell_xml(row_ref, 1, decision_id), + _macro_cell_xml(row_ref, 2, details.get("decisionName", "")), + _macro_cell_xml(row_ref, 3, details.get("hitPolicy", "")), + _macro_cell_xml(row_ref, 4, details.get("candidateCountEvaluated", "")), + _macro_formula_cell_xml(row_ref, 5, f"COUNTIF({decision_range},A{row_ref})", None, total_cache), + _macro_formula_cell_xml(row_ref, 6, f"G{row_ref}+H{row_ref}", None, 0), + _macro_formula_cell_xml(row_ref, 7, f"COUNTIFS({decision_range},A{row_ref},{status_range},\"PASS\")", None, 0), + _macro_formula_cell_xml(row_ref, 8, f"COUNTIFS({decision_range},A{row_ref},{status_range},\"<>PASS\",{status_range},\"<>NOT RUN\",{status_range},\"<>\")", None, 0), + _macro_formula_cell_xml(row_ref, 9, f"COUNTIFS({decision_range},A{row_ref},{status_range},\"NOT RUN\")", None, total_cache), + ], spans_end, height="32")) + current_row += 1 + + if current_row == decision_start + 1: + rows.append(_macro_row_xml(current_row, ["No decision details available", None, None, None, n_tests, 0, 0, 0, n_tests], {})) + current_row += 1 + + last_row = max(current_row - 1, decision_start) + cols_xml = "".join([ + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + ]) + merge_xml = ( + '' + '' + '' + '' + '' + '' + ) + auto_filter_xml = f'' if last_row >= decision_start else "" + return f''' +{cols_xml}{''.join(rows)}{auto_filter_xml}{merge_xml}008014762501038100''' + + +def _macro_build_dashboard_vml() -> str: + return f''' + + + + + + + + + + +
Run All Tests
+
+ + 0, 0, 8, 0, 1, 75, 10, 6 + False + False + {RUN_ALL_MACRO} + Center + Center + +
''' + + +def _macro_build_dashboard_drawing() -> str: + shape_id = 1025 + cid = "{00000000-0008-0000-0000-000000000001}" + chart_cid = "{00000000-0008-0000-0000-000000000002}" + return f''' +008014762501038100Run All Tests6030150190''' + + +def _macro_build_decision_pie_chart_xml(test_cases: List[JsonObj], analysis: JsonObj, decision_start: int = 20) -> str: + counts = Counter(tc.get("decisionId", "UNKNOWN") for tc in test_cases) + decisions = analysis.get("decisions") or {} + ordered = list(sorted(decisions.items(), key=_macro_decision_sort_key)) + if not ordered: + ordered = [("All tests", {"selectedTestCaseCount": len(test_cases)})] + data_start = decision_start + 1 + data_end = data_start + len(ordered) - 1 + labels = [str(decision_id) for decision_id, _details in ordered] + values = [counts.get(decision_id, details.get("selectedTestCaseCount", 0)) for decision_id, details in ordered] + pt_count = len(labels) + str_pts = "".join(f'{_xml_text(label)}' for idx, label in enumerate(labels)) + num_pts = "".join(f'{value}' for idx, value in enumerate(values)) + return f''' +MC/DC cases by decisionDashboard!$E$20Total casesDashboard!$A${data_start}:$A${data_end}{str_pts}Dashboard!$E${data_start}:$E${data_end}General{num_pts}''' + + +def generate_excel_workbook( + test_cases: List[JsonObj], + analysis: JsonObj, + output_path: Path, + *, + base_url: str = "https://operaton.open-regels.nl", + tenant_id: str = "46", + postman_path: Optional[Path] = None, + runner_template_path: Optional[Path] = None, + username: str = "demo", + password: str = "cqa4fpd2jhz*tph5PVC", +) -> None: + """Create the macro-enabled Excel runner with a decision pie chart and dynamic status counts.""" + output_path = Path(output_path) + if output_path.suffix.lower() != ".xlsm": + output_path = output_path.with_suffix(".xlsm") + + template_tmp: Optional[str] = None + if runner_template_path is None: + template_tmp = tempfile.mkdtemp(prefix="mcdc_embedded_runner_") + runner_template_path = _macro_write_embedded_runner_template(Path(template_tmp) / "embedded_runner_template.xlsm") + else: + runner_template_path = Path(runner_template_path) + if not runner_template_path.exists(): + raise FileNotFoundError( + f"Macro-enabled runner template override not found: {runner_template_path}. " + "Omit --excel-runner-template to use the embedded runner." + ) + + with zipfile.ZipFile(runner_template_path) as template_zip: + names = set(template_zip.namelist()) + if "xl/vbaProject.bin" not in names: + raise ValueError(f"Runner template {runner_template_path} does not contain xl/vbaProject.bin macros.") + required_parts = { + "xl/workbook.xml", + "[Content_Types].xml", + "xl/worksheets/sheet1.xml", + "xl/worksheets/sheet2.xml", + "xl/worksheets/_rels/sheet2.xml.rels", + } + missing = sorted(required_parts - names) + if missing: + raise ValueError(f"Runner template {runner_template_path} is missing required parts: {', '.join(missing)}") + + var_names = sorted({ + name + for tc in test_cases + for name in (tc.get("requestBody", {}).get("variables", {}) or {}).keys() + }) + headers = [ + "Name", "Method", "URL", *var_names, "Expected", "Actual", "Status", "Run", "Username", "Password", + "Outcome Names", "Variable Types", "Generated JSON Body", "Decision ID", "Selected Rule ID", "Selected Rule Index", "Coverage Reasons", + ] + + def endpoint_for(decision_id: str) -> str: + return postman_url_for_decision(base_url, decision_id, tenant_id).get("raw", "") + + test_rows: List[List[Any]] = [] + for tc in test_cases: + variables = tc.get("requestBody", {}).get("variables", {}) or {} + coverage = tc.get("coverage", {}) or {} + decision_id = str(tc.get("decisionId", "")) + request_body = tc.get("requestBody", {}) or {} + row = [tc.get("name", ""), "POST", endpoint_for(decision_id)] + for var in var_names: + spec = variables.get(var) + row.append(spec.get("value") if isinstance(spec, dict) else None) + expected = tc.get("expected", "") + row.extend([ + expected, + "", + "NOT RUN", + "Run", + username, + password, + _macro_build_outcome_names(expected), + _macro_build_variable_types(request_body), + json.dumps(request_body, ensure_ascii=False, indent=2), + decision_id, + coverage.get("selectedRuleId", ""), + coverage.get("selectedRuleIndex", ""), + "\n".join(coverage.get("reasons", []) or []), + ]) + test_rows.append(row) + + n_tests = len(test_rows) + last_row = 4 + n_tests + last_col_letter = excel_col_name(len(headers)) + run_col_zero_based = headers.index("Run") + + tmp = tempfile.mkdtemp(prefix="mcdc_xlsm_") + try: + with zipfile.ZipFile(runner_template_path) as zin: + zin.extractall(tmp) + + Path(tmp, "xl/worksheets").mkdir(parents=True, exist_ok=True) + Path(tmp, "xl/drawings").mkdir(parents=True, exist_ok=True) + Path(tmp, "xl/drawings/_rels").mkdir(parents=True, exist_ok=True) + Path(tmp, "xl/charts").mkdir(parents=True, exist_ok=True) + Path(tmp, "xl/worksheets/_rels").mkdir(parents=True, exist_ok=True) + Path(tmp, "xl/ctrlProps").mkdir(parents=True, exist_ok=True) + + Path(tmp, "xl/worksheets/sheet1.xml").write_text(_macro_build_dashboard_xml_v4(test_cases, analysis, headers, postman_path), encoding="utf-8") + Path(tmp, "xl/worksheets/sheet2.xml").write_text(_macro_build_tests_xml(headers, test_rows), encoding="utf-8") + Path(tmp, "xl/drawings/vmlDrawing1.vml").write_text(_macro_build_dashboard_vml(), encoding="utf-8") + Path(tmp, "xl/drawings/drawing1.xml").write_text(_macro_build_dashboard_drawing(), encoding="utf-8") + Path(tmp, "xl/drawings/_rels/drawing1.xml.rels").write_text( + '\n' + '' + '' + '', + encoding="utf-8", + ) + Path(tmp, "xl/charts/chart1.xml").write_text(_macro_build_decision_pie_chart_xml(test_cases, analysis), encoding="utf-8") + Path(tmp, "xl/drawings/vmlDrawing2.vml").write_text(_macro_build_vml(n_tests, run_col_zero_based), encoding="utf-8") + Path(tmp, "xl/drawings/drawing2.xml").write_text(_macro_build_drawing(n_tests, run_col_zero_based), encoding="utf-8") + Path(tmp, "xl/worksheets/_rels/sheet2.xml.rels").write_text(_macro_build_sheet2_rels(n_tests), encoding="utf-8") + + ctrl_xml = ( + '\n' + '' + ) + for cp in range(2, n_tests + 2): + Path(tmp, "xl/ctrlProps", f"ctrlProp{cp}.xml").write_text(ctrl_xml, encoding="utf-8") + + content_types_path = Path(tmp, "[Content_Types].xml") + content_types_path.write_bytes(_macro_update_content_types(content_types_path.read_bytes(), n_tests)) + workbook_path = Path(tmp, "xl/workbook.xml") + workbook_path.write_bytes(_macro_update_workbook_xml(workbook_path.read_bytes(), last_col_letter, last_row)) + app_path = Path(tmp, "docProps/app.xml") + if app_path.exists(): + app_path.write_bytes(_macro_update_app_xml(app_path.read_bytes())) + + output_path.parent.mkdir(parents=True, exist_ok=True) + if output_path.exists(): + output_path.unlink() + with zipfile.ZipFile(output_path, "w", compression=zipfile.ZIP_DEFLATED) as zout: + for root_dir, _dirs, files in os.walk(tmp): + for filename in files: + full = os.path.join(root_dir, filename) + arc = os.path.relpath(full, tmp).replace(os.sep, "/") + zout.write(full, arc) + finally: + shutil.rmtree(tmp) + if template_tmp is not None: + shutil.rmtree(template_tmp) + + +# --------------------------------------------------------------------------- +# End-to-end CLI +# --------------------------------------------------------------------------- + + +def default_prefix_from_dmn(dmn_path: Path) -> str: + stem = dmn_path.stem + return re.sub(r"[^A-Za-z0-9_.-]+", "_", stem).strip("_") or "dmn" + + +def main(argv: Optional[List[str]] = None) -> int: + parser = argparse.ArgumentParser( + description=( + "Generate MC/DC + boundary DMN test cases as JSON, an Excel workbook, " + "and a Postman collection with the generated cases grouped as examples." + ) + ) + parser.add_argument("dmn", type=Path, help="Path to the DMN XML file") + parser.add_argument("--postman-template", type=Path, help="Optional base Postman collection with one decision-evaluate request per table") + parser.add_argument("--out-dir", type=Path, default=Path("."), help="Directory for generated files") + parser.add_argument("--prefix", help="Output filename prefix. Defaults to the DMN filename stem") + parser.add_argument("--base-url", default="https://operaton.open-regels.nl", help="Base URL used when no Postman template exists. Accepts either host root or /engine-rest URL.") + parser.add_argument("--tenant-id", default="46", help="Tenant id used when no Postman template exists") + parser.add_argument("--excel-runner-template", type=Path, help="Optional macro-enabled Excel runner template override. Omit to use the embedded runner.") + parser.add_argument("--excel-username", default="demo", help="Username written to the generated Tests sheet") + parser.add_argument("--excel-password", default="cqa4fpd2jhz*tph5PVC", help="Password written to the generated Tests sheet") + parser.add_argument("--max-candidates-per-decision", type=int, default=100_000, help="Candidate safety cap per decision table") + parser.add_argument("--max-cases-per-decision", type=int, help="Optional cap after MC/DC/boundary selection per decision table") + parser.add_argument("--skip-excel", action="store_true", help="Skip writing the .xlsm workbook") + parser.add_argument("--skip-postman", action="store_true", help="Skip writing the Postman collection") + args = parser.parse_args(argv) + + if not args.dmn.exists(): + raise FileNotFoundError(args.dmn) + if args.postman_template and not args.postman_template.exists(): + raise FileNotFoundError(args.postman_template) + + args.out_dir.mkdir(parents=True, exist_ok=True) + prefix = args.prefix or default_prefix_from_dmn(args.dmn) + json_path = args.out_dir / f"{prefix}-mcdc-test-cases.json" + analysis_path = args.out_dir / f"{prefix}-mcdc-analysis.json" + excel_path = args.out_dir / f"{prefix}-mcdc-test-cases.xlsm" + postman_path = args.out_dir / f"{prefix}.postman_collection.mcdc_examples.json" + + cases, analysis = generate( + args.dmn, + max_candidates_per_decision=args.max_candidates_per_decision, + max_cases_per_decision=args.max_cases_per_decision, + ) + + dump_json(cases, json_path) + dump_json(analysis, analysis_path) + + if not args.skip_excel: + runner_template = args.excel_runner_template + if runner_template is not None and not runner_template.exists(): + candidate = args.dmn.parent / runner_template + if candidate.exists(): + runner_template = candidate + generate_excel_workbook( + cases, + analysis, + excel_path, + base_url=args.base_url, + tenant_id=args.tenant_id, + postman_path=postman_path if not args.skip_postman else None, + runner_template_path=runner_template, + username=args.excel_username, + password=args.excel_password, + ) + + if not args.skip_postman: + collection = generate_postman_collection( + cases, + postman_template=args.postman_template, + base_url=args.base_url, + tenant_id=args.tenant_id, + ) + dump_json(collection, postman_path) + + counts = Counter(tc.get("decisionId", "UNKNOWN") for tc in cases) + print(f"Generated MC/DC test cases: {len(cases)}") + for decision_id, count in sorted(counts.items()): + print(f" {decision_id}: {count}") + print(f"Uncovered conditions: {len(analysis.get('uncoveredConditions', []))}") + print(f"JSON: {json_path}") + print(f"Analysis: {analysis_path}") + if not args.skip_excel: + print(f"Excel: {excel_path}") + if not args.skip_postman: + print(f"Postman: {postman_path}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/organizations/flevoland/thuisbatterij/generatedRepaired/hoogteVergoedingThuisbatterijBijRechtEnBudget-mcdc-analysis.json b/examples/organizations/flevoland/thuisbatterij/generatedRepaired/hoogteVergoedingThuisbatterijBijRechtEnBudget-mcdc-analysis.json new file mode 100644 index 0000000..ab0a2e0 --- /dev/null +++ b/examples/organizations/flevoland/thuisbatterij/generatedRepaired/hoogteVergoedingThuisbatterijBijRechtEnBudget-mcdc-analysis.json @@ -0,0 +1,368 @@ +{ + "metadata": { + "sourceDmn": "hoogteVergoedingThuisbatterijBijRechtEnBudget.dmn", + "algorithm": "Per-decision-table boundary domains + MC/DC-style condition-pair selection", + "note": "Each decision table is tested in direct-table-input mode. Non-boolean DMN entries such as date ranges, comparisons, and string equality tests are treated as atomic boolean predicates for MC/DC pair selection. Boundary/domain representatives are added so all generated input ranges appear in at least one case per table.", + "decisionCount": 5, + "selectedTestCaseCount": 98 + }, + "decisions": { + "BehaalbareHoogteSubsidie": { + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "hitPolicy": "UNIQUE", + "candidateCountEvaluated": 121, + "selectedTestCaseCount": 19, + "inputDomainsUsed": { + "basisHoogteSubsidie": [ + 0, + 1, + 749, + 750, + 751, + 1249, + 1250, + 1251, + 437499, + 437500, + 437501 + ], + "beschikbaarSubsidiePlafond": [ + 0, + 1, + 749, + 750, + 751, + 1249, + 1250, + 1251, + 437499, + 437500, + 437501 + ] + }, + "rules": [ + { + "ruleIndex": 1, + "ruleId": "Rule_Behaalbare_BasisPastBinnenResterendBudget", + "description": "", + "inputEntries": [ + "<= beschikbaarSubsidiePlafond" + ], + "outputEntries": [ + "basisHoogteSubsidie" + ] + }, + { + "ruleIndex": 2, + "ruleId": "Rule_Behaalbare_BeperktDoorResterendBudget", + "description": "", + "inputEntries": [ + "> beschikbaarSubsidiePlafond" + ], + "outputEntries": [ + "beschikbaarSubsidiePlafond" + ] + } + ], + "uncoveredConditions": [] + }, + "BerekenBasisHoogteSubsidie": { + "decisionName": "Bereken Basis Hoogte Subsidie", + "decisionTableId": "DecisionTable_BerekenBasisHoogteSubsidie", + "hitPolicy": "UNIQUE", + "candidateCountEvaluated": 8, + "selectedTestCaseCount": 8, + "inputDomainsUsed": { + "gemaakteKosten": [ + 0, + 2999, + 3000, + 3001, + 4999, + 5000, + 5001, + 10000 + ], + "minimaleNoodzakelijkeKosten": [ + 750 + ], + "subsidieMaximum": [ + 1250 + ], + "subsidieMinimum": [ + 750 + ], + "subsidiePercentage": [ + 0.25 + ] + }, + "rules": [ + { + "ruleIndex": 1, + "ruleId": "Rule_Basis_BovenMinimum", + "description": "", + "inputEntries": [ + ">= minimaleNoodzakelijkeKosten / subsidiePercentage" + ], + "outputEntries": [ + "if subsidiePercentage * gemaakteKosten > subsidieMaximum then subsidieMaximum else if subsidiePercentage * gemaakteKosten < subsidieMinimum then subsidieMinimum else subsidiePercentage * gemaakteKosten" + ] + }, + { + "ruleIndex": 2, + "ruleId": "Rule_Basis_OnderMinimum", + "description": "", + "inputEntries": [ + "< minimaleNoodzakelijkeKosten / subsidiePercentage" + ], + "outputEntries": [ + "0" + ] + } + ], + "uncoveredConditions": [] + }, + "BerekenBeschikbaarSubsidiePlafond": { + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "hitPolicy": "FIRST", + "candidateCountEvaluated": 65340, + "selectedTestCaseCount": 64, + "inputDomainsUsed": { + "aanvraagDatum": [ + "2025-12-31", + "2026-01-11", + "2026-01-12", + "2026-01-13", + "2026-09-29", + "2026-09-30", + "2026-10-01", + "2026-10-02", + "2026-12-30", + "2026-12-31", + "2027-01-01", + "2027-01-02", + "2027-09-29", + "2027-09-30", + "2027-10-01", + "2027-10-02", + "2027-12-30", + "2027-12-31", + "2028-01-01", + "2029-01-01" + ], + "aanvragerType": [ + "eigenaar", + "huurder", + "onbekend" + ], + "plafondEigenaren": [ + 437500, + 875000, + 1000000 + ], + "plafondHuurders": [ + 437500, + 875000, + 1000000 + ], + "reedsGesubsidieerdEigenaren": [ + 0, + 1, + 437499, + 437500, + 437501, + 874999, + 875000, + 875001, + 999999, + 1000000, + 1000001 + ], + "reedsGesubsidieerdHuurders": [ + 0, + 1, + 437499, + 437500, + 437501, + 874999, + 875000, + 875001, + 999999, + 1000000, + 1000001 + ] + }, + "rules": [ + { + "ruleIndex": 1, + "ruleId": "Rule_Platform_2026_Eigenaar_Gescheiden", + "description": "12 januari 2026 t/m 30 september 2026: apart plafond voor eigenaren.", + "inputEntries": [ + "date(aanvraagDatum) >= date(\"2026-01-12\") and date(aanvraagDatum) <= date(\"2026-09-30\")", + "\"eigenaar\"", + "-", + "-" + ], + "outputEntries": [ + "if plafondEigenaren - reedsGesubsidieerdEigenaren > 0 then plafondEigenaren - reedsGesubsidieerdEigenaren else 0" + ] + }, + { + "ruleIndex": 2, + "ruleId": "Rule_Platform_2026_Huurder_Gescheiden", + "description": "12 januari 2026 t/m 30 september 2026: apart plafond voor huurders.", + "inputEntries": [ + "date(aanvraagDatum) >= date(\"2026-01-12\") and date(aanvraagDatum) <= date(\"2026-09-30\")", + "\"huurder\"", + "-", + "-" + ], + "outputEntries": [ + "if plafondHuurders - reedsGesubsidieerdHuurders > 0 then plafondHuurders - reedsGesubsidieerdHuurders else 0" + ] + }, + { + "ruleIndex": 3, + "ruleId": "Rule_Platform_2026_Gebundeld", + "description": "Vanaf 1 oktober 2026: wat over is van beide plafonds samen, geen onderscheid. Voor 2026 is het hele budget opgesplitst naar huurder en verhuurders.", + "inputEntries": [ + "date(aanvraagDatum) >= date(\"2026-10-01\") and date(aanvraagDatum) <= date(\"2026-12-31\")", + "-", + "-", + "-" + ], + "outputEntries": [ + "if plafondEigenaren + plafondHuurders - reedsGesubsidieerdEigenaren - reedsGesubsidieerdHuurders > 0 then plafondEigenaren + plafondHuurders - reedsGesubsidieerdEigenaren - reedsGesubsidieerdHuurders else 0" + ] + }, + { + "ruleIndex": 4, + "ruleId": "Rule_Platform_2027_Eigenaar_Gescheiden", + "description": "1 januari 2027 t/m 30 september 2027: apart plafond voor eigenaren. NB: in deze berekening wordt nog geen 2026 overgebleven budget overgeheveld.", + "inputEntries": [ + "date(\"2027-01-01\") <= date(aanvraagDatum) and date(aanvraagDatum) <= date(\"2027-09-30\")", + "\"eigenaar\"", + "-", + "-" + ], + "outputEntries": [ + "if plafondEigenaren - reedsGesubsidieerdEigenaren > 0 then plafondEigenaren - reedsGesubsidieerdEigenaren else 0" + ] + }, + { + "ruleIndex": 5, + "ruleId": "Rule_Platform_2027_Huurder_Gescheiden", + "description": "1 januari 2027 t/m 30 september 2027: apart plafond voor huurders.", + "inputEntries": [ + "date(aanvraagDatum) >= date(\"2027-01-01\") and date(aanvraagDatum) <= date(\"2027-09-30\")", + "\"huurder\"", + "-", + "-" + ], + "outputEntries": [ + "if plafondHuurders - reedsGesubsidieerdHuurders > 0 then plafondHuurders - reedsGesubsidieerdHuurders else 0" + ] + }, + { + "ruleIndex": 6, + "ruleId": "Rule_Platform_2027_Gebundeld", + "description": "Vanaf 1 oktober 2027: wat over is van beide plafonds samen, geen onderscheid. NB\": het totale budget voor 2027 is meer dan wat tm september mag worden uitgegeven!", + "inputEntries": [ + "date(aanvraagDatum) >= date(\"2027-10-01\") and date(aanvraagDatum) <= date(\"2027-12-31\")", + "-", + "-", + "-" + ], + "outputEntries": [ + "if 1000000 - reedsGesubsidieerdEigenaren - reedsGesubsidieerdHuurders > 0 then 1000000 - reedsGesubsidieerdEigenaren - reedsGesubsidieerdHuurders else 0" + ] + }, + { + "ruleIndex": 7, + "ruleId": "Rule_Platform_BuitenAanvraagperiodeOfOnbekendType", + "description": "Buiten de gemodelleerde aanvraagperiodes of onbekend type: geen beschikbaar plafond.", + "inputEntries": [ + "-", + "-", + "-", + "-" + ], + "outputEntries": [ + "0" + ] + } + ], + "uncoveredConditions": [] + }, + "SubsidieConstantenThuisbatterij": { + "decisionName": "Subsidie Constanten Thuisbatterij", + "decisionTableId": "DecisionTable_SubsidieConstantenThuisbatterij", + "hitPolicy": "UNIQUE", + "candidateCountEvaluated": 1, + "selectedTestCaseCount": 1, + "inputDomainsUsed": {}, + "rules": [ + { + "ruleIndex": 1, + "ruleId": "Rule_Constanten_Standaard", + "description": "", + "inputEntries": [], + "outputEntries": [ + "0.25", + "750", + "1250", + "750" + ] + } + ], + "uncoveredConditions": [] + }, + "jaarGebondenBudget": { + "decisionName": "Jaar Gebonden Budget", + "decisionTableId": "DecisionTable_1bhjh1w", + "hitPolicy": "UNIQUE", + "candidateCountEvaluated": 6, + "selectedTestCaseCount": 6, + "inputDomainsUsed": { + "aanvraagDatum": [ + "2025-12-31", + "2026-01-01", + "2026-12-31", + "2027-01-01", + "2027-12-31", + "2028-01-01" + ] + }, + "rules": [ + { + "ruleIndex": 1, + "ruleId": "DecisionRule_0nfr6kl", + "description": "", + "inputEntries": [ + "2026" + ], + "outputEntries": [ + "437500", + "437500" + ] + }, + { + "ruleIndex": 2, + "ruleId": "DecisionRule_1e5agpr", + "description": "", + "inputEntries": [ + "2027" + ], + "outputEntries": [ + "437500", + "437500" + ] + } + ], + "uncoveredConditions": [] + } + }, + "uncoveredConditions": [] +} diff --git a/examples/organizations/flevoland/thuisbatterij/generatedRepaired/hoogteVergoedingThuisbatterijBijRechtEnBudget-mcdc-test-cases.json b/examples/organizations/flevoland/thuisbatterij/generatedRepaired/hoogteVergoedingThuisbatterijBijRechtEnBudget-mcdc-test-cases.json new file mode 100644 index 0000000..5f39637 --- /dev/null +++ b/examples/organizations/flevoland/thuisbatterij/generatedRepaired/hoogteVergoedingThuisbatterijBijRechtEnBudget-mcdc-test-cases.json @@ -0,0 +1,3813 @@ +[ + { + "name": "TC_001 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=0.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 0, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BasisPastBinnenResterendBudget", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: basisHoogteSubsidie=0", + "Boundary/domain value: beschikbaarSubsidiePlafond=0", + "Output minimum: hoogteSubsidie=0", + "Output near zero boundary: hoogteSubsidie=0" + ] + } + }, + { + "name": "TC_002 Behaalbare Hoogte Subsidie - MC/DC - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=0.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 0, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 1, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BasisPastBinnenResterendBudget", + "selectedRuleIndex": 1, + "reasons": [ + "MC/DC: rule 2 Rule_Behaalbare_BeperktDoorResterendBudget, condition [basisHoogteSubsidie > beschikbaarSubsidiePlafond] baseline" + ] + } + }, + { + "name": "TC_003 Behaalbare Hoogte Subsidie - MC/DC - Rule_Behaalbare_BeperktDoorResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=0.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 1, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BeperktDoorResterendBudget", + "selectedRuleIndex": 2, + "reasons": [ + "MC/DC: rule 1 Rule_Behaalbare_BasisPastBinnenResterendBudget, condition [basisHoogteSubsidie <= beschikbaarSubsidiePlafond] baseline" + ] + } + }, + { + "name": "TC_004 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=1.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 1, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 1249, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BasisPastBinnenResterendBudget", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: basisHoogteSubsidie=1" + ] + } + }, + { + "name": "TC_005 Behaalbare Hoogte Subsidie - MC/DC - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=1.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 1, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 1, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BasisPastBinnenResterendBudget", + "selectedRuleIndex": 1, + "reasons": [ + "MC/DC: rule 1 Rule_Behaalbare_BasisPastBinnenResterendBudget, condition [basisHoogteSubsidie <= beschikbaarSubsidiePlafond] flipped", + "Boundary/domain value: beschikbaarSubsidiePlafond=1" + ] + } + }, + { + "name": "TC_006 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=1249.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 1249, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 1249, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BasisPastBinnenResterendBudget", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: basisHoogteSubsidie=1249", + "Boundary/domain value: beschikbaarSubsidiePlafond=1249" + ] + } + }, + { + "name": "TC_007 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BeperktDoorResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=749.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 1249, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 749, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BeperktDoorResterendBudget", + "selectedRuleIndex": 2, + "reasons": [ + "Boundary/domain value: beschikbaarSubsidiePlafond=749" + ] + } + }, + { + "name": "TC_008 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BeperktDoorResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=750.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 1249, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 750, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BeperktDoorResterendBudget", + "selectedRuleIndex": 2, + "reasons": [ + "Boundary/domain value: beschikbaarSubsidiePlafond=750" + ] + } + }, + { + "name": "TC_009 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BeperktDoorResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=751.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 1249, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 751, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BeperktDoorResterendBudget", + "selectedRuleIndex": 2, + "reasons": [ + "Boundary/domain value: beschikbaarSubsidiePlafond=751" + ] + } + }, + { + "name": "TC_010 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=1250.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 1250, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 1250, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BasisPastBinnenResterendBudget", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: basisHoogteSubsidie=1250", + "Boundary/domain value: beschikbaarSubsidiePlafond=1250" + ] + } + }, + { + "name": "TC_011 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=1251.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 1251, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 1251, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BasisPastBinnenResterendBudget", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: basisHoogteSubsidie=1251", + "Boundary/domain value: beschikbaarSubsidiePlafond=1251" + ] + } + }, + { + "name": "TC_012 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=437499.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 437499, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 437499, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BasisPastBinnenResterendBudget", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: basisHoogteSubsidie=437499", + "Boundary/domain value: beschikbaarSubsidiePlafond=437499" + ] + } + }, + { + "name": "TC_013 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=437500.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 437500, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 437500, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BasisPastBinnenResterendBudget", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: basisHoogteSubsidie=437500", + "Boundary/domain value: beschikbaarSubsidiePlafond=437500" + ] + } + }, + { + "name": "TC_014 Behaalbare Hoogte Subsidie - Representative selected rule 2 - Rule_Behaalbare_BeperktDoorResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=437500.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 437501, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 437500, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BeperktDoorResterendBudget", + "selectedRuleIndex": 2, + "reasons": [ + "Representative selected rule 2: Rule_Behaalbare_BeperktDoorResterendBudget" + ] + } + }, + { + "name": "TC_015 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=437501.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 437501, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 437501, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BasisPastBinnenResterendBudget", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: basisHoogteSubsidie=437501", + "Boundary/domain value: beschikbaarSubsidiePlafond=437501", + "Representative selected rule 1: Rule_Behaalbare_BasisPastBinnenResterendBudget", + "Output maximum: hoogteSubsidie=437501" + ] + } + }, + { + "name": "TC_016 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=749.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 749, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 1249, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BasisPastBinnenResterendBudget", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: basisHoogteSubsidie=749" + ] + } + }, + { + "name": "TC_017 Behaalbare Hoogte Subsidie - MC/DC - Rule_Behaalbare_BeperktDoorResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=1.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 749, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 1, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BeperktDoorResterendBudget", + "selectedRuleIndex": 2, + "reasons": [ + "MC/DC: rule 2 Rule_Behaalbare_BeperktDoorResterendBudget, condition [basisHoogteSubsidie > beschikbaarSubsidiePlafond] flipped" + ] + } + }, + { + "name": "TC_018 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=750.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 750, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 1249, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BasisPastBinnenResterendBudget", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: basisHoogteSubsidie=750" + ] + } + }, + { + "name": "TC_019 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=751.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 751, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 1249, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BasisPastBinnenResterendBudget", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: basisHoogteSubsidie=751" + ] + } + }, + { + "name": "TC_020 Bereken Basis Hoogte Subsidie - MC/DC - Rule_Basis_OnderMinimum", + "decisionId": "BerekenBasisHoogteSubsidie", + "decisionName": "Bereken Basis Hoogte Subsidie", + "decisionTableId": "DecisionTable_BerekenBasisHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "basisHoogteSubsidie=0.0", + "requestBody": { + "variables": { + "gemaakteKosten": { + "value": 0, + "type": "Double" + }, + "minimaleNoodzakelijkeKosten": { + "value": 750, + "type": "Double" + }, + "subsidieMaximum": { + "value": 1250, + "type": "Double" + }, + "subsidieMinimum": { + "value": 750, + "type": "Double" + }, + "subsidiePercentage": { + "value": 0.25, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Basis_OnderMinimum", + "selectedRuleIndex": 2, + "reasons": [ + "MC/DC: rule 1 Rule_Basis_BovenMinimum, condition [gemaakteKosten >= minimaleNoodzakelijkeKosten / subsidiePercentage] baseline", + "MC/DC: rule 2 Rule_Basis_OnderMinimum, condition [gemaakteKosten < minimaleNoodzakelijkeKosten / subsidiePercentage] flipped", + "Boundary/domain value: gemaakteKosten=0", + "Representative selected rule 2: Rule_Basis_OnderMinimum", + "Output minimum: basisHoogteSubsidie=0", + "Output near zero boundary: basisHoogteSubsidie=0" + ] + } + }, + { + "name": "TC_021 Bereken Basis Hoogte Subsidie - Boundary/domain value - Rule_Basis_BovenMinimum", + "decisionId": "BerekenBasisHoogteSubsidie", + "decisionName": "Bereken Basis Hoogte Subsidie", + "decisionTableId": "DecisionTable_BerekenBasisHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "basisHoogteSubsidie=1250.0", + "requestBody": { + "variables": { + "gemaakteKosten": { + "value": 10000, + "type": "Double" + }, + "minimaleNoodzakelijkeKosten": { + "value": 750, + "type": "Double" + }, + "subsidieMaximum": { + "value": 1250, + "type": "Double" + }, + "subsidieMinimum": { + "value": 750, + "type": "Double" + }, + "subsidiePercentage": { + "value": 0.25, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Basis_BovenMinimum", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: gemaakteKosten=10000", + "Boundary/domain value: minimaleNoodzakelijkeKosten=750", + "Boundary/domain value: subsidieMaximum=1250", + "Boundary/domain value: subsidieMinimum=750", + "Boundary/domain value: subsidiePercentage=0.25", + "Representative selected rule 1: Rule_Basis_BovenMinimum", + "Output maximum: basisHoogteSubsidie=1250" + ] + } + }, + { + "name": "TC_022 Bereken Basis Hoogte Subsidie - Boundary/domain value - Rule_Basis_OnderMinimum", + "decisionId": "BerekenBasisHoogteSubsidie", + "decisionName": "Bereken Basis Hoogte Subsidie", + "decisionTableId": "DecisionTable_BerekenBasisHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "basisHoogteSubsidie=0.0", + "requestBody": { + "variables": { + "gemaakteKosten": { + "value": 2999, + "type": "Double" + }, + "minimaleNoodzakelijkeKosten": { + "value": 750, + "type": "Double" + }, + "subsidieMaximum": { + "value": 1250, + "type": "Double" + }, + "subsidieMinimum": { + "value": 750, + "type": "Double" + }, + "subsidiePercentage": { + "value": 0.25, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Basis_OnderMinimum", + "selectedRuleIndex": 2, + "reasons": [ + "Boundary/domain value: gemaakteKosten=2999" + ] + } + }, + { + "name": "TC_023 Bereken Basis Hoogte Subsidie - MC/DC - Rule_Basis_BovenMinimum", + "decisionId": "BerekenBasisHoogteSubsidie", + "decisionName": "Bereken Basis Hoogte Subsidie", + "decisionTableId": "DecisionTable_BerekenBasisHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "basisHoogteSubsidie=750.0", + "requestBody": { + "variables": { + "gemaakteKosten": { + "value": 3000, + "type": "Double" + }, + "minimaleNoodzakelijkeKosten": { + "value": 750, + "type": "Double" + }, + "subsidieMaximum": { + "value": 1250, + "type": "Double" + }, + "subsidieMinimum": { + "value": 750, + "type": "Double" + }, + "subsidiePercentage": { + "value": 0.25, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Basis_BovenMinimum", + "selectedRuleIndex": 1, + "reasons": [ + "MC/DC: rule 1 Rule_Basis_BovenMinimum, condition [gemaakteKosten >= minimaleNoodzakelijkeKosten / subsidiePercentage] flipped", + "MC/DC: rule 2 Rule_Basis_OnderMinimum, condition [gemaakteKosten < minimaleNoodzakelijkeKosten / subsidiePercentage] baseline", + "Boundary/domain value: gemaakteKosten=3000" + ] + } + }, + { + "name": "TC_024 Bereken Basis Hoogte Subsidie - Boundary/domain value - Rule_Basis_BovenMinimum", + "decisionId": "BerekenBasisHoogteSubsidie", + "decisionName": "Bereken Basis Hoogte Subsidie", + "decisionTableId": "DecisionTable_BerekenBasisHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "basisHoogteSubsidie=750.25", + "requestBody": { + "variables": { + "gemaakteKosten": { + "value": 3001, + "type": "Double" + }, + "minimaleNoodzakelijkeKosten": { + "value": 750, + "type": "Double" + }, + "subsidieMaximum": { + "value": 1250, + "type": "Double" + }, + "subsidieMinimum": { + "value": 750, + "type": "Double" + }, + "subsidiePercentage": { + "value": 0.25, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Basis_BovenMinimum", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: gemaakteKosten=3001" + ] + } + }, + { + "name": "TC_025 Bereken Basis Hoogte Subsidie - Boundary/domain value - Rule_Basis_BovenMinimum", + "decisionId": "BerekenBasisHoogteSubsidie", + "decisionName": "Bereken Basis Hoogte Subsidie", + "decisionTableId": "DecisionTable_BerekenBasisHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "basisHoogteSubsidie=1249.75", + "requestBody": { + "variables": { + "gemaakteKosten": { + "value": 4999, + "type": "Double" + }, + "minimaleNoodzakelijkeKosten": { + "value": 750, + "type": "Double" + }, + "subsidieMaximum": { + "value": 1250, + "type": "Double" + }, + "subsidieMinimum": { + "value": 750, + "type": "Double" + }, + "subsidiePercentage": { + "value": 0.25, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Basis_BovenMinimum", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: gemaakteKosten=4999" + ] + } + }, + { + "name": "TC_026 Bereken Basis Hoogte Subsidie - Boundary/domain value - Rule_Basis_BovenMinimum", + "decisionId": "BerekenBasisHoogteSubsidie", + "decisionName": "Bereken Basis Hoogte Subsidie", + "decisionTableId": "DecisionTable_BerekenBasisHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "basisHoogteSubsidie=1250.0", + "requestBody": { + "variables": { + "gemaakteKosten": { + "value": 5000, + "type": "Double" + }, + "minimaleNoodzakelijkeKosten": { + "value": 750, + "type": "Double" + }, + "subsidieMaximum": { + "value": 1250, + "type": "Double" + }, + "subsidieMinimum": { + "value": 750, + "type": "Double" + }, + "subsidiePercentage": { + "value": 0.25, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Basis_BovenMinimum", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: gemaakteKosten=5000" + ] + } + }, + { + "name": "TC_027 Bereken Basis Hoogte Subsidie - Boundary/domain value - Rule_Basis_BovenMinimum", + "decisionId": "BerekenBasisHoogteSubsidie", + "decisionName": "Bereken Basis Hoogte Subsidie", + "decisionTableId": "DecisionTable_BerekenBasisHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "basisHoogteSubsidie=1250.0", + "requestBody": { + "variables": { + "gemaakteKosten": { + "value": 5001, + "type": "Double" + }, + "minimaleNoodzakelijkeKosten": { + "value": 750, + "type": "Double" + }, + "subsidieMaximum": { + "value": 1250, + "type": "Double" + }, + "subsidieMinimum": { + "value": 750, + "type": "Double" + }, + "subsidiePercentage": { + "value": 0.25, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Basis_BovenMinimum", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: gemaakteKosten=5001" + ] + } + }, + { + "name": "TC_028 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_BuitenAanvraagperiodeOfOnbekendType", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=0.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2025-12-31", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_BuitenAanvraagperiodeOfOnbekendType", + "selectedRuleIndex": 7, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2025-12-31'", + "Representative selected rule 7: Rule_Platform_BuitenAanvraagperiodeOfOnbekendType" + ] + } + }, + { + "name": "TC_029 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_BuitenAanvraagperiodeOfOnbekendType", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=0.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2025-12-31", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 437500, + "type": "Double" + }, + "plafondHuurders": { + "value": 437500, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_BuitenAanvraagperiodeOfOnbekendType", + "selectedRuleIndex": 7, + "reasons": [ + "MC/DC: rule 1 Rule_Platform_2026_Eigenaar_Gescheiden, condition [date(aanvraagDatum) >= date(\"2026-01-12\")] baseline", + "MC/DC: rule 3 Rule_Platform_2026_Gebundeld, condition [date(aanvraagDatum) >= date(\"2026-10-01\")] baseline", + "MC/DC: rule 4 Rule_Platform_2027_Eigenaar_Gescheiden, condition [date(\"2027-01-01\") <= date(aanvraagDatum)] baseline", + "MC/DC: rule 6 Rule_Platform_2027_Gebundeld, condition [date(aanvraagDatum) >= date(\"2027-10-01\")] baseline", + "Output minimum: beschikbaarSubsidiePlafond=0", + "Output near zero boundary: beschikbaarSubsidiePlafond=0" + ] + } + }, + { + "name": "TC_030 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_BuitenAanvraagperiodeOfOnbekendType", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=0.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2025-12-31", + "type": "String" + }, + "aanvragerType": { + "value": "huurder", + "type": "String" + }, + "plafondEigenaren": { + "value": 437500, + "type": "Double" + }, + "plafondHuurders": { + "value": 437500, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_BuitenAanvraagperiodeOfOnbekendType", + "selectedRuleIndex": 7, + "reasons": [ + "MC/DC: rule 2 Rule_Platform_2026_Huurder_Gescheiden, condition [date(aanvraagDatum) >= date(\"2026-01-12\")] baseline", + "MC/DC: rule 5 Rule_Platform_2027_Huurder_Gescheiden, condition [date(aanvraagDatum) >= date(\"2027-01-01\")] baseline" + ] + } + }, + { + "name": "TC_031 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_BuitenAanvraagperiodeOfOnbekendType", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=0.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-01-11", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_BuitenAanvraagperiodeOfOnbekendType", + "selectedRuleIndex": 7, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2026-01-11'" + ] + } + }, + { + "name": "TC_032 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Eigenaar_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-01-12", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Eigenaar_Gescheiden", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2026-01-12'", + "Representative selected rule 1: Rule_Platform_2026_Eigenaar_Gescheiden" + ] + } + }, + { + "name": "TC_033 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Eigenaar_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-01-12", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 1000000, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Eigenaar_Gescheiden", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdHuurders=1000000" + ] + } + }, + { + "name": "TC_034 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Eigenaar_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-01-12", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 1000001, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Eigenaar_Gescheiden", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdHuurders=1000001" + ] + } + }, + { + "name": "TC_035 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_2026_Eigenaar_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=437500.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-01-12", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 437500, + "type": "Double" + }, + "plafondHuurders": { + "value": 437500, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Eigenaar_Gescheiden", + "selectedRuleIndex": 1, + "reasons": [ + "MC/DC: rule 1 Rule_Platform_2026_Eigenaar_Gescheiden, condition [date(aanvraagDatum) >= date(\"2026-01-12\")] flipped", + "MC/DC: rule 1 Rule_Platform_2026_Eigenaar_Gescheiden, condition [date(aanvraagDatum) <= date(\"2026-09-30\")] flipped" + ] + } + }, + { + "name": "TC_036 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_2026_Eigenaar_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=437500.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-01-12", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 437500, + "type": "Double" + }, + "plafondHuurders": { + "value": 437500, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 1, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Eigenaar_Gescheiden", + "selectedRuleIndex": 1, + "reasons": [ + "MC/DC: rule 1 Rule_Platform_2026_Eigenaar_Gescheiden, condition [aanvragerType == \"eigenaar\"] flipped", + "MC/DC: rule 2 Rule_Platform_2026_Huurder_Gescheiden, condition [aanvragerType == \"huurder\"] baseline" + ] + } + }, + { + "name": "TC_037 Bereken Beschikbaar Subsidie Plafond - Representative selected rule 2 - Rule_Platform_2026_Huurder_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-01-12", + "type": "String" + }, + "aanvragerType": { + "value": "huurder", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Huurder_Gescheiden", + "selectedRuleIndex": 2, + "reasons": [ + "Representative selected rule 2: Rule_Platform_2026_Huurder_Gescheiden" + ] + } + }, + { + "name": "TC_038 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Huurder_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-01-12", + "type": "String" + }, + "aanvragerType": { + "value": "huurder", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Huurder_Gescheiden", + "selectedRuleIndex": 2, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdEigenaren=1000000" + ] + } + }, + { + "name": "TC_039 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Huurder_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-01-12", + "type": "String" + }, + "aanvragerType": { + "value": "huurder", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 1000001, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Huurder_Gescheiden", + "selectedRuleIndex": 2, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdEigenaren=1000001" + ] + } + }, + { + "name": "TC_040 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_2026_Huurder_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=437500.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-01-12", + "type": "String" + }, + "aanvragerType": { + "value": "huurder", + "type": "String" + }, + "plafondEigenaren": { + "value": 437500, + "type": "Double" + }, + "plafondHuurders": { + "value": 437500, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Huurder_Gescheiden", + "selectedRuleIndex": 2, + "reasons": [ + "MC/DC: rule 2 Rule_Platform_2026_Huurder_Gescheiden, condition [date(aanvraagDatum) >= date(\"2026-01-12\")] flipped", + "MC/DC: rule 2 Rule_Platform_2026_Huurder_Gescheiden, condition [date(aanvraagDatum) <= date(\"2026-09-30\")] flipped" + ] + } + }, + { + "name": "TC_041 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_2026_Huurder_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=437499.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-01-12", + "type": "String" + }, + "aanvragerType": { + "value": "huurder", + "type": "String" + }, + "plafondEigenaren": { + "value": 437500, + "type": "Double" + }, + "plafondHuurders": { + "value": 437500, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 1, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Huurder_Gescheiden", + "selectedRuleIndex": 2, + "reasons": [ + "MC/DC: rule 1 Rule_Platform_2026_Eigenaar_Gescheiden, condition [aanvragerType == \"eigenaar\"] baseline", + "MC/DC: rule 2 Rule_Platform_2026_Huurder_Gescheiden, condition [aanvragerType == \"huurder\"] flipped" + ] + } + }, + { + "name": "TC_042 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Eigenaar_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-01-13", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Eigenaar_Gescheiden", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2026-01-13'" + ] + } + }, + { + "name": "TC_043 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Eigenaar_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-09-29", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Eigenaar_Gescheiden", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2026-09-29'" + ] + } + }, + { + "name": "TC_044 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Eigenaar_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-09-30", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Eigenaar_Gescheiden", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2026-09-30'" + ] + } + }, + { + "name": "TC_045 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=2000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2026-10-01'", + "Boundary/domain value: aanvragerType='eigenaar'", + "Boundary/domain value: plafondEigenaren=1000000", + "Boundary/domain value: plafondHuurders=1000000", + "Boundary/domain value: reedsGesubsidieerdEigenaren=0", + "Boundary/domain value: reedsGesubsidieerdHuurders=0", + "Representative selected rule 3: Rule_Platform_2026_Gebundeld" + ] + } + }, + { + "name": "TC_046 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1999999.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 1, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdHuurders=1" + ] + } + }, + { + "name": "TC_047 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1562501.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 437499, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdHuurders=437499" + ] + } + }, + { + "name": "TC_048 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1562500.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 437500, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdHuurders=437500" + ] + } + }, + { + "name": "TC_049 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1562499.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 437501, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdHuurders=437501" + ] + } + }, + { + "name": "TC_050 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1125001.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 874999, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdHuurders=874999" + ] + } + }, + { + "name": "TC_051 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1125000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 875000, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdHuurders=875000" + ] + } + }, + { + "name": "TC_052 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1124999.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 875001, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdHuurders=875001" + ] + } + }, + { + "name": "TC_053 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000001.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 999999, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdHuurders=999999" + ] + } + }, + { + "name": "TC_054 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1999999.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 1, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdEigenaren=1" + ] + } + }, + { + "name": "TC_055 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1562501.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 437499, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdEigenaren=437499" + ] + } + }, + { + "name": "TC_056 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1562500.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 437500, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdEigenaren=437500" + ] + } + }, + { + "name": "TC_057 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1562499.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 437501, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdEigenaren=437501" + ] + } + }, + { + "name": "TC_058 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1125001.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 874999, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdEigenaren=874999" + ] + } + }, + { + "name": "TC_059 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1125000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 875000, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdEigenaren=875000" + ] + } + }, + { + "name": "TC_060 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1124999.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 875001, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdEigenaren=875001" + ] + } + }, + { + "name": "TC_061 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000001.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 999999, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdEigenaren=999999" + ] + } + }, + { + "name": "TC_062 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1437500.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 437500, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: plafondHuurders=437500" + ] + } + }, + { + "name": "TC_063 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1875000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 875000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: plafondHuurders=875000" + ] + } + }, + { + "name": "TC_064 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1437500.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 437500, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: plafondEigenaren=437500" + ] + } + }, + { + "name": "TC_065 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=875000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 437500, + "type": "Double" + }, + "plafondHuurders": { + "value": 437500, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "MC/DC: rule 1 Rule_Platform_2026_Eigenaar_Gescheiden, condition [date(aanvraagDatum) <= date(\"2026-09-30\")] baseline", + "MC/DC: rule 3 Rule_Platform_2026_Gebundeld, condition [date(aanvraagDatum) >= date(\"2026-10-01\")] flipped", + "MC/DC: rule 3 Rule_Platform_2026_Gebundeld, condition [date(aanvraagDatum) <= date(\"2026-12-31\")] flipped" + ] + } + }, + { + "name": "TC_066 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1875000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 875000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: plafondEigenaren=875000" + ] + } + }, + { + "name": "TC_067 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=2000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "huurder", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: aanvragerType='huurder'" + ] + } + }, + { + "name": "TC_068 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=875000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "huurder", + "type": "String" + }, + "plafondEigenaren": { + "value": 437500, + "type": "Double" + }, + "plafondHuurders": { + "value": 437500, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "MC/DC: rule 2 Rule_Platform_2026_Huurder_Gescheiden, condition [date(aanvraagDatum) <= date(\"2026-09-30\")] baseline" + ] + } + }, + { + "name": "TC_069 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=2000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "onbekend", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: aanvragerType='onbekend'" + ] + } + }, + { + "name": "TC_070 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=2000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-02", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2026-10-02'" + ] + } + }, + { + "name": "TC_071 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=2000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-12-30", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2026-12-30'" + ] + } + }, + { + "name": "TC_072 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=2000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-12-31", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2026-12-31'" + ] + } + }, + { + "name": "TC_073 Bereken Beschikbaar Subsidie Plafond - Output maximum - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=2000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-12-31", + "type": "String" + }, + "aanvragerType": { + "value": "onbekend", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Output maximum: beschikbaarSubsidiePlafond=2000000" + ] + } + }, + { + "name": "TC_074 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2027_Eigenaar_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2027-01-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2027_Eigenaar_Gescheiden", + "selectedRuleIndex": 4, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2027-01-01'", + "Representative selected rule 4: Rule_Platform_2027_Eigenaar_Gescheiden" + ] + } + }, + { + "name": "TC_075 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_2027_Eigenaar_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=437500.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2027-01-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 437500, + "type": "Double" + }, + "plafondHuurders": { + "value": 437500, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2027_Eigenaar_Gescheiden", + "selectedRuleIndex": 4, + "reasons": [ + "MC/DC: rule 3 Rule_Platform_2026_Gebundeld, condition [date(aanvraagDatum) <= date(\"2026-12-31\")] baseline", + "MC/DC: rule 4 Rule_Platform_2027_Eigenaar_Gescheiden, condition [date(\"2027-01-01\") <= date(aanvraagDatum)] flipped", + "MC/DC: rule 4 Rule_Platform_2027_Eigenaar_Gescheiden, condition [date(aanvraagDatum) <= date(\"2027-09-30\")] flipped" + ] + } + }, + { + "name": "TC_076 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_2027_Eigenaar_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=437500.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2027-01-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 437500, + "type": "Double" + }, + "plafondHuurders": { + "value": 437500, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 1, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2027_Eigenaar_Gescheiden", + "selectedRuleIndex": 4, + "reasons": [ + "MC/DC: rule 4 Rule_Platform_2027_Eigenaar_Gescheiden, condition [aanvragerType == \"eigenaar\"] flipped", + "MC/DC: rule 5 Rule_Platform_2027_Huurder_Gescheiden, condition [aanvragerType == \"huurder\"] baseline" + ] + } + }, + { + "name": "TC_077 Bereken Beschikbaar Subsidie Plafond - Representative selected rule 5 - Rule_Platform_2027_Huurder_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2027-01-01", + "type": "String" + }, + "aanvragerType": { + "value": "huurder", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2027_Huurder_Gescheiden", + "selectedRuleIndex": 5, + "reasons": [ + "Representative selected rule 5: Rule_Platform_2027_Huurder_Gescheiden" + ] + } + }, + { + "name": "TC_078 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_2027_Huurder_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=437500.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2027-01-01", + "type": "String" + }, + "aanvragerType": { + "value": "huurder", + "type": "String" + }, + "plafondEigenaren": { + "value": 437500, + "type": "Double" + }, + "plafondHuurders": { + "value": 437500, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2027_Huurder_Gescheiden", + "selectedRuleIndex": 5, + "reasons": [ + "MC/DC: rule 5 Rule_Platform_2027_Huurder_Gescheiden, condition [date(aanvraagDatum) >= date(\"2027-01-01\")] flipped", + "MC/DC: rule 5 Rule_Platform_2027_Huurder_Gescheiden, condition [date(aanvraagDatum) <= date(\"2027-09-30\")] flipped" + ] + } + }, + { + "name": "TC_079 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_2027_Huurder_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=437499.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2027-01-01", + "type": "String" + }, + "aanvragerType": { + "value": "huurder", + "type": "String" + }, + "plafondEigenaren": { + "value": 437500, + "type": "Double" + }, + "plafondHuurders": { + "value": 437500, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 1, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2027_Huurder_Gescheiden", + "selectedRuleIndex": 5, + "reasons": [ + "MC/DC: rule 4 Rule_Platform_2027_Eigenaar_Gescheiden, condition [aanvragerType == \"eigenaar\"] baseline", + "MC/DC: rule 5 Rule_Platform_2027_Huurder_Gescheiden, condition [aanvragerType == \"huurder\"] flipped" + ] + } + }, + { + "name": "TC_080 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2027_Eigenaar_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2027-01-02", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2027_Eigenaar_Gescheiden", + "selectedRuleIndex": 4, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2027-01-02'" + ] + } + }, + { + "name": "TC_081 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2027_Eigenaar_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2027-09-29", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2027_Eigenaar_Gescheiden", + "selectedRuleIndex": 4, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2027-09-29'" + ] + } + }, + { + "name": "TC_082 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2027_Eigenaar_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2027-09-30", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2027_Eigenaar_Gescheiden", + "selectedRuleIndex": 4, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2027-09-30'" + ] + } + }, + { + "name": "TC_083 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2027_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2027-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2027_Gebundeld", + "selectedRuleIndex": 6, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2027-10-01'", + "Representative selected rule 6: Rule_Platform_2027_Gebundeld" + ] + } + }, + { + "name": "TC_084 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_2027_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2027-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 437500, + "type": "Double" + }, + "plafondHuurders": { + "value": 437500, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2027_Gebundeld", + "selectedRuleIndex": 6, + "reasons": [ + "MC/DC: rule 4 Rule_Platform_2027_Eigenaar_Gescheiden, condition [date(aanvraagDatum) <= date(\"2027-09-30\")] baseline", + "MC/DC: rule 6 Rule_Platform_2027_Gebundeld, condition [date(aanvraagDatum) >= date(\"2027-10-01\")] flipped", + "MC/DC: rule 6 Rule_Platform_2027_Gebundeld, condition [date(aanvraagDatum) <= date(\"2027-12-31\")] flipped" + ] + } + }, + { + "name": "TC_085 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_2027_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2027-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "huurder", + "type": "String" + }, + "plafondEigenaren": { + "value": 437500, + "type": "Double" + }, + "plafondHuurders": { + "value": 437500, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2027_Gebundeld", + "selectedRuleIndex": 6, + "reasons": [ + "MC/DC: rule 5 Rule_Platform_2027_Huurder_Gescheiden, condition [date(aanvraagDatum) <= date(\"2027-09-30\")] baseline" + ] + } + }, + { + "name": "TC_086 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2027_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2027-10-02", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2027_Gebundeld", + "selectedRuleIndex": 6, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2027-10-02'" + ] + } + }, + { + "name": "TC_087 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2027_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2027-12-30", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2027_Gebundeld", + "selectedRuleIndex": 6, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2027-12-30'" + ] + } + }, + { + "name": "TC_088 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2027_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2027-12-31", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2027_Gebundeld", + "selectedRuleIndex": 6, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2027-12-31'" + ] + } + }, + { + "name": "TC_089 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_BuitenAanvraagperiodeOfOnbekendType", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=0.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2028-01-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_BuitenAanvraagperiodeOfOnbekendType", + "selectedRuleIndex": 7, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2028-01-01'" + ] + } + }, + { + "name": "TC_090 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_BuitenAanvraagperiodeOfOnbekendType", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=0.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2028-01-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 437500, + "type": "Double" + }, + "plafondHuurders": { + "value": 437500, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_BuitenAanvraagperiodeOfOnbekendType", + "selectedRuleIndex": 7, + "reasons": [ + "MC/DC: rule 6 Rule_Platform_2027_Gebundeld, condition [date(aanvraagDatum) <= date(\"2027-12-31\")] baseline" + ] + } + }, + { + "name": "TC_091 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_BuitenAanvraagperiodeOfOnbekendType", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=0.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2029-01-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_BuitenAanvraagperiodeOfOnbekendType", + "selectedRuleIndex": 7, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2029-01-01'" + ] + } + }, + { + "name": "TC_092 Subsidie Constanten Thuisbatterij - Representative selected rule 1 - Rule_Constanten_Standaard", + "decisionId": "SubsidieConstantenThuisbatterij", + "decisionName": "Subsidie Constanten Thuisbatterij", + "decisionTableId": "DecisionTable_SubsidieConstantenThuisbatterij", + "evaluationMode": "direct-table-inputs", + "expected": "subsidiePercentage=0.25, subsidieMinimum=750.0, subsidieMaximum=1250.0, minimaleNoodzakelijkeKosten=750.0", + "requestBody": { + "variables": {} + }, + "coverage": { + "selectedRuleId": "Rule_Constanten_Standaard", + "selectedRuleIndex": 1, + "reasons": [ + "Representative selected rule 1: Rule_Constanten_Standaard", + "Output minimum: subsidiePercentage=0.25", + "Output maximum: subsidiePercentage=0.25", + "Output near zero boundary: subsidiePercentage=0.25", + "Output minimum: subsidieMinimum=750", + "Output maximum: subsidieMinimum=750", + "Output near zero boundary: subsidieMinimum=750", + "Output minimum: subsidieMaximum=1250", + "Output maximum: subsidieMaximum=1250", + "Output near zero boundary: subsidieMaximum=1250", + "Output minimum: minimaleNoodzakelijkeKosten=750", + "Output maximum: minimaleNoodzakelijkeKosten=750", + "Output near zero boundary: minimaleNoodzakelijkeKosten=750" + ] + } + }, + { + "name": "TC_093 Jaar Gebonden Budget - MC/DC - no matching rule", + "decisionId": "jaarGebondenBudget", + "decisionName": "Jaar Gebonden Budget", + "decisionTableId": "DecisionTable_1bhjh1w", + "evaluationMode": "direct-table-inputs", + "expected": "plafondEigenaren=null, plafondHuurders=null", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2025-12-31", + "type": "String" + } + } + }, + "coverage": { + "selectedRuleId": null, + "selectedRuleIndex": null, + "reasons": [ + "MC/DC: rule 1 DecisionRule_0nfr6kl, condition [date(aanvraagDatum).year == 2026] baseline", + "MC/DC: rule 2 DecisionRule_1e5agpr, condition [date(aanvraagDatum).year == 2027] baseline", + "Boundary/domain value: aanvraagDatum='2025-12-31'" + ] + } + }, + { + "name": "TC_094 Jaar Gebonden Budget - MC/DC - DecisionRule_0nfr6kl", + "decisionId": "jaarGebondenBudget", + "decisionName": "Jaar Gebonden Budget", + "decisionTableId": "DecisionTable_1bhjh1w", + "evaluationMode": "direct-table-inputs", + "expected": "plafondEigenaren=437500.0, plafondHuurders=437500.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-01-01", + "type": "String" + } + } + }, + "coverage": { + "selectedRuleId": "DecisionRule_0nfr6kl", + "selectedRuleIndex": 1, + "reasons": [ + "MC/DC: rule 1 DecisionRule_0nfr6kl, condition [date(aanvraagDatum).year == 2026] flipped", + "Boundary/domain value: aanvraagDatum='2026-01-01'", + "Representative selected rule 1: DecisionRule_0nfr6kl", + "Output minimum: plafondEigenaren=437500", + "Output near zero boundary: plafondEigenaren=437500", + "Output minimum: plafondHuurders=437500", + "Output near zero boundary: plafondHuurders=437500" + ] + } + }, + { + "name": "TC_095 Jaar Gebonden Budget - Boundary/domain value - DecisionRule_0nfr6kl", + "decisionId": "jaarGebondenBudget", + "decisionName": "Jaar Gebonden Budget", + "decisionTableId": "DecisionTable_1bhjh1w", + "evaluationMode": "direct-table-inputs", + "expected": "plafondEigenaren=437500.0, plafondHuurders=437500.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-12-31", + "type": "String" + } + } + }, + "coverage": { + "selectedRuleId": "DecisionRule_0nfr6kl", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2026-12-31'" + ] + } + }, + { + "name": "TC_096 Jaar Gebonden Budget - MC/DC - DecisionRule_1e5agpr", + "decisionId": "jaarGebondenBudget", + "decisionName": "Jaar Gebonden Budget", + "decisionTableId": "DecisionTable_1bhjh1w", + "evaluationMode": "direct-table-inputs", + "expected": "plafondEigenaren=437500.0, plafondHuurders=437500.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2027-01-01", + "type": "String" + } + } + }, + "coverage": { + "selectedRuleId": "DecisionRule_1e5agpr", + "selectedRuleIndex": 2, + "reasons": [ + "MC/DC: rule 2 DecisionRule_1e5agpr, condition [date(aanvraagDatum).year == 2027] flipped", + "Boundary/domain value: aanvraagDatum='2027-01-01'", + "Representative selected rule 2: DecisionRule_1e5agpr" + ] + } + }, + { + "name": "TC_097 Jaar Gebonden Budget - Boundary/domain value - DecisionRule_1e5agpr", + "decisionId": "jaarGebondenBudget", + "decisionName": "Jaar Gebonden Budget", + "decisionTableId": "DecisionTable_1bhjh1w", + "evaluationMode": "direct-table-inputs", + "expected": "plafondEigenaren=437500.0, plafondHuurders=437500.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2027-12-31", + "type": "String" + } + } + }, + "coverage": { + "selectedRuleId": "DecisionRule_1e5agpr", + "selectedRuleIndex": 2, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2027-12-31'", + "Output maximum: plafondEigenaren=437500", + "Output maximum: plafondHuurders=437500" + ] + } + }, + { + "name": "TC_098 Jaar Gebonden Budget - Boundary/domain value - no matching rule", + "decisionId": "jaarGebondenBudget", + "decisionName": "Jaar Gebonden Budget", + "decisionTableId": "DecisionTable_1bhjh1w", + "evaluationMode": "direct-table-inputs", + "expected": "plafondEigenaren=null, plafondHuurders=null", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2028-01-01", + "type": "String" + } + } + }, + "coverage": { + "selectedRuleId": null, + "selectedRuleIndex": null, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2028-01-01'" + ] + } + } +] diff --git a/examples/organizations/flevoland/thuisbatterij/generatedRepaired/hoogteVergoedingThuisbatterijBijRechtEnBudget-mcdc-test-cases.xlsm b/examples/organizations/flevoland/thuisbatterij/generatedRepaired/hoogteVergoedingThuisbatterijBijRechtEnBudget-mcdc-test-cases.xlsm new file mode 100644 index 0000000..1fb801d Binary files /dev/null and b/examples/organizations/flevoland/thuisbatterij/generatedRepaired/hoogteVergoedingThuisbatterijBijRechtEnBudget-mcdc-test-cases.xlsm differ diff --git a/examples/organizations/flevoland/thuisbatterij/generatedRepaired/hoogteVergoedingThuisbatterijBijRechtEnBudget.postman_collection.mcdc_examples.json b/examples/organizations/flevoland/thuisbatterij/generatedRepaired/hoogteVergoedingThuisbatterijBijRechtEnBudget.postman_collection.mcdc_examples.json new file mode 100644 index 0000000..b169333 --- /dev/null +++ b/examples/organizations/flevoland/thuisbatterij/generatedRepaired/hoogteVergoedingThuisbatterijBijRechtEnBudget.postman_collection.mcdc_examples.json @@ -0,0 +1,4724 @@ +{ + "info": { + "_postman_id": "28b9242d-68f1-4aa8-9c21-e993bc527036", + "name": "DMN MC/DC generated collection - MC/DC examples", + "description": "Generated from DMN and MC/DC boundary test cases.\n\nGenerated collection with 98 MC/DC and boundary-value test cases grouped as Postman examples under their corresponding DMN decision calls.", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "MC/DC generation summary", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "about:blank", + "host": [ + "about:blank" + ] + }, + "description": "{\n \"summary\": \"Generated MC/DC examples grouped under the corresponding DMN decision-evaluate request.\",\n \"totalTestCases\": 98,\n \"testCasesPerDecision\": [\n [\n \"BehaalbareHoogteSubsidie\",\n 19\n ],\n [\n \"BerekenBasisHoogteSubsidie\",\n 8\n ],\n [\n \"BerekenBeschikbaarSubsidiePlafond\",\n 64\n ],\n [\n \"SubsidieConstantenThuisbatterij\",\n 1\n ],\n [\n \"jaarGebondenBudget\",\n 6\n ]\n ]\n}" + }, + "response": [] + }, + { + "name": "DMN decision calls with generated MC/DC examples", + "description": "One request per DMN decision key. Test cases are attached as Postman examples.", + "item": [ + { + "name": "BehaalbareHoogteSubsidie - MC/DC examples (19)", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"basisHoogteSubsidie\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"beschikbaarSubsidiePlafond\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BehaalbareHoogteSubsidie/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BehaalbareHoogteSubsidie", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "response": [ + { + "name": "TC_001 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"basisHoogteSubsidie\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"beschikbaarSubsidiePlafond\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BehaalbareHoogteSubsidie/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BehaalbareHoogteSubsidie", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"hoogteSubsidie\": {\n \"type\": \"Double\",\n \"value\": 0.0\n }\n }\n]", + "description": "Decision: Behaalbare Hoogte Subsidie\nDecision table: DecisionTable_BehaalbareHoogteSubsidie\nExpected: hoogteSubsidie=0.0\nCoverage reasons:\n- Boundary/domain value: basisHoogteSubsidie=0\n- Boundary/domain value: beschikbaarSubsidiePlafond=0\n- Output minimum: hoogteSubsidie=0\n- Output near zero boundary: hoogteSubsidie=0" + }, + { + "name": "TC_002 Behaalbare Hoogte Subsidie - MC/DC - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"basisHoogteSubsidie\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"beschikbaarSubsidiePlafond\": {\n \"value\": 1,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BehaalbareHoogteSubsidie/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BehaalbareHoogteSubsidie", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"hoogteSubsidie\": {\n \"type\": \"Double\",\n \"value\": 0.0\n }\n }\n]", + "description": "Decision: Behaalbare Hoogte Subsidie\nDecision table: DecisionTable_BehaalbareHoogteSubsidie\nExpected: hoogteSubsidie=0.0\nCoverage reasons:\n- MC/DC: rule 2 Rule_Behaalbare_BeperktDoorResterendBudget, condition [basisHoogteSubsidie > beschikbaarSubsidiePlafond] baseline" + }, + { + "name": "TC_003 Behaalbare Hoogte Subsidie - MC/DC - Rule_Behaalbare_BeperktDoorResterendBudget", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"basisHoogteSubsidie\": {\n \"value\": 1,\n \"type\": \"Double\"\n },\n \"beschikbaarSubsidiePlafond\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BehaalbareHoogteSubsidie/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BehaalbareHoogteSubsidie", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"hoogteSubsidie\": {\n \"type\": \"Double\",\n \"value\": 0.0\n }\n }\n]", + "description": "Decision: Behaalbare Hoogte Subsidie\nDecision table: DecisionTable_BehaalbareHoogteSubsidie\nExpected: hoogteSubsidie=0.0\nCoverage reasons:\n- MC/DC: rule 1 Rule_Behaalbare_BasisPastBinnenResterendBudget, condition [basisHoogteSubsidie <= beschikbaarSubsidiePlafond] baseline" + }, + { + "name": "TC_004 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"basisHoogteSubsidie\": {\n \"value\": 1,\n \"type\": \"Double\"\n },\n \"beschikbaarSubsidiePlafond\": {\n \"value\": 1249,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BehaalbareHoogteSubsidie/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BehaalbareHoogteSubsidie", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"hoogteSubsidie\": {\n \"type\": \"Double\",\n \"value\": 1.0\n }\n }\n]", + "description": "Decision: Behaalbare Hoogte Subsidie\nDecision table: DecisionTable_BehaalbareHoogteSubsidie\nExpected: hoogteSubsidie=1.0\nCoverage reasons:\n- Boundary/domain value: basisHoogteSubsidie=1" + }, + { + "name": "TC_005 Behaalbare Hoogte Subsidie - MC/DC - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"basisHoogteSubsidie\": {\n \"value\": 1,\n \"type\": \"Double\"\n },\n \"beschikbaarSubsidiePlafond\": {\n \"value\": 1,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BehaalbareHoogteSubsidie/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BehaalbareHoogteSubsidie", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"hoogteSubsidie\": {\n \"type\": \"Double\",\n \"value\": 1.0\n }\n }\n]", + "description": "Decision: Behaalbare Hoogte Subsidie\nDecision table: DecisionTable_BehaalbareHoogteSubsidie\nExpected: hoogteSubsidie=1.0\nCoverage reasons:\n- MC/DC: rule 1 Rule_Behaalbare_BasisPastBinnenResterendBudget, condition [basisHoogteSubsidie <= beschikbaarSubsidiePlafond] flipped\n- Boundary/domain value: beschikbaarSubsidiePlafond=1" + }, + { + "name": "TC_006 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"basisHoogteSubsidie\": {\n \"value\": 1249,\n \"type\": \"Double\"\n },\n \"beschikbaarSubsidiePlafond\": {\n \"value\": 1249,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BehaalbareHoogteSubsidie/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BehaalbareHoogteSubsidie", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"hoogteSubsidie\": {\n \"type\": \"Double\",\n \"value\": 1249.0\n }\n }\n]", + "description": "Decision: Behaalbare Hoogte Subsidie\nDecision table: DecisionTable_BehaalbareHoogteSubsidie\nExpected: hoogteSubsidie=1249.0\nCoverage reasons:\n- Boundary/domain value: basisHoogteSubsidie=1249\n- Boundary/domain value: beschikbaarSubsidiePlafond=1249" + }, + { + "name": "TC_007 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BeperktDoorResterendBudget", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"basisHoogteSubsidie\": {\n \"value\": 1249,\n \"type\": \"Double\"\n },\n \"beschikbaarSubsidiePlafond\": {\n \"value\": 749,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BehaalbareHoogteSubsidie/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BehaalbareHoogteSubsidie", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"hoogteSubsidie\": {\n \"type\": \"Double\",\n \"value\": 749.0\n }\n }\n]", + "description": "Decision: Behaalbare Hoogte Subsidie\nDecision table: DecisionTable_BehaalbareHoogteSubsidie\nExpected: hoogteSubsidie=749.0\nCoverage reasons:\n- Boundary/domain value: beschikbaarSubsidiePlafond=749" + }, + { + "name": "TC_008 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BeperktDoorResterendBudget", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"basisHoogteSubsidie\": {\n \"value\": 1249,\n \"type\": \"Double\"\n },\n \"beschikbaarSubsidiePlafond\": {\n \"value\": 750,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BehaalbareHoogteSubsidie/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BehaalbareHoogteSubsidie", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"hoogteSubsidie\": {\n \"type\": \"Double\",\n \"value\": 750.0\n }\n }\n]", + "description": "Decision: Behaalbare Hoogte Subsidie\nDecision table: DecisionTable_BehaalbareHoogteSubsidie\nExpected: hoogteSubsidie=750.0\nCoverage reasons:\n- Boundary/domain value: beschikbaarSubsidiePlafond=750" + }, + { + "name": "TC_009 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BeperktDoorResterendBudget", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"basisHoogteSubsidie\": {\n \"value\": 1249,\n \"type\": \"Double\"\n },\n \"beschikbaarSubsidiePlafond\": {\n \"value\": 751,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BehaalbareHoogteSubsidie/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BehaalbareHoogteSubsidie", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"hoogteSubsidie\": {\n \"type\": \"Double\",\n \"value\": 751.0\n }\n }\n]", + "description": "Decision: Behaalbare Hoogte Subsidie\nDecision table: DecisionTable_BehaalbareHoogteSubsidie\nExpected: hoogteSubsidie=751.0\nCoverage reasons:\n- Boundary/domain value: beschikbaarSubsidiePlafond=751" + }, + { + "name": "TC_010 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"basisHoogteSubsidie\": {\n \"value\": 1250,\n \"type\": \"Double\"\n },\n \"beschikbaarSubsidiePlafond\": {\n \"value\": 1250,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BehaalbareHoogteSubsidie/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BehaalbareHoogteSubsidie", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"hoogteSubsidie\": {\n \"type\": \"Double\",\n \"value\": 1250.0\n }\n }\n]", + "description": "Decision: Behaalbare Hoogte Subsidie\nDecision table: DecisionTable_BehaalbareHoogteSubsidie\nExpected: hoogteSubsidie=1250.0\nCoverage reasons:\n- Boundary/domain value: basisHoogteSubsidie=1250\n- Boundary/domain value: beschikbaarSubsidiePlafond=1250" + }, + { + "name": "TC_011 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"basisHoogteSubsidie\": {\n \"value\": 1251,\n \"type\": \"Double\"\n },\n \"beschikbaarSubsidiePlafond\": {\n \"value\": 1251,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BehaalbareHoogteSubsidie/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BehaalbareHoogteSubsidie", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"hoogteSubsidie\": {\n \"type\": \"Double\",\n \"value\": 1251.0\n }\n }\n]", + "description": "Decision: Behaalbare Hoogte Subsidie\nDecision table: DecisionTable_BehaalbareHoogteSubsidie\nExpected: hoogteSubsidie=1251.0\nCoverage reasons:\n- Boundary/domain value: basisHoogteSubsidie=1251\n- Boundary/domain value: beschikbaarSubsidiePlafond=1251" + }, + { + "name": "TC_012 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"basisHoogteSubsidie\": {\n \"value\": 437499,\n \"type\": \"Double\"\n },\n \"beschikbaarSubsidiePlafond\": {\n \"value\": 437499,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BehaalbareHoogteSubsidie/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BehaalbareHoogteSubsidie", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"hoogteSubsidie\": {\n \"type\": \"Double\",\n \"value\": 437499.0\n }\n }\n]", + "description": "Decision: Behaalbare Hoogte Subsidie\nDecision table: DecisionTable_BehaalbareHoogteSubsidie\nExpected: hoogteSubsidie=437499.0\nCoverage reasons:\n- Boundary/domain value: basisHoogteSubsidie=437499\n- Boundary/domain value: beschikbaarSubsidiePlafond=437499" + }, + { + "name": "TC_013 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"basisHoogteSubsidie\": {\n \"value\": 437500,\n \"type\": \"Double\"\n },\n \"beschikbaarSubsidiePlafond\": {\n \"value\": 437500,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BehaalbareHoogteSubsidie/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BehaalbareHoogteSubsidie", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"hoogteSubsidie\": {\n \"type\": \"Double\",\n \"value\": 437500.0\n }\n }\n]", + "description": "Decision: Behaalbare Hoogte Subsidie\nDecision table: DecisionTable_BehaalbareHoogteSubsidie\nExpected: hoogteSubsidie=437500.0\nCoverage reasons:\n- Boundary/domain value: basisHoogteSubsidie=437500\n- Boundary/domain value: beschikbaarSubsidiePlafond=437500" + }, + { + "name": "TC_014 Behaalbare Hoogte Subsidie - Representative selected rule 2 - Rule_Behaalbare_BeperktDoorResterendBudget", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"basisHoogteSubsidie\": {\n \"value\": 437501,\n \"type\": \"Double\"\n },\n \"beschikbaarSubsidiePlafond\": {\n \"value\": 437500,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BehaalbareHoogteSubsidie/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BehaalbareHoogteSubsidie", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"hoogteSubsidie\": {\n \"type\": \"Double\",\n \"value\": 437500.0\n }\n }\n]", + "description": "Decision: Behaalbare Hoogte Subsidie\nDecision table: DecisionTable_BehaalbareHoogteSubsidie\nExpected: hoogteSubsidie=437500.0\nCoverage reasons:\n- Representative selected rule 2: Rule_Behaalbare_BeperktDoorResterendBudget" + }, + { + "name": "TC_015 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"basisHoogteSubsidie\": {\n \"value\": 437501,\n \"type\": \"Double\"\n },\n \"beschikbaarSubsidiePlafond\": {\n \"value\": 437501,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BehaalbareHoogteSubsidie/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BehaalbareHoogteSubsidie", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"hoogteSubsidie\": {\n \"type\": \"Double\",\n \"value\": 437501.0\n }\n }\n]", + "description": "Decision: Behaalbare Hoogte Subsidie\nDecision table: DecisionTable_BehaalbareHoogteSubsidie\nExpected: hoogteSubsidie=437501.0\nCoverage reasons:\n- Boundary/domain value: basisHoogteSubsidie=437501\n- Boundary/domain value: beschikbaarSubsidiePlafond=437501\n- Representative selected rule 1: Rule_Behaalbare_BasisPastBinnenResterendBudget\n- Output maximum: hoogteSubsidie=437501" + }, + { + "name": "TC_016 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"basisHoogteSubsidie\": {\n \"value\": 749,\n \"type\": \"Double\"\n },\n \"beschikbaarSubsidiePlafond\": {\n \"value\": 1249,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BehaalbareHoogteSubsidie/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BehaalbareHoogteSubsidie", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"hoogteSubsidie\": {\n \"type\": \"Double\",\n \"value\": 749.0\n }\n }\n]", + "description": "Decision: Behaalbare Hoogte Subsidie\nDecision table: DecisionTable_BehaalbareHoogteSubsidie\nExpected: hoogteSubsidie=749.0\nCoverage reasons:\n- Boundary/domain value: basisHoogteSubsidie=749" + }, + { + "name": "TC_017 Behaalbare Hoogte Subsidie - MC/DC - Rule_Behaalbare_BeperktDoorResterendBudget", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"basisHoogteSubsidie\": {\n \"value\": 749,\n \"type\": \"Double\"\n },\n \"beschikbaarSubsidiePlafond\": {\n \"value\": 1,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BehaalbareHoogteSubsidie/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BehaalbareHoogteSubsidie", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"hoogteSubsidie\": {\n \"type\": \"Double\",\n \"value\": 1.0\n }\n }\n]", + "description": "Decision: Behaalbare Hoogte Subsidie\nDecision table: DecisionTable_BehaalbareHoogteSubsidie\nExpected: hoogteSubsidie=1.0\nCoverage reasons:\n- MC/DC: rule 2 Rule_Behaalbare_BeperktDoorResterendBudget, condition [basisHoogteSubsidie > beschikbaarSubsidiePlafond] flipped" + }, + { + "name": "TC_018 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"basisHoogteSubsidie\": {\n \"value\": 750,\n \"type\": \"Double\"\n },\n \"beschikbaarSubsidiePlafond\": {\n \"value\": 1249,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BehaalbareHoogteSubsidie/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BehaalbareHoogteSubsidie", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"hoogteSubsidie\": {\n \"type\": \"Double\",\n \"value\": 750.0\n }\n }\n]", + "description": "Decision: Behaalbare Hoogte Subsidie\nDecision table: DecisionTable_BehaalbareHoogteSubsidie\nExpected: hoogteSubsidie=750.0\nCoverage reasons:\n- Boundary/domain value: basisHoogteSubsidie=750" + }, + { + "name": "TC_019 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"basisHoogteSubsidie\": {\n \"value\": 751,\n \"type\": \"Double\"\n },\n \"beschikbaarSubsidiePlafond\": {\n \"value\": 1249,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BehaalbareHoogteSubsidie/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BehaalbareHoogteSubsidie", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"hoogteSubsidie\": {\n \"type\": \"Double\",\n \"value\": 751.0\n }\n }\n]", + "description": "Decision: Behaalbare Hoogte Subsidie\nDecision table: DecisionTable_BehaalbareHoogteSubsidie\nExpected: hoogteSubsidie=751.0\nCoverage reasons:\n- Boundary/domain value: basisHoogteSubsidie=751" + } + ], + "description": "Generated MC/DC and boundary-value examples for `Behaalbare Hoogte Subsidie`. Each example contains the request body for one generated test case and an expected Operaton/Camunda-style decision-evaluation response body." + }, + { + "name": "BerekenBasisHoogteSubsidie - MC/DC examples (8)", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"gemaakteKosten\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"minimaleNoodzakelijkeKosten\": {\n \"value\": 750,\n \"type\": \"Double\"\n },\n \"subsidieMaximum\": {\n \"value\": 1250,\n \"type\": \"Double\"\n },\n \"subsidieMinimum\": {\n \"value\": 750,\n \"type\": \"Double\"\n },\n \"subsidiePercentage\": {\n \"value\": 0.25,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBasisHoogteSubsidie/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBasisHoogteSubsidie", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "response": [ + { + "name": "TC_020 Bereken Basis Hoogte Subsidie - MC/DC - Rule_Basis_OnderMinimum", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"gemaakteKosten\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"minimaleNoodzakelijkeKosten\": {\n \"value\": 750,\n \"type\": \"Double\"\n },\n \"subsidieMaximum\": {\n \"value\": 1250,\n \"type\": \"Double\"\n },\n \"subsidieMinimum\": {\n \"value\": 750,\n \"type\": \"Double\"\n },\n \"subsidiePercentage\": {\n \"value\": 0.25,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBasisHoogteSubsidie/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBasisHoogteSubsidie", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"basisHoogteSubsidie\": {\n \"type\": \"Double\",\n \"value\": 0.0\n }\n }\n]", + "description": "Decision: Bereken Basis Hoogte Subsidie\nDecision table: DecisionTable_BerekenBasisHoogteSubsidie\nExpected: basisHoogteSubsidie=0.0\nCoverage reasons:\n- MC/DC: rule 1 Rule_Basis_BovenMinimum, condition [gemaakteKosten >= minimaleNoodzakelijkeKosten / subsidiePercentage] baseline\n- MC/DC: rule 2 Rule_Basis_OnderMinimum, condition [gemaakteKosten < minimaleNoodzakelijkeKosten / subsidiePercentage] flipped\n- Boundary/domain value: gemaakteKosten=0\n- Representative selected rule 2: Rule_Basis_OnderMinimum\n- Output minimum: basisHoogteSubsidie=0\n- Output near zero boundary: basisHoogteSubsidie=0" + }, + { + "name": "TC_021 Bereken Basis Hoogte Subsidie - Boundary/domain value - Rule_Basis_BovenMinimum", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"gemaakteKosten\": {\n \"value\": 10000,\n \"type\": \"Double\"\n },\n \"minimaleNoodzakelijkeKosten\": {\n \"value\": 750,\n \"type\": \"Double\"\n },\n \"subsidieMaximum\": {\n \"value\": 1250,\n \"type\": \"Double\"\n },\n \"subsidieMinimum\": {\n \"value\": 750,\n \"type\": \"Double\"\n },\n \"subsidiePercentage\": {\n \"value\": 0.25,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBasisHoogteSubsidie/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBasisHoogteSubsidie", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"basisHoogteSubsidie\": {\n \"type\": \"Double\",\n \"value\": 1250.0\n }\n }\n]", + "description": "Decision: Bereken Basis Hoogte Subsidie\nDecision table: DecisionTable_BerekenBasisHoogteSubsidie\nExpected: basisHoogteSubsidie=1250.0\nCoverage reasons:\n- Boundary/domain value: gemaakteKosten=10000\n- Boundary/domain value: minimaleNoodzakelijkeKosten=750\n- Boundary/domain value: subsidieMaximum=1250\n- Boundary/domain value: subsidieMinimum=750\n- Boundary/domain value: subsidiePercentage=0.25\n- Representative selected rule 1: Rule_Basis_BovenMinimum\n- Output maximum: basisHoogteSubsidie=1250" + }, + { + "name": "TC_022 Bereken Basis Hoogte Subsidie - Boundary/domain value - Rule_Basis_OnderMinimum", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"gemaakteKosten\": {\n \"value\": 2999,\n \"type\": \"Double\"\n },\n \"minimaleNoodzakelijkeKosten\": {\n \"value\": 750,\n \"type\": \"Double\"\n },\n \"subsidieMaximum\": {\n \"value\": 1250,\n \"type\": \"Double\"\n },\n \"subsidieMinimum\": {\n \"value\": 750,\n \"type\": \"Double\"\n },\n \"subsidiePercentage\": {\n \"value\": 0.25,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBasisHoogteSubsidie/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBasisHoogteSubsidie", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"basisHoogteSubsidie\": {\n \"type\": \"Double\",\n \"value\": 0.0\n }\n }\n]", + "description": "Decision: Bereken Basis Hoogte Subsidie\nDecision table: DecisionTable_BerekenBasisHoogteSubsidie\nExpected: basisHoogteSubsidie=0.0\nCoverage reasons:\n- Boundary/domain value: gemaakteKosten=2999" + }, + { + "name": "TC_023 Bereken Basis Hoogte Subsidie - MC/DC - Rule_Basis_BovenMinimum", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"gemaakteKosten\": {\n \"value\": 3000,\n \"type\": \"Double\"\n },\n \"minimaleNoodzakelijkeKosten\": {\n \"value\": 750,\n \"type\": \"Double\"\n },\n \"subsidieMaximum\": {\n \"value\": 1250,\n \"type\": \"Double\"\n },\n \"subsidieMinimum\": {\n \"value\": 750,\n \"type\": \"Double\"\n },\n \"subsidiePercentage\": {\n \"value\": 0.25,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBasisHoogteSubsidie/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBasisHoogteSubsidie", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"basisHoogteSubsidie\": {\n \"type\": \"Double\",\n \"value\": 750.0\n }\n }\n]", + "description": "Decision: Bereken Basis Hoogte Subsidie\nDecision table: DecisionTable_BerekenBasisHoogteSubsidie\nExpected: basisHoogteSubsidie=750.0\nCoverage reasons:\n- MC/DC: rule 1 Rule_Basis_BovenMinimum, condition [gemaakteKosten >= minimaleNoodzakelijkeKosten / subsidiePercentage] flipped\n- MC/DC: rule 2 Rule_Basis_OnderMinimum, condition [gemaakteKosten < minimaleNoodzakelijkeKosten / subsidiePercentage] baseline\n- Boundary/domain value: gemaakteKosten=3000" + }, + { + "name": "TC_024 Bereken Basis Hoogte Subsidie - Boundary/domain value - Rule_Basis_BovenMinimum", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"gemaakteKosten\": {\n \"value\": 3001,\n \"type\": \"Double\"\n },\n \"minimaleNoodzakelijkeKosten\": {\n \"value\": 750,\n \"type\": \"Double\"\n },\n \"subsidieMaximum\": {\n \"value\": 1250,\n \"type\": \"Double\"\n },\n \"subsidieMinimum\": {\n \"value\": 750,\n \"type\": \"Double\"\n },\n \"subsidiePercentage\": {\n \"value\": 0.25,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBasisHoogteSubsidie/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBasisHoogteSubsidie", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"basisHoogteSubsidie\": {\n \"type\": \"Double\",\n \"value\": 750.25\n }\n }\n]", + "description": "Decision: Bereken Basis Hoogte Subsidie\nDecision table: DecisionTable_BerekenBasisHoogteSubsidie\nExpected: basisHoogteSubsidie=750.25\nCoverage reasons:\n- Boundary/domain value: gemaakteKosten=3001" + }, + { + "name": "TC_025 Bereken Basis Hoogte Subsidie - Boundary/domain value - Rule_Basis_BovenMinimum", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"gemaakteKosten\": {\n \"value\": 4999,\n \"type\": \"Double\"\n },\n \"minimaleNoodzakelijkeKosten\": {\n \"value\": 750,\n \"type\": \"Double\"\n },\n \"subsidieMaximum\": {\n \"value\": 1250,\n \"type\": \"Double\"\n },\n \"subsidieMinimum\": {\n \"value\": 750,\n \"type\": \"Double\"\n },\n \"subsidiePercentage\": {\n \"value\": 0.25,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBasisHoogteSubsidie/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBasisHoogteSubsidie", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"basisHoogteSubsidie\": {\n \"type\": \"Double\",\n \"value\": 1249.75\n }\n }\n]", + "description": "Decision: Bereken Basis Hoogte Subsidie\nDecision table: DecisionTable_BerekenBasisHoogteSubsidie\nExpected: basisHoogteSubsidie=1249.75\nCoverage reasons:\n- Boundary/domain value: gemaakteKosten=4999" + }, + { + "name": "TC_026 Bereken Basis Hoogte Subsidie - Boundary/domain value - Rule_Basis_BovenMinimum", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"gemaakteKosten\": {\n \"value\": 5000,\n \"type\": \"Double\"\n },\n \"minimaleNoodzakelijkeKosten\": {\n \"value\": 750,\n \"type\": \"Double\"\n },\n \"subsidieMaximum\": {\n \"value\": 1250,\n \"type\": \"Double\"\n },\n \"subsidieMinimum\": {\n \"value\": 750,\n \"type\": \"Double\"\n },\n \"subsidiePercentage\": {\n \"value\": 0.25,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBasisHoogteSubsidie/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBasisHoogteSubsidie", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"basisHoogteSubsidie\": {\n \"type\": \"Double\",\n \"value\": 1250.0\n }\n }\n]", + "description": "Decision: Bereken Basis Hoogte Subsidie\nDecision table: DecisionTable_BerekenBasisHoogteSubsidie\nExpected: basisHoogteSubsidie=1250.0\nCoverage reasons:\n- Boundary/domain value: gemaakteKosten=5000" + }, + { + "name": "TC_027 Bereken Basis Hoogte Subsidie - Boundary/domain value - Rule_Basis_BovenMinimum", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"gemaakteKosten\": {\n \"value\": 5001,\n \"type\": \"Double\"\n },\n \"minimaleNoodzakelijkeKosten\": {\n \"value\": 750,\n \"type\": \"Double\"\n },\n \"subsidieMaximum\": {\n \"value\": 1250,\n \"type\": \"Double\"\n },\n \"subsidieMinimum\": {\n \"value\": 750,\n \"type\": \"Double\"\n },\n \"subsidiePercentage\": {\n \"value\": 0.25,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBasisHoogteSubsidie/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBasisHoogteSubsidie", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"basisHoogteSubsidie\": {\n \"type\": \"Double\",\n \"value\": 1250.0\n }\n }\n]", + "description": "Decision: Bereken Basis Hoogte Subsidie\nDecision table: DecisionTable_BerekenBasisHoogteSubsidie\nExpected: basisHoogteSubsidie=1250.0\nCoverage reasons:\n- Boundary/domain value: gemaakteKosten=5001" + } + ], + "description": "Generated MC/DC and boundary-value examples for `Bereken Basis Hoogte Subsidie`. Each example contains the request body for one generated test case and an expected Operaton/Camunda-style decision-evaluation response body." + }, + { + "name": "BerekenBeschikbaarSubsidiePlafond - MC/DC examples (64)", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2025-12-31\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "response": [ + { + "name": "TC_028 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_BuitenAanvraagperiodeOfOnbekendType", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2025-12-31\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 0.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=0.0\nCoverage reasons:\n- Boundary/domain value: aanvraagDatum='2025-12-31'\n- Representative selected rule 7: Rule_Platform_BuitenAanvraagperiodeOfOnbekendType" + }, + { + "name": "TC_029 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_BuitenAanvraagperiodeOfOnbekendType", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2025-12-31\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 437500,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 437500,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 0.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=0.0\nCoverage reasons:\n- MC/DC: rule 1 Rule_Platform_2026_Eigenaar_Gescheiden, condition [date(aanvraagDatum) >= date(\"2026-01-12\")] baseline\n- MC/DC: rule 3 Rule_Platform_2026_Gebundeld, condition [date(aanvraagDatum) >= date(\"2026-10-01\")] baseline\n- MC/DC: rule 4 Rule_Platform_2027_Eigenaar_Gescheiden, condition [date(\"2027-01-01\") <= date(aanvraagDatum)] baseline\n- MC/DC: rule 6 Rule_Platform_2027_Gebundeld, condition [date(aanvraagDatum) >= date(\"2027-10-01\")] baseline\n- Output minimum: beschikbaarSubsidiePlafond=0\n- Output near zero boundary: beschikbaarSubsidiePlafond=0" + }, + { + "name": "TC_030 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_BuitenAanvraagperiodeOfOnbekendType", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2025-12-31\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"huurder\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 437500,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 437500,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 0.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=0.0\nCoverage reasons:\n- MC/DC: rule 2 Rule_Platform_2026_Huurder_Gescheiden, condition [date(aanvraagDatum) >= date(\"2026-01-12\")] baseline\n- MC/DC: rule 5 Rule_Platform_2027_Huurder_Gescheiden, condition [date(aanvraagDatum) >= date(\"2027-01-01\")] baseline" + }, + { + "name": "TC_031 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_BuitenAanvraagperiodeOfOnbekendType", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-01-11\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 0.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=0.0\nCoverage reasons:\n- Boundary/domain value: aanvraagDatum='2026-01-11'" + }, + { + "name": "TC_032 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Eigenaar_Gescheiden", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-01-12\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1000000.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1000000.0\nCoverage reasons:\n- Boundary/domain value: aanvraagDatum='2026-01-12'\n- Representative selected rule 1: Rule_Platform_2026_Eigenaar_Gescheiden" + }, + { + "name": "TC_033 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Eigenaar_Gescheiden", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-01-12\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1000000.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1000000.0\nCoverage reasons:\n- Boundary/domain value: reedsGesubsidieerdHuurders=1000000" + }, + { + "name": "TC_034 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Eigenaar_Gescheiden", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-01-12\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 1000001,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1000000.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1000000.0\nCoverage reasons:\n- Boundary/domain value: reedsGesubsidieerdHuurders=1000001" + }, + { + "name": "TC_035 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_2026_Eigenaar_Gescheiden", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-01-12\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 437500,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 437500,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 437500.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=437500.0\nCoverage reasons:\n- MC/DC: rule 1 Rule_Platform_2026_Eigenaar_Gescheiden, condition [date(aanvraagDatum) >= date(\"2026-01-12\")] flipped\n- MC/DC: rule 1 Rule_Platform_2026_Eigenaar_Gescheiden, condition [date(aanvraagDatum) <= date(\"2026-09-30\")] flipped" + }, + { + "name": "TC_036 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_2026_Eigenaar_Gescheiden", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-01-12\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 437500,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 437500,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 1,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 437500.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=437500.0\nCoverage reasons:\n- MC/DC: rule 1 Rule_Platform_2026_Eigenaar_Gescheiden, condition [aanvragerType == \"eigenaar\"] flipped\n- MC/DC: rule 2 Rule_Platform_2026_Huurder_Gescheiden, condition [aanvragerType == \"huurder\"] baseline" + }, + { + "name": "TC_037 Bereken Beschikbaar Subsidie Plafond - Representative selected rule 2 - Rule_Platform_2026_Huurder_Gescheiden", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-01-12\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"huurder\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1000000.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1000000.0\nCoverage reasons:\n- Representative selected rule 2: Rule_Platform_2026_Huurder_Gescheiden" + }, + { + "name": "TC_038 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Huurder_Gescheiden", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-01-12\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"huurder\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1000000.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1000000.0\nCoverage reasons:\n- Boundary/domain value: reedsGesubsidieerdEigenaren=1000000" + }, + { + "name": "TC_039 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Huurder_Gescheiden", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-01-12\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"huurder\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 1000001,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1000000.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1000000.0\nCoverage reasons:\n- Boundary/domain value: reedsGesubsidieerdEigenaren=1000001" + }, + { + "name": "TC_040 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_2026_Huurder_Gescheiden", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-01-12\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"huurder\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 437500,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 437500,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 437500.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=437500.0\nCoverage reasons:\n- MC/DC: rule 2 Rule_Platform_2026_Huurder_Gescheiden, condition [date(aanvraagDatum) >= date(\"2026-01-12\")] flipped\n- MC/DC: rule 2 Rule_Platform_2026_Huurder_Gescheiden, condition [date(aanvraagDatum) <= date(\"2026-09-30\")] flipped" + }, + { + "name": "TC_041 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_2026_Huurder_Gescheiden", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-01-12\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"huurder\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 437500,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 437500,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 1,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 437499.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=437499.0\nCoverage reasons:\n- MC/DC: rule 1 Rule_Platform_2026_Eigenaar_Gescheiden, condition [aanvragerType == \"eigenaar\"] baseline\n- MC/DC: rule 2 Rule_Platform_2026_Huurder_Gescheiden, condition [aanvragerType == \"huurder\"] flipped" + }, + { + "name": "TC_042 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Eigenaar_Gescheiden", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-01-13\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1000000.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1000000.0\nCoverage reasons:\n- Boundary/domain value: aanvraagDatum='2026-01-13'" + }, + { + "name": "TC_043 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Eigenaar_Gescheiden", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-09-29\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1000000.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1000000.0\nCoverage reasons:\n- Boundary/domain value: aanvraagDatum='2026-09-29'" + }, + { + "name": "TC_044 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Eigenaar_Gescheiden", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-09-30\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1000000.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1000000.0\nCoverage reasons:\n- Boundary/domain value: aanvraagDatum='2026-09-30'" + }, + { + "name": "TC_045 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-10-01\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 2000000.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=2000000.0\nCoverage reasons:\n- Boundary/domain value: aanvraagDatum='2026-10-01'\n- Boundary/domain value: aanvragerType='eigenaar'\n- Boundary/domain value: plafondEigenaren=1000000\n- Boundary/domain value: plafondHuurders=1000000\n- Boundary/domain value: reedsGesubsidieerdEigenaren=0\n- Boundary/domain value: reedsGesubsidieerdHuurders=0\n- Representative selected rule 3: Rule_Platform_2026_Gebundeld" + }, + { + "name": "TC_046 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-10-01\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 1,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1999999.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1999999.0\nCoverage reasons:\n- Boundary/domain value: reedsGesubsidieerdHuurders=1" + }, + { + "name": "TC_047 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-10-01\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 437499,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1562501.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1562501.0\nCoverage reasons:\n- Boundary/domain value: reedsGesubsidieerdHuurders=437499" + }, + { + "name": "TC_048 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-10-01\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 437500,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1562500.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1562500.0\nCoverage reasons:\n- Boundary/domain value: reedsGesubsidieerdHuurders=437500" + }, + { + "name": "TC_049 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-10-01\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 437501,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1562499.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1562499.0\nCoverage reasons:\n- Boundary/domain value: reedsGesubsidieerdHuurders=437501" + }, + { + "name": "TC_050 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-10-01\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 874999,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1125001.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1125001.0\nCoverage reasons:\n- Boundary/domain value: reedsGesubsidieerdHuurders=874999" + }, + { + "name": "TC_051 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-10-01\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 875000,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1125000.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1125000.0\nCoverage reasons:\n- Boundary/domain value: reedsGesubsidieerdHuurders=875000" + }, + { + "name": "TC_052 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-10-01\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 875001,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1124999.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1124999.0\nCoverage reasons:\n- Boundary/domain value: reedsGesubsidieerdHuurders=875001" + }, + { + "name": "TC_053 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-10-01\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 999999,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1000001.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1000001.0\nCoverage reasons:\n- Boundary/domain value: reedsGesubsidieerdHuurders=999999" + }, + { + "name": "TC_054 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-10-01\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 1,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1999999.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1999999.0\nCoverage reasons:\n- Boundary/domain value: reedsGesubsidieerdEigenaren=1" + }, + { + "name": "TC_055 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-10-01\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 437499,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1562501.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1562501.0\nCoverage reasons:\n- Boundary/domain value: reedsGesubsidieerdEigenaren=437499" + }, + { + "name": "TC_056 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-10-01\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 437500,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1562500.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1562500.0\nCoverage reasons:\n- Boundary/domain value: reedsGesubsidieerdEigenaren=437500" + }, + { + "name": "TC_057 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-10-01\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 437501,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1562499.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1562499.0\nCoverage reasons:\n- Boundary/domain value: reedsGesubsidieerdEigenaren=437501" + }, + { + "name": "TC_058 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-10-01\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 874999,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1125001.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1125001.0\nCoverage reasons:\n- Boundary/domain value: reedsGesubsidieerdEigenaren=874999" + }, + { + "name": "TC_059 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-10-01\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 875000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1125000.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1125000.0\nCoverage reasons:\n- Boundary/domain value: reedsGesubsidieerdEigenaren=875000" + }, + { + "name": "TC_060 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-10-01\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 875001,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1124999.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1124999.0\nCoverage reasons:\n- Boundary/domain value: reedsGesubsidieerdEigenaren=875001" + }, + { + "name": "TC_061 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-10-01\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 999999,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1000001.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1000001.0\nCoverage reasons:\n- Boundary/domain value: reedsGesubsidieerdEigenaren=999999" + }, + { + "name": "TC_062 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-10-01\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 437500,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1437500.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1437500.0\nCoverage reasons:\n- Boundary/domain value: plafondHuurders=437500" + }, + { + "name": "TC_063 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-10-01\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 875000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1875000.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1875000.0\nCoverage reasons:\n- Boundary/domain value: plafondHuurders=875000" + }, + { + "name": "TC_064 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-10-01\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 437500,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1437500.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1437500.0\nCoverage reasons:\n- Boundary/domain value: plafondEigenaren=437500" + }, + { + "name": "TC_065 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_2026_Gebundeld", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-10-01\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 437500,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 437500,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 875000.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=875000.0\nCoverage reasons:\n- MC/DC: rule 1 Rule_Platform_2026_Eigenaar_Gescheiden, condition [date(aanvraagDatum) <= date(\"2026-09-30\")] baseline\n- MC/DC: rule 3 Rule_Platform_2026_Gebundeld, condition [date(aanvraagDatum) >= date(\"2026-10-01\")] flipped\n- MC/DC: rule 3 Rule_Platform_2026_Gebundeld, condition [date(aanvraagDatum) <= date(\"2026-12-31\")] flipped" + }, + { + "name": "TC_066 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-10-01\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 875000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1875000.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1875000.0\nCoverage reasons:\n- Boundary/domain value: plafondEigenaren=875000" + }, + { + "name": "TC_067 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-10-01\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"huurder\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 2000000.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=2000000.0\nCoverage reasons:\n- Boundary/domain value: aanvragerType='huurder'" + }, + { + "name": "TC_068 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_2026_Gebundeld", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-10-01\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"huurder\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 437500,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 437500,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 875000.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=875000.0\nCoverage reasons:\n- MC/DC: rule 2 Rule_Platform_2026_Huurder_Gescheiden, condition [date(aanvraagDatum) <= date(\"2026-09-30\")] baseline" + }, + { + "name": "TC_069 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-10-01\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"onbekend\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 2000000.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=2000000.0\nCoverage reasons:\n- Boundary/domain value: aanvragerType='onbekend'" + }, + { + "name": "TC_070 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-10-02\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 2000000.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=2000000.0\nCoverage reasons:\n- Boundary/domain value: aanvraagDatum='2026-10-02'" + }, + { + "name": "TC_071 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-12-30\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 2000000.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=2000000.0\nCoverage reasons:\n- Boundary/domain value: aanvraagDatum='2026-12-30'" + }, + { + "name": "TC_072 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-12-31\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 2000000.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=2000000.0\nCoverage reasons:\n- Boundary/domain value: aanvraagDatum='2026-12-31'" + }, + { + "name": "TC_073 Bereken Beschikbaar Subsidie Plafond - Output maximum - Rule_Platform_2026_Gebundeld", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-12-31\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"onbekend\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 2000000.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=2000000.0\nCoverage reasons:\n- Output maximum: beschikbaarSubsidiePlafond=2000000" + }, + { + "name": "TC_074 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2027_Eigenaar_Gescheiden", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2027-01-01\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1000000.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1000000.0\nCoverage reasons:\n- Boundary/domain value: aanvraagDatum='2027-01-01'\n- Representative selected rule 4: Rule_Platform_2027_Eigenaar_Gescheiden" + }, + { + "name": "TC_075 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_2027_Eigenaar_Gescheiden", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2027-01-01\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 437500,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 437500,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 437500.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=437500.0\nCoverage reasons:\n- MC/DC: rule 3 Rule_Platform_2026_Gebundeld, condition [date(aanvraagDatum) <= date(\"2026-12-31\")] baseline\n- MC/DC: rule 4 Rule_Platform_2027_Eigenaar_Gescheiden, condition [date(\"2027-01-01\") <= date(aanvraagDatum)] flipped\n- MC/DC: rule 4 Rule_Platform_2027_Eigenaar_Gescheiden, condition [date(aanvraagDatum) <= date(\"2027-09-30\")] flipped" + }, + { + "name": "TC_076 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_2027_Eigenaar_Gescheiden", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2027-01-01\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 437500,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 437500,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 1,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 437500.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=437500.0\nCoverage reasons:\n- MC/DC: rule 4 Rule_Platform_2027_Eigenaar_Gescheiden, condition [aanvragerType == \"eigenaar\"] flipped\n- MC/DC: rule 5 Rule_Platform_2027_Huurder_Gescheiden, condition [aanvragerType == \"huurder\"] baseline" + }, + { + "name": "TC_077 Bereken Beschikbaar Subsidie Plafond - Representative selected rule 5 - Rule_Platform_2027_Huurder_Gescheiden", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2027-01-01\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"huurder\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1000000.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1000000.0\nCoverage reasons:\n- Representative selected rule 5: Rule_Platform_2027_Huurder_Gescheiden" + }, + { + "name": "TC_078 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_2027_Huurder_Gescheiden", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2027-01-01\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"huurder\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 437500,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 437500,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 437500.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=437500.0\nCoverage reasons:\n- MC/DC: rule 5 Rule_Platform_2027_Huurder_Gescheiden, condition [date(aanvraagDatum) >= date(\"2027-01-01\")] flipped\n- MC/DC: rule 5 Rule_Platform_2027_Huurder_Gescheiden, condition [date(aanvraagDatum) <= date(\"2027-09-30\")] flipped" + }, + { + "name": "TC_079 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_2027_Huurder_Gescheiden", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2027-01-01\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"huurder\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 437500,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 437500,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 1,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 437499.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=437499.0\nCoverage reasons:\n- MC/DC: rule 4 Rule_Platform_2027_Eigenaar_Gescheiden, condition [aanvragerType == \"eigenaar\"] baseline\n- MC/DC: rule 5 Rule_Platform_2027_Huurder_Gescheiden, condition [aanvragerType == \"huurder\"] flipped" + }, + { + "name": "TC_080 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2027_Eigenaar_Gescheiden", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2027-01-02\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1000000.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1000000.0\nCoverage reasons:\n- Boundary/domain value: aanvraagDatum='2027-01-02'" + }, + { + "name": "TC_081 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2027_Eigenaar_Gescheiden", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2027-09-29\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1000000.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1000000.0\nCoverage reasons:\n- Boundary/domain value: aanvraagDatum='2027-09-29'" + }, + { + "name": "TC_082 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2027_Eigenaar_Gescheiden", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2027-09-30\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1000000.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1000000.0\nCoverage reasons:\n- Boundary/domain value: aanvraagDatum='2027-09-30'" + }, + { + "name": "TC_083 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2027_Gebundeld", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2027-10-01\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1000000.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1000000.0\nCoverage reasons:\n- Boundary/domain value: aanvraagDatum='2027-10-01'\n- Representative selected rule 6: Rule_Platform_2027_Gebundeld" + }, + { + "name": "TC_084 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_2027_Gebundeld", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2027-10-01\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 437500,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 437500,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1000000.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1000000.0\nCoverage reasons:\n- MC/DC: rule 4 Rule_Platform_2027_Eigenaar_Gescheiden, condition [date(aanvraagDatum) <= date(\"2027-09-30\")] baseline\n- MC/DC: rule 6 Rule_Platform_2027_Gebundeld, condition [date(aanvraagDatum) >= date(\"2027-10-01\")] flipped\n- MC/DC: rule 6 Rule_Platform_2027_Gebundeld, condition [date(aanvraagDatum) <= date(\"2027-12-31\")] flipped" + }, + { + "name": "TC_085 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_2027_Gebundeld", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2027-10-01\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"huurder\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 437500,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 437500,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1000000.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1000000.0\nCoverage reasons:\n- MC/DC: rule 5 Rule_Platform_2027_Huurder_Gescheiden, condition [date(aanvraagDatum) <= date(\"2027-09-30\")] baseline" + }, + { + "name": "TC_086 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2027_Gebundeld", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2027-10-02\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1000000.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1000000.0\nCoverage reasons:\n- Boundary/domain value: aanvraagDatum='2027-10-02'" + }, + { + "name": "TC_087 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2027_Gebundeld", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2027-12-30\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1000000.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1000000.0\nCoverage reasons:\n- Boundary/domain value: aanvraagDatum='2027-12-30'" + }, + { + "name": "TC_088 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2027_Gebundeld", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2027-12-31\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 1000000.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=1000000.0\nCoverage reasons:\n- Boundary/domain value: aanvraagDatum='2027-12-31'" + }, + { + "name": "TC_089 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_BuitenAanvraagperiodeOfOnbekendType", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2028-01-01\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 0.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=0.0\nCoverage reasons:\n- Boundary/domain value: aanvraagDatum='2028-01-01'" + }, + { + "name": "TC_090 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_BuitenAanvraagperiodeOfOnbekendType", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2028-01-01\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 437500,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 437500,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 0.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=0.0\nCoverage reasons:\n- MC/DC: rule 6 Rule_Platform_2027_Gebundeld, condition [date(aanvraagDatum) <= date(\"2027-12-31\")] baseline" + }, + { + "name": "TC_091 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_BuitenAanvraagperiodeOfOnbekendType", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2029-01-01\",\n \"type\": \"String\"\n },\n \"aanvragerType\": {\n \"value\": \"eigenaar\",\n \"type\": \"String\"\n },\n \"plafondEigenaren\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"plafondHuurders\": {\n \"value\": 1000000,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdEigenaren\": {\n \"value\": 0,\n \"type\": \"Double\"\n },\n \"reedsGesubsidieerdHuurders\": {\n \"value\": 0,\n \"type\": \"Double\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/BerekenBeschikbaarSubsidiePlafond/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "BerekenBeschikbaarSubsidiePlafond", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"beschikbaarSubsidiePlafond\": {\n \"type\": \"Double\",\n \"value\": 0.0\n }\n }\n]", + "description": "Decision: Bereken Beschikbaar Subsidie Plafond\nDecision table: DecisionTable_BerekenBeschikbaarSubsidiePlafond\nExpected: beschikbaarSubsidiePlafond=0.0\nCoverage reasons:\n- Boundary/domain value: aanvraagDatum='2029-01-01'" + } + ], + "description": "Generated MC/DC and boundary-value examples for `Bereken Beschikbaar Subsidie Plafond`. Each example contains the request body for one generated test case and an expected Operaton/Camunda-style decision-evaluation response body." + }, + { + "name": "SubsidieConstantenThuisbatterij - MC/DC examples (1)", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/SubsidieConstantenThuisbatterij/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "SubsidieConstantenThuisbatterij", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "response": [ + { + "name": "TC_092 Subsidie Constanten Thuisbatterij - Representative selected rule 1 - Rule_Constanten_Standaard", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/SubsidieConstantenThuisbatterij/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "SubsidieConstantenThuisbatterij", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"subsidiePercentage\": {\n \"type\": \"Double\",\n \"value\": 0.25\n },\n \"subsidieMinimum\": {\n \"type\": \"Double\",\n \"value\": 750.0\n },\n \"subsidieMaximum\": {\n \"type\": \"Double\",\n \"value\": 1250.0\n },\n \"minimaleNoodzakelijkeKosten\": {\n \"type\": \"Double\",\n \"value\": 750.0\n }\n }\n]", + "description": "Decision: Subsidie Constanten Thuisbatterij\nDecision table: DecisionTable_SubsidieConstantenThuisbatterij\nExpected: subsidiePercentage=0.25, subsidieMinimum=750.0, subsidieMaximum=1250.0, minimaleNoodzakelijkeKosten=750.0\nCoverage reasons:\n- Representative selected rule 1: Rule_Constanten_Standaard\n- Output minimum: subsidiePercentage=0.25\n- Output maximum: subsidiePercentage=0.25\n- Output near zero boundary: subsidiePercentage=0.25\n- Output minimum: subsidieMinimum=750\n- Output maximum: subsidieMinimum=750\n- Output near zero boundary: subsidieMinimum=750\n- Output minimum: subsidieMaximum=1250\n- Output maximum: subsidieMaximum=1250\n- Output near zero boundary: subsidieMaximum=1250\n- Output minimum: minimaleNoodzakelijkeKosten=750\n- Output maximum: minimaleNoodzakelijkeKosten=750\n- Output near zero boundary: minimaleNoodzakelijkeKosten=750" + } + ], + "description": "Generated MC/DC and boundary-value examples for `Subsidie Constanten Thuisbatterij`. Each example contains the request body for one generated test case and an expected Operaton/Camunda-style decision-evaluation response body." + }, + { + "name": "jaarGebondenBudget - MC/DC examples (6)", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2025-12-31\",\n \"type\": \"String\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/jaarGebondenBudget/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "jaarGebondenBudget", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "response": [ + { + "name": "TC_093 Jaar Gebonden Budget - MC/DC - no matching rule", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2025-12-31\",\n \"type\": \"String\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/jaarGebondenBudget/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "jaarGebondenBudget", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"plafondEigenaren\": {\n \"type\": \"Null\",\n \"value\": null\n },\n \"plafondHuurders\": {\n \"type\": \"Null\",\n \"value\": null\n }\n }\n]", + "description": "Decision: Jaar Gebonden Budget\nDecision table: DecisionTable_1bhjh1w\nExpected: plafondEigenaren=null, plafondHuurders=null\nCoverage reasons:\n- MC/DC: rule 1 DecisionRule_0nfr6kl, condition [date(aanvraagDatum).year == 2026] baseline\n- MC/DC: rule 2 DecisionRule_1e5agpr, condition [date(aanvraagDatum).year == 2027] baseline\n- Boundary/domain value: aanvraagDatum='2025-12-31'" + }, + { + "name": "TC_094 Jaar Gebonden Budget - MC/DC - DecisionRule_0nfr6kl", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-01-01\",\n \"type\": \"String\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/jaarGebondenBudget/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "jaarGebondenBudget", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"plafondEigenaren\": {\n \"type\": \"Double\",\n \"value\": 437500.0\n },\n \"plafondHuurders\": {\n \"type\": \"Double\",\n \"value\": 437500.0\n }\n }\n]", + "description": "Decision: Jaar Gebonden Budget\nDecision table: DecisionTable_1bhjh1w\nExpected: plafondEigenaren=437500.0, plafondHuurders=437500.0\nCoverage reasons:\n- MC/DC: rule 1 DecisionRule_0nfr6kl, condition [date(aanvraagDatum).year == 2026] flipped\n- Boundary/domain value: aanvraagDatum='2026-01-01'\n- Representative selected rule 1: DecisionRule_0nfr6kl\n- Output minimum: plafondEigenaren=437500\n- Output near zero boundary: plafondEigenaren=437500\n- Output minimum: plafondHuurders=437500\n- Output near zero boundary: plafondHuurders=437500" + }, + { + "name": "TC_095 Jaar Gebonden Budget - Boundary/domain value - DecisionRule_0nfr6kl", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2026-12-31\",\n \"type\": \"String\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/jaarGebondenBudget/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "jaarGebondenBudget", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"plafondEigenaren\": {\n \"type\": \"Double\",\n \"value\": 437500.0\n },\n \"plafondHuurders\": {\n \"type\": \"Double\",\n \"value\": 437500.0\n }\n }\n]", + "description": "Decision: Jaar Gebonden Budget\nDecision table: DecisionTable_1bhjh1w\nExpected: plafondEigenaren=437500.0, plafondHuurders=437500.0\nCoverage reasons:\n- Boundary/domain value: aanvraagDatum='2026-12-31'" + }, + { + "name": "TC_096 Jaar Gebonden Budget - MC/DC - DecisionRule_1e5agpr", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2027-01-01\",\n \"type\": \"String\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/jaarGebondenBudget/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "jaarGebondenBudget", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"plafondEigenaren\": {\n \"type\": \"Double\",\n \"value\": 437500.0\n },\n \"plafondHuurders\": {\n \"type\": \"Double\",\n \"value\": 437500.0\n }\n }\n]", + "description": "Decision: Jaar Gebonden Budget\nDecision table: DecisionTable_1bhjh1w\nExpected: plafondEigenaren=437500.0, plafondHuurders=437500.0\nCoverage reasons:\n- MC/DC: rule 2 DecisionRule_1e5agpr, condition [date(aanvraagDatum).year == 2027] flipped\n- Boundary/domain value: aanvraagDatum='2027-01-01'\n- Representative selected rule 2: DecisionRule_1e5agpr" + }, + { + "name": "TC_097 Jaar Gebonden Budget - Boundary/domain value - DecisionRule_1e5agpr", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2027-12-31\",\n \"type\": \"String\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/jaarGebondenBudget/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "jaarGebondenBudget", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"plafondEigenaren\": {\n \"type\": \"Double\",\n \"value\": 437500.0\n },\n \"plafondHuurders\": {\n \"type\": \"Double\",\n \"value\": 437500.0\n }\n }\n]", + "description": "Decision: Jaar Gebonden Budget\nDecision table: DecisionTable_1bhjh1w\nExpected: plafondEigenaren=437500.0, plafondHuurders=437500.0\nCoverage reasons:\n- Boundary/domain value: aanvraagDatum='2027-12-31'\n- Output maximum: plafondEigenaren=437500\n- Output maximum: plafondHuurders=437500" + }, + { + "name": "TC_098 Jaar Gebonden Budget - Boundary/domain value - no matching rule", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"variables\": {\n \"aanvraagDatum\": {\n \"value\": \"2028-01-01\",\n \"type\": \"String\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://operaton.open-regels.nl/engine-rest/decision-definition/key/jaarGebondenBudget/tenant-id/46/evaluate", + "protocol": "https", + "host": [ + "operaton", + "open-regels", + "nl" + ], + "path": [ + "engine-rest", + "decision-definition", + "key", + "jaarGebondenBudget", + "tenant-id", + "46", + "evaluate" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "cookie": [], + "body": "[\n {\n \"plafondEigenaren\": {\n \"type\": \"Null\",\n \"value\": null\n },\n \"plafondHuurders\": {\n \"type\": \"Null\",\n \"value\": null\n }\n }\n]", + "description": "Decision: Jaar Gebonden Budget\nDecision table: DecisionTable_1bhjh1w\nExpected: plafondEigenaren=null, plafondHuurders=null\nCoverage reasons:\n- Boundary/domain value: aanvraagDatum='2028-01-01'" + } + ], + "description": "Generated MC/DC and boundary-value examples for `Jaar Gebonden Budget`. Each example contains the request body for one generated test case and an expected Operaton/Camunda-style decision-evaluation response body." + } + ] + } + ] +} diff --git a/examples/organizations/flevoland/thuisbatterij/hoogte-subsidie-thuisbatterij.form b/examples/organizations/flevoland/thuisbatterij/hoogte-subsidie-thuisbatterij.form new file mode 100644 index 0000000..e403bbb --- /dev/null +++ b/examples/organizations/flevoland/thuisbatterij/hoogte-subsidie-thuisbatterij.form @@ -0,0 +1,106 @@ +{ + "components": [ + { + "text": "# Hoogte subsidie thuisbatterij\nVul deze gegevens in om de DMN-beslissing `BehaalbareHoogteSubsidie` te kunnen evalueren.", + "type": "text", + "layout": { + "row": "Row_hoogte_intro", + "columns": null + }, + "id": "Field_hoogte_intro" + }, + { + "label": "Gemaakte kosten", + "type": "number", + "layout": { + "row": "Row_hoogte_01", + "columns": null + }, + "id": "Field_gemaakteKosten", + "key": "gemaakteKosten", + "description": "Bedrag in euro's.", + "validate": { + "required": true, + "min": 0 + } + }, + { + "subtype": "date", + "dateLabel": "Datum van aanvraag", + "label": "Datum van aanvraag", + "type": "datetime", + "layout": { + "row": "Row_hoogte_02", + "columns": null + }, + "id": "Field_aanvraagDatum", + "key": "aanvraagDatum", + "description": "Wordt als string in ISO-formaat yyyy-MM-dd opgeslagen; dit past bij date(aanvraagDatum) in de DMN.", + "validate": { + "required": true + } + }, + { + "values": [ + { + "label": "Eigenaar", + "value": "eigenaar" + }, + { + "label": "Huurder", + "value": "huurder" + } + ], + "label": "Aanvrager type", + "type": "select", + "layout": { + "row": "Row_hoogte_03", + "columns": null + }, + "id": "Field_aanvragerType", + "key": "aanvragerType", + "validate": { + "required": true + } + }, + { + "label": "Reeds gesubsidieerd bedrag eigenaren", + "type": "number", + "layout": { + "row": "Row_hoogte_04", + "columns": null + }, + "id": "Field_reedsGesubsidieerdEigenaren", + "key": "reedsGesubsidieerdEigenaren", + "description": "Tot nu toe toegekend bedrag uit het eigenarenplafond, in euro's.", + "validate": { + "required": true, + "min": 0 + } + }, + { + "label": "Reeds gesubsidieerd bedrag huurders", + "type": "number", + "layout": { + "row": "Row_hoogte_05", + "columns": null + }, + "id": "Field_reedsGesubsidieerdHuurders", + "key": "reedsGesubsidieerdHuurders", + "description": "Tot nu toe toegekend bedrag uit het huurdersplafond, in euro's.", + "validate": { + "required": true, + "min": 0 + } + } + ], + "type": "default", + "id": "hoogte-subsidie-thuisbatterij", + "exporter": { + "name": "Camunda Modeler", + "version": "5.46.1" + }, + "executionPlatform": "Camunda Platform", + "executionPlatformVersion": "7.24.0", + "schemaVersion": 16 +} diff --git a/examples/organizations/flevoland/thuisbatterij/recht-en-hoogte-subsidie-thuisbatterij-Flevoland.ttl b/examples/organizations/flevoland/thuisbatterij/recht-en-hoogte-subsidie-thuisbatterij-Flevoland.ttl new file mode 100644 index 0000000..689632b --- /dev/null +++ b/examples/organizations/flevoland/thuisbatterij/recht-en-hoogte-subsidie-thuisbatterij-Flevoland.ttl @@ -0,0 +1,491 @@ +@prefix cpsv: . +@prefix cv: . +@prefix dct: . +@prefix dcat: . +@prefix eli: . +@prefix foaf: . +@prefix org: . +@prefix ronl: . +@prefix skos: . +@prefix schema: . +@prefix xsd: . +@prefix cprmv: . + +# ============================================================ +# Public Service +# ============================================================ + + a cpsv:PublicService ; + dct:identifier "recht-hoogte-subsidie-thuisbatterij-flevoland" ; + dct:title "recht en hoogte subsidie thuisbatterij Flevoland"@nl ; + dct:description "Onder de aanname van het recht, wordt de hoogte van de subsidie voor een thuisbatterij berekend. Deze wordt vermindert met de ruimte in het budget (2026,2027) op het moment van aanvraag"@nl ; + cv:thematicArea ; + cv:sector ; + dcat:keyword "thuisbatterij, Flevoland, subsidie"@nl ; + dct:language ; + cv:hasCompetentAuthority ; + cv:hasLegalResource ; + cprmv:hasDecisionModel . + + +# ============================================================ +# Organization +# ============================================================ + + a cv:PublicOrganisation ; + dct:identifier "Provincie_Flevoland" ; + skos:prefLabel "Provincie Flevoland"@nl ; + foaf:homepage ; + cv:spatial ; + foaf:logo <./assets/Provincie_Flevoland_logo.png> ; + schema:image <./assets/Provincie_Flevoland_logo.png> . + + +# ============================================================ +# Legal Resource +# ============================================================ + + a eli:LegalResource ; + dct:identifier "CVDR750157" ; + dct:title "Regels Thuisbaterij Subsidie "@nl ; + dct:description "Wetsanalyse regels thuis batterij Flevoland uitgevoerd door Stefan 't Hoen"@nl ; + cprmv:hasAnalysis ; + cprmv:hasMethod ; + eli:is_realized_by . + + +# ============================================================ +# Temporal Rules +# ============================================================ + + a cpsv:Rule, cprmv:TemporalRule ; + cpsv:implements ; + dct:identifier "SubsidieThuisbatterij kale hoogte 001" ; + dct:title "kale Subsidiehoogte Thuisbatterij bepaling, zonder rekening houdend met budget"@nl ; + cprmv:validFrom "2026-01-01"^^xsd:date ; + cprmv:validUntil "2027-12-31"^^xsd:date ; + cprmv:confidenceLevel "high" ; + dct:description "Provinciaal \n-Algemene Subsidieverordening Flevoland 2023 \n-Nadere regels Subsidie Thuisbatterij Provincie Flevoland\n\nAanname van recht op subsidie, bepaling van hoogte mogelijk beperkt door lopend budget.\n\nArtikel 5 Regeling \n\nDe hoogte van de subsidie voor een thuisbatterij bedraagt 25% van de subsidiabele kosten, met een minimumbedrag van € 750 en een maximumbedrag van € 1.250. \n\nHoogte subsidie = 25% van subsidiabel bedrag (gegeven) \nMinimum van 750 euro, max 1250 euro.\n\n"@nl . + + a cpsv:Rule, cprmv:TemporalRule ; + cpsv:implements ; + dct:identifier "SubsidieThuisbatterijbudget ruimte 002" ; + dct:title "Subsidiehoogte Thuisbatterij bepaling, met welk jaargebonden budget"@nl ; + cprmv:validFrom "2026-01-12"^^xsd:date ; + cprmv:validUntil "2027-12-31"^^xsd:date ; + cprmv:confidenceLevel "high" ; + dct:description "2026: 875.000 totaal \n\nAanvragen 12 januari 2026 – 30 september 2027 \n\nPlafond Eigenaren: 437.500 \nPlafond Huurder: 437.500 \n\nNa 1 oktober: wat nog over is, geen onderscheid. \n\n2027: 1.000.000 + wat over was van het vorige jaar totaal \n\nAanvragen 1 januari 2027 – 30 september 2027 \n\nPlafond Eigenaren: 437.500 \nPlafond Huurder: 437.500 \n\nNa 1 oktober: wat nog over is, geen onderscheid. "@nl . + + a cpsv:Rule, cprmv:TemporalRule ; + cpsv:implements ; + dct:identifier "Subsidie Thuisbatterij Recht 01" ; + dct:title "Recht op subsidie thuisbatterij"@nl ; + cprmv:validFrom "2026-01-01"^^xsd:date ; + cprmv:validUntil "2028-12-31"^^xsd:date ; + cprmv:confidenceLevel "high" ; + dct:description "Invulling recht in de demo (basis vragen die vanuit formulieren/integraties ingevuld moeten worden): \n\nDe aanvrager heeft recht op subsidie voor de thuis batterij als \n\nDe aanvrager is niet failliet \n\nHet adres van de woning is in Flevoland \n\nDe aanvrager is eigenaar van de woning, of de aanvrager is huurder van de woning \n\nAls de aanvrager huurder is van de woning, dan heeft de aanvrager toestemming van de eigenaar \n\nDe aanvrager is ook de naam op de rekening van de energiemaatschappij "@nl . + + +# ============================================================ +# DMN Decision Model +# ============================================================ + + a cprmv:DecisionModel ; + dct:identifier "BehaalbareHoogteSubsidie" ; + dct:title "RechtEnHoogteSubsidieThuisbatterij.dmn"@nl ; + cprmv:implements ; + dct:source ; + cprmv:deploymentId "29f13fef-5a91-11f1-8761-8ae6f7653f6b" ; + cprmv:deployedAt "2026-05-28T12:31:34.586Z"^^xsd:dateTime ; + cprmv:implementedBy ; + cprmv:lastTested "2026-05-28T12:31:36.896Z"^^xsd:dateTime ; + cprmv:testStatus "passed" ; + dct:description "DMN decision model for service evaluation"@nl . + + a cpsv:Input ; + dct:identifier "Reeds gesubsidieerd eigenaren" ; + dct:title "Reeds gesubsidieerd eigenaren"@nl ; + dct:type "String" ; + schema:value "" ; + cpsv:isRequiredBy . + + a cpsv:Input ; + dct:identifier "Reeds gesubsidieerd huurders" ; + dct:title "Reeds gesubsidieerd huurders"@nl ; + dct:type "String" ; + schema:value "" ; + cpsv:isRequiredBy . + + a cpsv:Input ; + dct:identifier "Datum van aanvraag" ; + dct:title "Datum van aanvraag"@nl ; + dct:type "String" ; + schema:value "2026-05-28" ; + cpsv:isRequiredBy . + + a cpsv:Input ; + dct:identifier "Gemaakte Kosten" ; + dct:title "Gemaakte Kosten"@nl ; + dct:type "String" ; + schema:value "" ; + cpsv:isRequiredBy . + + a cpsv:Input ; + dct:identifier "Aanvraag type" ; + dct:title "Aanvraag type"@nl ; + dct:type "String" ; + schema:value "" ; + cpsv:isRequiredBy . + + a cpsv:Input ; + dct:identifier "Aanvrager is failliet" ; + dct:title "Aanvrager is failliet"@nl ; + dct:type "String" ; + schema:value "" ; + cpsv:isRequiredBy . + + a cpsv:Input ; + dct:identifier "Provincie woning" ; + dct:title "Provincie woning"@nl ; + dct:type "String" ; + schema:value "" ; + cpsv:isRequiredBy . + + a cpsv:Input ; + dct:identifier "Relatie tot woning" ; + dct:title "Relatie tot woning"@nl ; + dct:type "String" ; + schema:value "" ; + cpsv:isRequiredBy . + + a cpsv:Input ; + dct:identifier "Toestemming eigenaar" ; + dct:title "Toestemming eigenaar"@nl ; + dct:type "String" ; + schema:value "" ; + cpsv:isRequiredBy . + + a cpsv:Input ; + dct:identifier "Naam op energierekening komt overeen" ; + dct:title "Naam op energierekening komt overeen"@nl ; + dct:type "String" ; + schema:value "" ; + cpsv:isRequiredBy . + + a cpsv:Rule, cprmv:DecisionRule ; + dct:identifier "Rule_Behaalbare_BasisPastBinnenResterendBudget" ; + cpsv:implements ; + cprmv:ruleType "decision-rule" ; + cprmv:confidence "medium" ; + cprmv:decisionTable "DecisionTable_BehaalbareHoogteSubsidie" ; + cprmv:rulesetType "decision-table" . + + a cpsv:Rule, cprmv:DecisionRule ; + dct:identifier "Rule_Behaalbare_BeperktDoorResterendBudget" ; + cpsv:implements ; + cprmv:ruleType "decision-rule" ; + cprmv:confidence "medium" ; + cprmv:decisionTable "DecisionTable_BehaalbareHoogteSubsidie" ; + cprmv:rulesetType "decision-table" . + + a cpsv:Rule, cprmv:DecisionRule ; + dct:identifier "Rule_Basis_BovenMinimum" ; + cpsv:implements ; + cprmv:ruleType "decision-rule" ; + cprmv:confidence "medium" ; + cprmv:decisionTable "DecisionTable_BerekenBasisHoogteSubsidie" ; + cprmv:rulesetType "decision-table" . + + a cpsv:Rule, cprmv:DecisionRule ; + dct:identifier "Rule_Basis_OnderMinimum" ; + cpsv:implements ; + cprmv:ruleType "decision-rule" ; + cprmv:confidence "medium" ; + cprmv:decisionTable "DecisionTable_BerekenBasisHoogteSubsidie" ; + cprmv:rulesetType "decision-table" . + + a cpsv:Rule, cprmv:DecisionRule ; + dct:identifier "Rule_Platform_2026_Eigenaar_Gescheiden" ; + cpsv:implements ; + cprmv:ruleType "decision-rule" ; + cprmv:confidence "medium" ; + cprmv:decisionTable "DecisionTable_BerekenBeschikbaarSubsidiePlafond" ; + cprmv:rulesetType "decision-table" . + + a cpsv:Rule, cprmv:DecisionRule ; + dct:identifier "Rule_Platform_2026_Huurder_Gescheiden" ; + cpsv:implements ; + cprmv:ruleType "decision-rule" ; + cprmv:confidence "medium" ; + cprmv:decisionTable "DecisionTable_BerekenBeschikbaarSubsidiePlafond" ; + cprmv:rulesetType "decision-table" . + + a cpsv:Rule, cprmv:DecisionRule ; + dct:identifier "Rule_Platform_2026_Gebundeld" ; + cpsv:implements ; + cprmv:ruleType "decision-rule" ; + cprmv:confidence "medium" ; + cprmv:decisionTable "DecisionTable_BerekenBeschikbaarSubsidiePlafond" ; + cprmv:rulesetType "decision-table" . + + a cpsv:Rule, cprmv:DecisionRule ; + dct:identifier "Rule_Platform_2027_Eigenaar_Gescheiden" ; + cpsv:implements ; + cprmv:ruleType "decision-rule" ; + cprmv:confidence "medium" ; + cprmv:decisionTable "DecisionTable_BerekenBeschikbaarSubsidiePlafond" ; + cprmv:rulesetType "decision-table" . + + a cpsv:Rule, cprmv:DecisionRule ; + dct:identifier "Rule_Platform_2027_Huurder_Gescheiden" ; + cpsv:implements ; + cprmv:ruleType "decision-rule" ; + cprmv:confidence "medium" ; + cprmv:decisionTable "DecisionTable_BerekenBeschikbaarSubsidiePlafond" ; + cprmv:rulesetType "decision-table" . + + a cpsv:Rule, cprmv:DecisionRule ; + dct:identifier "Rule_Platform_2027_Gebundeld" ; + cpsv:implements ; + cprmv:ruleType "decision-rule" ; + cprmv:confidence "medium" ; + cprmv:decisionTable "DecisionTable_BerekenBeschikbaarSubsidiePlafond" ; + cprmv:rulesetType "decision-table" . + + a cpsv:Rule, cprmv:DecisionRule ; + dct:identifier "Rule_Platform_BuitenAanvraagperiodeOfOnbekendType" ; + cpsv:implements ; + cprmv:ruleType "decision-rule" ; + cprmv:confidence "medium" ; + cprmv:decisionTable "DecisionTable_BerekenBeschikbaarSubsidiePlafond" ; + cprmv:rulesetType "decision-table" . + + a cpsv:Rule, cprmv:DecisionRule ; + dct:identifier "DecisionRule_0klv7yq" ; + cpsv:implements ; + cprmv:ruleType "decision-rule" ; + cprmv:confidence "medium" ; + cprmv:decisionTable "DecisionTable_BerekenBeschikbaarSubsidiePlafond" ; + cprmv:rulesetType "decision-table" . + + a cpsv:Rule, cprmv:DecisionRule ; + dct:identifier "DecisionRule_16qfnra" ; + cpsv:implements ; + cprmv:ruleType "decision-rule" ; + cprmv:confidence "medium" ; + cprmv:decisionTable "DecisionTable_BerekenBeschikbaarSubsidiePlafond" ; + cprmv:rulesetType "decision-table" . + + a cpsv:Rule, cprmv:DecisionRule ; + dct:identifier "DecisionRule_1dqlhxp" ; + cpsv:implements ; + cprmv:ruleType "decision-rule" ; + cprmv:confidence "medium" ; + cprmv:decisionTable "DecisionTable_BerekenBeschikbaarSubsidiePlafond" ; + cprmv:rulesetType "decision-table" . + + a cpsv:Rule, cprmv:DecisionRule ; + dct:identifier "DecisionRule_1pjqt9f" ; + cpsv:implements ; + cprmv:ruleType "decision-rule" ; + cprmv:confidence "medium" ; + cprmv:decisionTable "DecisionTable_BerekenBeschikbaarSubsidiePlafond" ; + cprmv:rulesetType "decision-table" . + + a cpsv:Rule, cprmv:DecisionRule ; + dct:identifier "Rule_Constanten_Standaard" ; + cpsv:implements ; + cprmv:ruleType "decision-rule" ; + cprmv:confidence "medium" ; + cprmv:decisionTable "DecisionTable_SubsidieConstantenThuisbatterij" ; + cprmv:rulesetType "decision-table" . + + a cpsv:Rule, cprmv:DecisionRule ; + dct:identifier "DecisionRule_0nfr6kl" ; + cpsv:implements ; + cprmv:ruleType "decision-rule" ; + cprmv:confidence "medium" ; + cprmv:decisionTable "DecisionTable_1bhjh1w" ; + cprmv:rulesetType "decision-table" . + + a cpsv:Rule, cprmv:DecisionRule ; + dct:identifier "DecisionRule_1e5agpr" ; + cpsv:implements ; + cprmv:ruleType "decision-rule" ; + cprmv:confidence "medium" ; + cprmv:decisionTable "DecisionTable_1bhjh1w" ; + cprmv:rulesetType "decision-table" . + + a cpsv:Rule, cprmv:DecisionRule ; + dct:identifier "Rule_Afwijzing_AanvragerFailliet" ; + cpsv:implements ; + cprmv:ruleType "decision-rule" ; + cprmv:confidence "medium" ; + cprmv:decisionTable "DecisionTable_RechtOpSubsidieThuisbatterij" ; + cprmv:rulesetType "decision-table" . + + a cpsv:Rule, cprmv:DecisionRule ; + dct:identifier "Rule_Afwijzing_NietInFlevoland" ; + cpsv:implements ; + cprmv:ruleType "decision-rule" ; + cprmv:confidence "medium" ; + cprmv:decisionTable "DecisionTable_RechtOpSubsidieThuisbatterij" ; + cprmv:rulesetType "decision-table" . + + a cpsv:Rule, cprmv:DecisionRule ; + dct:identifier "Rule_Afwijzing_GeenEigenaarOfHuurder" ; + cpsv:implements ; + cprmv:ruleType "decision-rule" ; + cprmv:confidence "medium" ; + cprmv:decisionTable "DecisionTable_RechtOpSubsidieThuisbatterij" ; + cprmv:rulesetType "decision-table" . + + a cpsv:Rule, cprmv:DecisionRule ; + dct:identifier "Rule_Afwijzing_HuurderZonderToestemming" ; + cpsv:implements ; + cprmv:ruleType "decision-rule" ; + cprmv:confidence "medium" ; + cprmv:decisionTable "DecisionTable_RechtOpSubsidieThuisbatterij" ; + cprmv:rulesetType "decision-table" . + + a cpsv:Rule, cprmv:DecisionRule ; + dct:identifier "Rule_Afwijzing_NaamNietOpEnergierekening" ; + cpsv:implements ; + cprmv:ruleType "decision-rule" ; + cprmv:confidence "medium" ; + cprmv:decisionTable "DecisionTable_RechtOpSubsidieThuisbatterij" ; + cprmv:rulesetType "decision-table" . + + a cpsv:Rule, cprmv:DecisionRule ; + dct:identifier "Rule_Toekenning_Eigenaar" ; + cpsv:implements ; + cprmv:ruleType "decision-rule" ; + cprmv:confidence "medium" ; + cprmv:decisionTable "DecisionTable_RechtOpSubsidieThuisbatterij" ; + cprmv:rulesetType "decision-table" . + + a cpsv:Rule, cprmv:DecisionRule ; + dct:identifier "Rule_Toekenning_Huurder" ; + cpsv:implements ; + cprmv:ruleType "decision-rule" ; + cprmv:confidence "medium" ; + cprmv:decisionTable "DecisionTable_RechtOpSubsidieThuisbatterij" ; + cprmv:rulesetType "decision-table" . + + a cpsv:Rule, cprmv:DecisionRule ; + dct:identifier "Rule_Afwijzing_Standaard" ; + cpsv:implements ; + cprmv:ruleType "decision-rule" ; + cprmv:confidence "medium" ; + cprmv:decisionTable "DecisionTable_RechtOpSubsidieThuisbatterij" ; + cprmv:rulesetType "decision-table" . + + a skos:ConceptScheme ; + dct:title "DMN Variabelen Begrippenkader"@nl ; + dct:description "Begrippenkader voor invoer- en uitvoervariabelen van DMN beslisregels in het RONL stelsel."@nl ; + dct:creator "RONL" ; + dct:created "2026-05-28"^^xsd:date . + +# Input Variable Concepts +# ===================================== +# NL-SBB Concept Definitions (DMN Variables) +# ===================================== + + a skos:ConceptScheme ; + dct:title "DMN Variabelen Begrippenkader"@nl ; + dct:description "Begrippenkader voor invoer- en uitvoervariabelen van DMN beslisregels in het RONL stelsel."@nl ; + dct:creator "RONL" ; + dct:created "2026-06-04"^^xsd:date . + +# Input Variable Concepts + + a skos:Concept ; + skos:prefLabel "Reeds gesubsidieerd eigenaren"@nl ; + skos:definition "Reeds gesubsidieerd eigenaren is een tekstuele waarde die als invoer dient voor de beslisregel."@nl ; + skos:notation "RGE" ; + dct:subject ; + dct:type "dmn:InputVariable" ; + skos:exactMatch ; + skos:inScheme . + + a skos:Concept ; + skos:prefLabel "Reeds gesubsidieerd huurders"@nl ; + skos:definition "Reeds gesubsidieerd huurders is een tekstuele waarde die als invoer dient voor de beslisregel."@nl ; + skos:notation "RGH" ; + dct:subject ; + dct:type "dmn:InputVariable" ; + skos:exactMatch ; + skos:inScheme . + + a skos:Concept ; + skos:prefLabel "Datum van aanvraag"@nl ; + skos:definition "Datum van aanvraag is een tekstuele waarde die als invoer dient voor de beslisregel."@nl ; + skos:notation "DVA" ; + dct:subject ; + dct:type "dmn:InputVariable" ; + skos:exactMatch ; + skos:inScheme . + + a skos:Concept ; + skos:prefLabel "Gemaakte Kosten"@nl ; + skos:definition "Gemaakte Kosten is een tekstuele waarde die als invoer dient voor de beslisregel."@nl ; + skos:notation "GK" ; + dct:subject ; + dct:type "dmn:InputVariable" ; + skos:exactMatch ; + skos:inScheme . + + a skos:Concept ; + skos:prefLabel "Aanvraag type"@nl ; + skos:definition "Aanvraag type is een tekstuele waarde die als invoer dient voor de beslisregel."@nl ; + skos:notation "AT" ; + dct:subject ; + dct:type "dmn:InputVariable" ; + skos:exactMatch ; + skos:inScheme . + + a skos:Concept ; + skos:prefLabel "Aanvrager is failliet"@nl ; + skos:definition "Aanvrager is failliet is een tekstuele waarde die als invoer dient voor de beslisregel."@nl ; + skos:notation "AIF" ; + dct:subject ; + dct:type "dmn:InputVariable" ; + skos:exactMatch ; + skos:inScheme . + + a skos:Concept ; + skos:prefLabel "Provincie woning"@nl ; + skos:definition "Provincie woning is een tekstuele waarde die als invoer dient voor de beslisregel."@nl ; + skos:notation "PW" ; + dct:subject ; + dct:type "dmn:InputVariable" ; + skos:exactMatch ; + skos:inScheme . + + a skos:Concept ; + skos:prefLabel "Relatie tot woning"@nl ; + skos:definition "Relatie tot woning is een tekstuele waarde die als invoer dient voor de beslisregel."@nl ; + skos:notation "RTW" ; + dct:subject ; + dct:type "dmn:InputVariable" ; + skos:exactMatch ; + skos:inScheme . + + a skos:Concept ; + skos:prefLabel "Toestemming eigenaar"@nl ; + skos:definition "Toestemming eigenaar is een tekstuele waarde die als invoer dient voor de beslisregel."@nl ; + skos:notation "TE" ; + dct:subject ; + dct:type "dmn:InputVariable" ; + skos:exactMatch ; + skos:inScheme . + + a skos:Concept ; + skos:prefLabel "Naam op energierekening komt overeen"@nl ; + skos:definition "Naam op energierekening komt overeen is een tekstuele waarde die als invoer dient voor de beslisregel."@nl ; + skos:notation "NOEKO" ; + dct:subject ; + dct:type "dmn:InputVariable" ; + skos:exactMatch ; + skos:inScheme . + diff --git a/examples/organizations/flevoland/thuisbatterij/recht-op-subsidie-thuisbatterij.form b/examples/organizations/flevoland/thuisbatterij/recht-op-subsidie-thuisbatterij.form new file mode 100644 index 0000000..f5d38a6 --- /dev/null +++ b/examples/organizations/flevoland/thuisbatterij/recht-op-subsidie-thuisbatterij.form @@ -0,0 +1,148 @@ +{ + "components": [ + { + "text": "# Recht op subsidie thuisbatterij\nVul deze gegevens in om de DMN-beslissing `RechtOpSubsidieThuisbatterij` te kunnen evalueren. Boolean velden zijn aangevinkt = ja, niet aangevinkt = nee.", + "type": "text", + "layout": { + "row": "Row_recht_intro", + "columns": null + }, + "id": "Field_recht_intro" + }, + { + "label": "Is de aanvrager failliet?", + "type": "checkbox", + "layout": { + "row": "Row_recht_01", + "columns": null + }, + "id": "Field_aanvragerFailliet", + "key": "aanvragerFailliet", + "description": "Aangevinkt = ja; niet aangevinkt = nee.", + "defaultValue": false + }, + { + "values": [ + { + "label": "Flevoland", + "value": "Flevoland" + }, + { + "label": "Drenthe", + "value": "Drenthe" + }, + { + "label": "Friesland", + "value": "Friesland" + }, + { + "label": "Gelderland", + "value": "Gelderland" + }, + { + "label": "Groningen", + "value": "Groningen" + }, + { + "label": "Limburg", + "value": "Limburg" + }, + { + "label": "Noord-Brabant", + "value": "Noord-Brabant" + }, + { + "label": "Noord-Holland", + "value": "Noord-Holland" + }, + { + "label": "Overijssel", + "value": "Overijssel" + }, + { + "label": "Utrecht", + "value": "Utrecht" + }, + { + "label": "Zeeland", + "value": "Zeeland" + }, + { + "label": "Zuid-Holland", + "value": "Zuid-Holland" + } + ], + "label": "In welke provincie ligt de woning?", + "type": "select", + "layout": { + "row": "Row_recht_02", + "columns": null + }, + "id": "Field_provincieWoning", + "key": "provincieWoning", + "validate": { + "required": true + } + }, + { + "values": [ + { + "label": "Eigenaar", + "value": "eigenaar" + }, + { + "label": "Huurder", + "value": "huurder" + }, + { + "label": "Anders", + "value": "anders" + } + ], + "label": "Wat is de relatie van de aanvrager tot de woning?", + "type": "select", + "layout": { + "row": "Row_recht_03", + "columns": null + }, + "id": "Field_relatieTotWoning", + "key": "relatieTotWoning", + "validate": { + "required": true + } + }, + { + "label": "Heeft de huurder toestemming van de eigenaar?", + "type": "checkbox", + "layout": { + "row": "Row_recht_04", + "columns": null + }, + "id": "Field_toestemmingEigenaar", + "key": "toestemmingEigenaar", + "description": "Aangevinkt = ja; niet aangevinkt = nee. Bij een eigenaar wordt deze waarde door de DMN-regels genegeerd.", + "defaultValue": false + }, + { + "label": "Komt de naam op de energierekening overeen met de aanvrager?", + "type": "checkbox", + "layout": { + "row": "Row_recht_05", + "columns": null + }, + "id": "Field_rekeningNaamKomtOvereen", + "key": "rekeningNaamKomtOvereen", + "description": "Aangevinkt = ja; niet aangevinkt = nee.", + "defaultValue": false + } + ], + "type": "default", + "id": "recht-op-subsidie-thuisbatterij", + "exporter": { + "name": "Camunda Modeler", + "version": "5.46.1" + }, + "executionPlatform": "Camunda Platform", + "executionPlatformVersion": "7.24.0", + "schemaVersion": 16 +} diff --git a/examples/organizations/flevoland/thuisbatterij/thuisbatterij-mcdc-test-cases.json b/examples/organizations/flevoland/thuisbatterij/thuisbatterij-mcdc-test-cases.json new file mode 100644 index 0000000..5f39637 --- /dev/null +++ b/examples/organizations/flevoland/thuisbatterij/thuisbatterij-mcdc-test-cases.json @@ -0,0 +1,3813 @@ +[ + { + "name": "TC_001 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=0.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 0, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BasisPastBinnenResterendBudget", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: basisHoogteSubsidie=0", + "Boundary/domain value: beschikbaarSubsidiePlafond=0", + "Output minimum: hoogteSubsidie=0", + "Output near zero boundary: hoogteSubsidie=0" + ] + } + }, + { + "name": "TC_002 Behaalbare Hoogte Subsidie - MC/DC - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=0.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 0, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 1, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BasisPastBinnenResterendBudget", + "selectedRuleIndex": 1, + "reasons": [ + "MC/DC: rule 2 Rule_Behaalbare_BeperktDoorResterendBudget, condition [basisHoogteSubsidie > beschikbaarSubsidiePlafond] baseline" + ] + } + }, + { + "name": "TC_003 Behaalbare Hoogte Subsidie - MC/DC - Rule_Behaalbare_BeperktDoorResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=0.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 1, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BeperktDoorResterendBudget", + "selectedRuleIndex": 2, + "reasons": [ + "MC/DC: rule 1 Rule_Behaalbare_BasisPastBinnenResterendBudget, condition [basisHoogteSubsidie <= beschikbaarSubsidiePlafond] baseline" + ] + } + }, + { + "name": "TC_004 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=1.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 1, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 1249, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BasisPastBinnenResterendBudget", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: basisHoogteSubsidie=1" + ] + } + }, + { + "name": "TC_005 Behaalbare Hoogte Subsidie - MC/DC - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=1.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 1, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 1, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BasisPastBinnenResterendBudget", + "selectedRuleIndex": 1, + "reasons": [ + "MC/DC: rule 1 Rule_Behaalbare_BasisPastBinnenResterendBudget, condition [basisHoogteSubsidie <= beschikbaarSubsidiePlafond] flipped", + "Boundary/domain value: beschikbaarSubsidiePlafond=1" + ] + } + }, + { + "name": "TC_006 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=1249.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 1249, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 1249, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BasisPastBinnenResterendBudget", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: basisHoogteSubsidie=1249", + "Boundary/domain value: beschikbaarSubsidiePlafond=1249" + ] + } + }, + { + "name": "TC_007 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BeperktDoorResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=749.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 1249, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 749, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BeperktDoorResterendBudget", + "selectedRuleIndex": 2, + "reasons": [ + "Boundary/domain value: beschikbaarSubsidiePlafond=749" + ] + } + }, + { + "name": "TC_008 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BeperktDoorResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=750.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 1249, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 750, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BeperktDoorResterendBudget", + "selectedRuleIndex": 2, + "reasons": [ + "Boundary/domain value: beschikbaarSubsidiePlafond=750" + ] + } + }, + { + "name": "TC_009 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BeperktDoorResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=751.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 1249, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 751, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BeperktDoorResterendBudget", + "selectedRuleIndex": 2, + "reasons": [ + "Boundary/domain value: beschikbaarSubsidiePlafond=751" + ] + } + }, + { + "name": "TC_010 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=1250.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 1250, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 1250, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BasisPastBinnenResterendBudget", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: basisHoogteSubsidie=1250", + "Boundary/domain value: beschikbaarSubsidiePlafond=1250" + ] + } + }, + { + "name": "TC_011 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=1251.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 1251, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 1251, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BasisPastBinnenResterendBudget", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: basisHoogteSubsidie=1251", + "Boundary/domain value: beschikbaarSubsidiePlafond=1251" + ] + } + }, + { + "name": "TC_012 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=437499.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 437499, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 437499, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BasisPastBinnenResterendBudget", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: basisHoogteSubsidie=437499", + "Boundary/domain value: beschikbaarSubsidiePlafond=437499" + ] + } + }, + { + "name": "TC_013 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=437500.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 437500, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 437500, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BasisPastBinnenResterendBudget", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: basisHoogteSubsidie=437500", + "Boundary/domain value: beschikbaarSubsidiePlafond=437500" + ] + } + }, + { + "name": "TC_014 Behaalbare Hoogte Subsidie - Representative selected rule 2 - Rule_Behaalbare_BeperktDoorResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=437500.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 437501, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 437500, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BeperktDoorResterendBudget", + "selectedRuleIndex": 2, + "reasons": [ + "Representative selected rule 2: Rule_Behaalbare_BeperktDoorResterendBudget" + ] + } + }, + { + "name": "TC_015 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=437501.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 437501, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 437501, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BasisPastBinnenResterendBudget", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: basisHoogteSubsidie=437501", + "Boundary/domain value: beschikbaarSubsidiePlafond=437501", + "Representative selected rule 1: Rule_Behaalbare_BasisPastBinnenResterendBudget", + "Output maximum: hoogteSubsidie=437501" + ] + } + }, + { + "name": "TC_016 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=749.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 749, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 1249, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BasisPastBinnenResterendBudget", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: basisHoogteSubsidie=749" + ] + } + }, + { + "name": "TC_017 Behaalbare Hoogte Subsidie - MC/DC - Rule_Behaalbare_BeperktDoorResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=1.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 749, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 1, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BeperktDoorResterendBudget", + "selectedRuleIndex": 2, + "reasons": [ + "MC/DC: rule 2 Rule_Behaalbare_BeperktDoorResterendBudget, condition [basisHoogteSubsidie > beschikbaarSubsidiePlafond] flipped" + ] + } + }, + { + "name": "TC_018 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=750.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 750, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 1249, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BasisPastBinnenResterendBudget", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: basisHoogteSubsidie=750" + ] + } + }, + { + "name": "TC_019 Behaalbare Hoogte Subsidie - Boundary/domain value - Rule_Behaalbare_BasisPastBinnenResterendBudget", + "decisionId": "BehaalbareHoogteSubsidie", + "decisionName": "Behaalbare Hoogte Subsidie", + "decisionTableId": "DecisionTable_BehaalbareHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "hoogteSubsidie=751.0", + "requestBody": { + "variables": { + "basisHoogteSubsidie": { + "value": 751, + "type": "Double" + }, + "beschikbaarSubsidiePlafond": { + "value": 1249, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Behaalbare_BasisPastBinnenResterendBudget", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: basisHoogteSubsidie=751" + ] + } + }, + { + "name": "TC_020 Bereken Basis Hoogte Subsidie - MC/DC - Rule_Basis_OnderMinimum", + "decisionId": "BerekenBasisHoogteSubsidie", + "decisionName": "Bereken Basis Hoogte Subsidie", + "decisionTableId": "DecisionTable_BerekenBasisHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "basisHoogteSubsidie=0.0", + "requestBody": { + "variables": { + "gemaakteKosten": { + "value": 0, + "type": "Double" + }, + "minimaleNoodzakelijkeKosten": { + "value": 750, + "type": "Double" + }, + "subsidieMaximum": { + "value": 1250, + "type": "Double" + }, + "subsidieMinimum": { + "value": 750, + "type": "Double" + }, + "subsidiePercentage": { + "value": 0.25, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Basis_OnderMinimum", + "selectedRuleIndex": 2, + "reasons": [ + "MC/DC: rule 1 Rule_Basis_BovenMinimum, condition [gemaakteKosten >= minimaleNoodzakelijkeKosten / subsidiePercentage] baseline", + "MC/DC: rule 2 Rule_Basis_OnderMinimum, condition [gemaakteKosten < minimaleNoodzakelijkeKosten / subsidiePercentage] flipped", + "Boundary/domain value: gemaakteKosten=0", + "Representative selected rule 2: Rule_Basis_OnderMinimum", + "Output minimum: basisHoogteSubsidie=0", + "Output near zero boundary: basisHoogteSubsidie=0" + ] + } + }, + { + "name": "TC_021 Bereken Basis Hoogte Subsidie - Boundary/domain value - Rule_Basis_BovenMinimum", + "decisionId": "BerekenBasisHoogteSubsidie", + "decisionName": "Bereken Basis Hoogte Subsidie", + "decisionTableId": "DecisionTable_BerekenBasisHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "basisHoogteSubsidie=1250.0", + "requestBody": { + "variables": { + "gemaakteKosten": { + "value": 10000, + "type": "Double" + }, + "minimaleNoodzakelijkeKosten": { + "value": 750, + "type": "Double" + }, + "subsidieMaximum": { + "value": 1250, + "type": "Double" + }, + "subsidieMinimum": { + "value": 750, + "type": "Double" + }, + "subsidiePercentage": { + "value": 0.25, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Basis_BovenMinimum", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: gemaakteKosten=10000", + "Boundary/domain value: minimaleNoodzakelijkeKosten=750", + "Boundary/domain value: subsidieMaximum=1250", + "Boundary/domain value: subsidieMinimum=750", + "Boundary/domain value: subsidiePercentage=0.25", + "Representative selected rule 1: Rule_Basis_BovenMinimum", + "Output maximum: basisHoogteSubsidie=1250" + ] + } + }, + { + "name": "TC_022 Bereken Basis Hoogte Subsidie - Boundary/domain value - Rule_Basis_OnderMinimum", + "decisionId": "BerekenBasisHoogteSubsidie", + "decisionName": "Bereken Basis Hoogte Subsidie", + "decisionTableId": "DecisionTable_BerekenBasisHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "basisHoogteSubsidie=0.0", + "requestBody": { + "variables": { + "gemaakteKosten": { + "value": 2999, + "type": "Double" + }, + "minimaleNoodzakelijkeKosten": { + "value": 750, + "type": "Double" + }, + "subsidieMaximum": { + "value": 1250, + "type": "Double" + }, + "subsidieMinimum": { + "value": 750, + "type": "Double" + }, + "subsidiePercentage": { + "value": 0.25, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Basis_OnderMinimum", + "selectedRuleIndex": 2, + "reasons": [ + "Boundary/domain value: gemaakteKosten=2999" + ] + } + }, + { + "name": "TC_023 Bereken Basis Hoogte Subsidie - MC/DC - Rule_Basis_BovenMinimum", + "decisionId": "BerekenBasisHoogteSubsidie", + "decisionName": "Bereken Basis Hoogte Subsidie", + "decisionTableId": "DecisionTable_BerekenBasisHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "basisHoogteSubsidie=750.0", + "requestBody": { + "variables": { + "gemaakteKosten": { + "value": 3000, + "type": "Double" + }, + "minimaleNoodzakelijkeKosten": { + "value": 750, + "type": "Double" + }, + "subsidieMaximum": { + "value": 1250, + "type": "Double" + }, + "subsidieMinimum": { + "value": 750, + "type": "Double" + }, + "subsidiePercentage": { + "value": 0.25, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Basis_BovenMinimum", + "selectedRuleIndex": 1, + "reasons": [ + "MC/DC: rule 1 Rule_Basis_BovenMinimum, condition [gemaakteKosten >= minimaleNoodzakelijkeKosten / subsidiePercentage] flipped", + "MC/DC: rule 2 Rule_Basis_OnderMinimum, condition [gemaakteKosten < minimaleNoodzakelijkeKosten / subsidiePercentage] baseline", + "Boundary/domain value: gemaakteKosten=3000" + ] + } + }, + { + "name": "TC_024 Bereken Basis Hoogte Subsidie - Boundary/domain value - Rule_Basis_BovenMinimum", + "decisionId": "BerekenBasisHoogteSubsidie", + "decisionName": "Bereken Basis Hoogte Subsidie", + "decisionTableId": "DecisionTable_BerekenBasisHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "basisHoogteSubsidie=750.25", + "requestBody": { + "variables": { + "gemaakteKosten": { + "value": 3001, + "type": "Double" + }, + "minimaleNoodzakelijkeKosten": { + "value": 750, + "type": "Double" + }, + "subsidieMaximum": { + "value": 1250, + "type": "Double" + }, + "subsidieMinimum": { + "value": 750, + "type": "Double" + }, + "subsidiePercentage": { + "value": 0.25, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Basis_BovenMinimum", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: gemaakteKosten=3001" + ] + } + }, + { + "name": "TC_025 Bereken Basis Hoogte Subsidie - Boundary/domain value - Rule_Basis_BovenMinimum", + "decisionId": "BerekenBasisHoogteSubsidie", + "decisionName": "Bereken Basis Hoogte Subsidie", + "decisionTableId": "DecisionTable_BerekenBasisHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "basisHoogteSubsidie=1249.75", + "requestBody": { + "variables": { + "gemaakteKosten": { + "value": 4999, + "type": "Double" + }, + "minimaleNoodzakelijkeKosten": { + "value": 750, + "type": "Double" + }, + "subsidieMaximum": { + "value": 1250, + "type": "Double" + }, + "subsidieMinimum": { + "value": 750, + "type": "Double" + }, + "subsidiePercentage": { + "value": 0.25, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Basis_BovenMinimum", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: gemaakteKosten=4999" + ] + } + }, + { + "name": "TC_026 Bereken Basis Hoogte Subsidie - Boundary/domain value - Rule_Basis_BovenMinimum", + "decisionId": "BerekenBasisHoogteSubsidie", + "decisionName": "Bereken Basis Hoogte Subsidie", + "decisionTableId": "DecisionTable_BerekenBasisHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "basisHoogteSubsidie=1250.0", + "requestBody": { + "variables": { + "gemaakteKosten": { + "value": 5000, + "type": "Double" + }, + "minimaleNoodzakelijkeKosten": { + "value": 750, + "type": "Double" + }, + "subsidieMaximum": { + "value": 1250, + "type": "Double" + }, + "subsidieMinimum": { + "value": 750, + "type": "Double" + }, + "subsidiePercentage": { + "value": 0.25, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Basis_BovenMinimum", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: gemaakteKosten=5000" + ] + } + }, + { + "name": "TC_027 Bereken Basis Hoogte Subsidie - Boundary/domain value - Rule_Basis_BovenMinimum", + "decisionId": "BerekenBasisHoogteSubsidie", + "decisionName": "Bereken Basis Hoogte Subsidie", + "decisionTableId": "DecisionTable_BerekenBasisHoogteSubsidie", + "evaluationMode": "direct-table-inputs", + "expected": "basisHoogteSubsidie=1250.0", + "requestBody": { + "variables": { + "gemaakteKosten": { + "value": 5001, + "type": "Double" + }, + "minimaleNoodzakelijkeKosten": { + "value": 750, + "type": "Double" + }, + "subsidieMaximum": { + "value": 1250, + "type": "Double" + }, + "subsidieMinimum": { + "value": 750, + "type": "Double" + }, + "subsidiePercentage": { + "value": 0.25, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Basis_BovenMinimum", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: gemaakteKosten=5001" + ] + } + }, + { + "name": "TC_028 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_BuitenAanvraagperiodeOfOnbekendType", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=0.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2025-12-31", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_BuitenAanvraagperiodeOfOnbekendType", + "selectedRuleIndex": 7, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2025-12-31'", + "Representative selected rule 7: Rule_Platform_BuitenAanvraagperiodeOfOnbekendType" + ] + } + }, + { + "name": "TC_029 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_BuitenAanvraagperiodeOfOnbekendType", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=0.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2025-12-31", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 437500, + "type": "Double" + }, + "plafondHuurders": { + "value": 437500, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_BuitenAanvraagperiodeOfOnbekendType", + "selectedRuleIndex": 7, + "reasons": [ + "MC/DC: rule 1 Rule_Platform_2026_Eigenaar_Gescheiden, condition [date(aanvraagDatum) >= date(\"2026-01-12\")] baseline", + "MC/DC: rule 3 Rule_Platform_2026_Gebundeld, condition [date(aanvraagDatum) >= date(\"2026-10-01\")] baseline", + "MC/DC: rule 4 Rule_Platform_2027_Eigenaar_Gescheiden, condition [date(\"2027-01-01\") <= date(aanvraagDatum)] baseline", + "MC/DC: rule 6 Rule_Platform_2027_Gebundeld, condition [date(aanvraagDatum) >= date(\"2027-10-01\")] baseline", + "Output minimum: beschikbaarSubsidiePlafond=0", + "Output near zero boundary: beschikbaarSubsidiePlafond=0" + ] + } + }, + { + "name": "TC_030 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_BuitenAanvraagperiodeOfOnbekendType", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=0.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2025-12-31", + "type": "String" + }, + "aanvragerType": { + "value": "huurder", + "type": "String" + }, + "plafondEigenaren": { + "value": 437500, + "type": "Double" + }, + "plafondHuurders": { + "value": 437500, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_BuitenAanvraagperiodeOfOnbekendType", + "selectedRuleIndex": 7, + "reasons": [ + "MC/DC: rule 2 Rule_Platform_2026_Huurder_Gescheiden, condition [date(aanvraagDatum) >= date(\"2026-01-12\")] baseline", + "MC/DC: rule 5 Rule_Platform_2027_Huurder_Gescheiden, condition [date(aanvraagDatum) >= date(\"2027-01-01\")] baseline" + ] + } + }, + { + "name": "TC_031 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_BuitenAanvraagperiodeOfOnbekendType", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=0.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-01-11", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_BuitenAanvraagperiodeOfOnbekendType", + "selectedRuleIndex": 7, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2026-01-11'" + ] + } + }, + { + "name": "TC_032 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Eigenaar_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-01-12", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Eigenaar_Gescheiden", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2026-01-12'", + "Representative selected rule 1: Rule_Platform_2026_Eigenaar_Gescheiden" + ] + } + }, + { + "name": "TC_033 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Eigenaar_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-01-12", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 1000000, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Eigenaar_Gescheiden", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdHuurders=1000000" + ] + } + }, + { + "name": "TC_034 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Eigenaar_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-01-12", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 1000001, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Eigenaar_Gescheiden", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdHuurders=1000001" + ] + } + }, + { + "name": "TC_035 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_2026_Eigenaar_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=437500.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-01-12", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 437500, + "type": "Double" + }, + "plafondHuurders": { + "value": 437500, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Eigenaar_Gescheiden", + "selectedRuleIndex": 1, + "reasons": [ + "MC/DC: rule 1 Rule_Platform_2026_Eigenaar_Gescheiden, condition [date(aanvraagDatum) >= date(\"2026-01-12\")] flipped", + "MC/DC: rule 1 Rule_Platform_2026_Eigenaar_Gescheiden, condition [date(aanvraagDatum) <= date(\"2026-09-30\")] flipped" + ] + } + }, + { + "name": "TC_036 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_2026_Eigenaar_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=437500.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-01-12", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 437500, + "type": "Double" + }, + "plafondHuurders": { + "value": 437500, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 1, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Eigenaar_Gescheiden", + "selectedRuleIndex": 1, + "reasons": [ + "MC/DC: rule 1 Rule_Platform_2026_Eigenaar_Gescheiden, condition [aanvragerType == \"eigenaar\"] flipped", + "MC/DC: rule 2 Rule_Platform_2026_Huurder_Gescheiden, condition [aanvragerType == \"huurder\"] baseline" + ] + } + }, + { + "name": "TC_037 Bereken Beschikbaar Subsidie Plafond - Representative selected rule 2 - Rule_Platform_2026_Huurder_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-01-12", + "type": "String" + }, + "aanvragerType": { + "value": "huurder", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Huurder_Gescheiden", + "selectedRuleIndex": 2, + "reasons": [ + "Representative selected rule 2: Rule_Platform_2026_Huurder_Gescheiden" + ] + } + }, + { + "name": "TC_038 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Huurder_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-01-12", + "type": "String" + }, + "aanvragerType": { + "value": "huurder", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Huurder_Gescheiden", + "selectedRuleIndex": 2, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdEigenaren=1000000" + ] + } + }, + { + "name": "TC_039 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Huurder_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-01-12", + "type": "String" + }, + "aanvragerType": { + "value": "huurder", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 1000001, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Huurder_Gescheiden", + "selectedRuleIndex": 2, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdEigenaren=1000001" + ] + } + }, + { + "name": "TC_040 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_2026_Huurder_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=437500.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-01-12", + "type": "String" + }, + "aanvragerType": { + "value": "huurder", + "type": "String" + }, + "plafondEigenaren": { + "value": 437500, + "type": "Double" + }, + "plafondHuurders": { + "value": 437500, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Huurder_Gescheiden", + "selectedRuleIndex": 2, + "reasons": [ + "MC/DC: rule 2 Rule_Platform_2026_Huurder_Gescheiden, condition [date(aanvraagDatum) >= date(\"2026-01-12\")] flipped", + "MC/DC: rule 2 Rule_Platform_2026_Huurder_Gescheiden, condition [date(aanvraagDatum) <= date(\"2026-09-30\")] flipped" + ] + } + }, + { + "name": "TC_041 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_2026_Huurder_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=437499.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-01-12", + "type": "String" + }, + "aanvragerType": { + "value": "huurder", + "type": "String" + }, + "plafondEigenaren": { + "value": 437500, + "type": "Double" + }, + "plafondHuurders": { + "value": 437500, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 1, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Huurder_Gescheiden", + "selectedRuleIndex": 2, + "reasons": [ + "MC/DC: rule 1 Rule_Platform_2026_Eigenaar_Gescheiden, condition [aanvragerType == \"eigenaar\"] baseline", + "MC/DC: rule 2 Rule_Platform_2026_Huurder_Gescheiden, condition [aanvragerType == \"huurder\"] flipped" + ] + } + }, + { + "name": "TC_042 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Eigenaar_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-01-13", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Eigenaar_Gescheiden", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2026-01-13'" + ] + } + }, + { + "name": "TC_043 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Eigenaar_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-09-29", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Eigenaar_Gescheiden", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2026-09-29'" + ] + } + }, + { + "name": "TC_044 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Eigenaar_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-09-30", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Eigenaar_Gescheiden", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2026-09-30'" + ] + } + }, + { + "name": "TC_045 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=2000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2026-10-01'", + "Boundary/domain value: aanvragerType='eigenaar'", + "Boundary/domain value: plafondEigenaren=1000000", + "Boundary/domain value: plafondHuurders=1000000", + "Boundary/domain value: reedsGesubsidieerdEigenaren=0", + "Boundary/domain value: reedsGesubsidieerdHuurders=0", + "Representative selected rule 3: Rule_Platform_2026_Gebundeld" + ] + } + }, + { + "name": "TC_046 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1999999.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 1, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdHuurders=1" + ] + } + }, + { + "name": "TC_047 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1562501.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 437499, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdHuurders=437499" + ] + } + }, + { + "name": "TC_048 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1562500.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 437500, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdHuurders=437500" + ] + } + }, + { + "name": "TC_049 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1562499.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 437501, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdHuurders=437501" + ] + } + }, + { + "name": "TC_050 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1125001.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 874999, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdHuurders=874999" + ] + } + }, + { + "name": "TC_051 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1125000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 875000, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdHuurders=875000" + ] + } + }, + { + "name": "TC_052 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1124999.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 875001, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdHuurders=875001" + ] + } + }, + { + "name": "TC_053 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000001.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 999999, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdHuurders=999999" + ] + } + }, + { + "name": "TC_054 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1999999.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 1, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdEigenaren=1" + ] + } + }, + { + "name": "TC_055 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1562501.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 437499, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdEigenaren=437499" + ] + } + }, + { + "name": "TC_056 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1562500.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 437500, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdEigenaren=437500" + ] + } + }, + { + "name": "TC_057 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1562499.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 437501, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdEigenaren=437501" + ] + } + }, + { + "name": "TC_058 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1125001.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 874999, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdEigenaren=874999" + ] + } + }, + { + "name": "TC_059 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1125000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 875000, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdEigenaren=875000" + ] + } + }, + { + "name": "TC_060 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1124999.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 875001, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdEigenaren=875001" + ] + } + }, + { + "name": "TC_061 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000001.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 999999, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: reedsGesubsidieerdEigenaren=999999" + ] + } + }, + { + "name": "TC_062 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1437500.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 437500, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: plafondHuurders=437500" + ] + } + }, + { + "name": "TC_063 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1875000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 875000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: plafondHuurders=875000" + ] + } + }, + { + "name": "TC_064 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1437500.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 437500, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: plafondEigenaren=437500" + ] + } + }, + { + "name": "TC_065 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=875000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 437500, + "type": "Double" + }, + "plafondHuurders": { + "value": 437500, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "MC/DC: rule 1 Rule_Platform_2026_Eigenaar_Gescheiden, condition [date(aanvraagDatum) <= date(\"2026-09-30\")] baseline", + "MC/DC: rule 3 Rule_Platform_2026_Gebundeld, condition [date(aanvraagDatum) >= date(\"2026-10-01\")] flipped", + "MC/DC: rule 3 Rule_Platform_2026_Gebundeld, condition [date(aanvraagDatum) <= date(\"2026-12-31\")] flipped" + ] + } + }, + { + "name": "TC_066 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1875000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 875000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: plafondEigenaren=875000" + ] + } + }, + { + "name": "TC_067 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=2000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "huurder", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: aanvragerType='huurder'" + ] + } + }, + { + "name": "TC_068 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=875000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "huurder", + "type": "String" + }, + "plafondEigenaren": { + "value": 437500, + "type": "Double" + }, + "plafondHuurders": { + "value": 437500, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "MC/DC: rule 2 Rule_Platform_2026_Huurder_Gescheiden, condition [date(aanvraagDatum) <= date(\"2026-09-30\")] baseline" + ] + } + }, + { + "name": "TC_069 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=2000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "onbekend", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: aanvragerType='onbekend'" + ] + } + }, + { + "name": "TC_070 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=2000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-10-02", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2026-10-02'" + ] + } + }, + { + "name": "TC_071 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=2000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-12-30", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2026-12-30'" + ] + } + }, + { + "name": "TC_072 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=2000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-12-31", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2026-12-31'" + ] + } + }, + { + "name": "TC_073 Bereken Beschikbaar Subsidie Plafond - Output maximum - Rule_Platform_2026_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=2000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-12-31", + "type": "String" + }, + "aanvragerType": { + "value": "onbekend", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2026_Gebundeld", + "selectedRuleIndex": 3, + "reasons": [ + "Output maximum: beschikbaarSubsidiePlafond=2000000" + ] + } + }, + { + "name": "TC_074 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2027_Eigenaar_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2027-01-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2027_Eigenaar_Gescheiden", + "selectedRuleIndex": 4, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2027-01-01'", + "Representative selected rule 4: Rule_Platform_2027_Eigenaar_Gescheiden" + ] + } + }, + { + "name": "TC_075 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_2027_Eigenaar_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=437500.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2027-01-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 437500, + "type": "Double" + }, + "plafondHuurders": { + "value": 437500, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2027_Eigenaar_Gescheiden", + "selectedRuleIndex": 4, + "reasons": [ + "MC/DC: rule 3 Rule_Platform_2026_Gebundeld, condition [date(aanvraagDatum) <= date(\"2026-12-31\")] baseline", + "MC/DC: rule 4 Rule_Platform_2027_Eigenaar_Gescheiden, condition [date(\"2027-01-01\") <= date(aanvraagDatum)] flipped", + "MC/DC: rule 4 Rule_Platform_2027_Eigenaar_Gescheiden, condition [date(aanvraagDatum) <= date(\"2027-09-30\")] flipped" + ] + } + }, + { + "name": "TC_076 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_2027_Eigenaar_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=437500.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2027-01-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 437500, + "type": "Double" + }, + "plafondHuurders": { + "value": 437500, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 1, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2027_Eigenaar_Gescheiden", + "selectedRuleIndex": 4, + "reasons": [ + "MC/DC: rule 4 Rule_Platform_2027_Eigenaar_Gescheiden, condition [aanvragerType == \"eigenaar\"] flipped", + "MC/DC: rule 5 Rule_Platform_2027_Huurder_Gescheiden, condition [aanvragerType == \"huurder\"] baseline" + ] + } + }, + { + "name": "TC_077 Bereken Beschikbaar Subsidie Plafond - Representative selected rule 5 - Rule_Platform_2027_Huurder_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2027-01-01", + "type": "String" + }, + "aanvragerType": { + "value": "huurder", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2027_Huurder_Gescheiden", + "selectedRuleIndex": 5, + "reasons": [ + "Representative selected rule 5: Rule_Platform_2027_Huurder_Gescheiden" + ] + } + }, + { + "name": "TC_078 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_2027_Huurder_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=437500.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2027-01-01", + "type": "String" + }, + "aanvragerType": { + "value": "huurder", + "type": "String" + }, + "plafondEigenaren": { + "value": 437500, + "type": "Double" + }, + "plafondHuurders": { + "value": 437500, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2027_Huurder_Gescheiden", + "selectedRuleIndex": 5, + "reasons": [ + "MC/DC: rule 5 Rule_Platform_2027_Huurder_Gescheiden, condition [date(aanvraagDatum) >= date(\"2027-01-01\")] flipped", + "MC/DC: rule 5 Rule_Platform_2027_Huurder_Gescheiden, condition [date(aanvraagDatum) <= date(\"2027-09-30\")] flipped" + ] + } + }, + { + "name": "TC_079 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_2027_Huurder_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=437499.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2027-01-01", + "type": "String" + }, + "aanvragerType": { + "value": "huurder", + "type": "String" + }, + "plafondEigenaren": { + "value": 437500, + "type": "Double" + }, + "plafondHuurders": { + "value": 437500, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 1, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2027_Huurder_Gescheiden", + "selectedRuleIndex": 5, + "reasons": [ + "MC/DC: rule 4 Rule_Platform_2027_Eigenaar_Gescheiden, condition [aanvragerType == \"eigenaar\"] baseline", + "MC/DC: rule 5 Rule_Platform_2027_Huurder_Gescheiden, condition [aanvragerType == \"huurder\"] flipped" + ] + } + }, + { + "name": "TC_080 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2027_Eigenaar_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2027-01-02", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2027_Eigenaar_Gescheiden", + "selectedRuleIndex": 4, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2027-01-02'" + ] + } + }, + { + "name": "TC_081 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2027_Eigenaar_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2027-09-29", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2027_Eigenaar_Gescheiden", + "selectedRuleIndex": 4, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2027-09-29'" + ] + } + }, + { + "name": "TC_082 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2027_Eigenaar_Gescheiden", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2027-09-30", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2027_Eigenaar_Gescheiden", + "selectedRuleIndex": 4, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2027-09-30'" + ] + } + }, + { + "name": "TC_083 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2027_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2027-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2027_Gebundeld", + "selectedRuleIndex": 6, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2027-10-01'", + "Representative selected rule 6: Rule_Platform_2027_Gebundeld" + ] + } + }, + { + "name": "TC_084 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_2027_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2027-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 437500, + "type": "Double" + }, + "plafondHuurders": { + "value": 437500, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2027_Gebundeld", + "selectedRuleIndex": 6, + "reasons": [ + "MC/DC: rule 4 Rule_Platform_2027_Eigenaar_Gescheiden, condition [date(aanvraagDatum) <= date(\"2027-09-30\")] baseline", + "MC/DC: rule 6 Rule_Platform_2027_Gebundeld, condition [date(aanvraagDatum) >= date(\"2027-10-01\")] flipped", + "MC/DC: rule 6 Rule_Platform_2027_Gebundeld, condition [date(aanvraagDatum) <= date(\"2027-12-31\")] flipped" + ] + } + }, + { + "name": "TC_085 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_2027_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2027-10-01", + "type": "String" + }, + "aanvragerType": { + "value": "huurder", + "type": "String" + }, + "plafondEigenaren": { + "value": 437500, + "type": "Double" + }, + "plafondHuurders": { + "value": 437500, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2027_Gebundeld", + "selectedRuleIndex": 6, + "reasons": [ + "MC/DC: rule 5 Rule_Platform_2027_Huurder_Gescheiden, condition [date(aanvraagDatum) <= date(\"2027-09-30\")] baseline" + ] + } + }, + { + "name": "TC_086 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2027_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2027-10-02", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2027_Gebundeld", + "selectedRuleIndex": 6, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2027-10-02'" + ] + } + }, + { + "name": "TC_087 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2027_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2027-12-30", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2027_Gebundeld", + "selectedRuleIndex": 6, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2027-12-30'" + ] + } + }, + { + "name": "TC_088 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_2027_Gebundeld", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=1000000.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2027-12-31", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_2027_Gebundeld", + "selectedRuleIndex": 6, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2027-12-31'" + ] + } + }, + { + "name": "TC_089 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_BuitenAanvraagperiodeOfOnbekendType", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=0.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2028-01-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_BuitenAanvraagperiodeOfOnbekendType", + "selectedRuleIndex": 7, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2028-01-01'" + ] + } + }, + { + "name": "TC_090 Bereken Beschikbaar Subsidie Plafond - MC/DC - Rule_Platform_BuitenAanvraagperiodeOfOnbekendType", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=0.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2028-01-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 437500, + "type": "Double" + }, + "plafondHuurders": { + "value": 437500, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_BuitenAanvraagperiodeOfOnbekendType", + "selectedRuleIndex": 7, + "reasons": [ + "MC/DC: rule 6 Rule_Platform_2027_Gebundeld, condition [date(aanvraagDatum) <= date(\"2027-12-31\")] baseline" + ] + } + }, + { + "name": "TC_091 Bereken Beschikbaar Subsidie Plafond - Boundary/domain value - Rule_Platform_BuitenAanvraagperiodeOfOnbekendType", + "decisionId": "BerekenBeschikbaarSubsidiePlafond", + "decisionName": "Bereken Beschikbaar Subsidie Plafond", + "decisionTableId": "DecisionTable_BerekenBeschikbaarSubsidiePlafond", + "evaluationMode": "direct-table-inputs", + "expected": "beschikbaarSubsidiePlafond=0.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2029-01-01", + "type": "String" + }, + "aanvragerType": { + "value": "eigenaar", + "type": "String" + }, + "plafondEigenaren": { + "value": 1000000, + "type": "Double" + }, + "plafondHuurders": { + "value": 1000000, + "type": "Double" + }, + "reedsGesubsidieerdEigenaren": { + "value": 0, + "type": "Double" + }, + "reedsGesubsidieerdHuurders": { + "value": 0, + "type": "Double" + } + } + }, + "coverage": { + "selectedRuleId": "Rule_Platform_BuitenAanvraagperiodeOfOnbekendType", + "selectedRuleIndex": 7, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2029-01-01'" + ] + } + }, + { + "name": "TC_092 Subsidie Constanten Thuisbatterij - Representative selected rule 1 - Rule_Constanten_Standaard", + "decisionId": "SubsidieConstantenThuisbatterij", + "decisionName": "Subsidie Constanten Thuisbatterij", + "decisionTableId": "DecisionTable_SubsidieConstantenThuisbatterij", + "evaluationMode": "direct-table-inputs", + "expected": "subsidiePercentage=0.25, subsidieMinimum=750.0, subsidieMaximum=1250.0, minimaleNoodzakelijkeKosten=750.0", + "requestBody": { + "variables": {} + }, + "coverage": { + "selectedRuleId": "Rule_Constanten_Standaard", + "selectedRuleIndex": 1, + "reasons": [ + "Representative selected rule 1: Rule_Constanten_Standaard", + "Output minimum: subsidiePercentage=0.25", + "Output maximum: subsidiePercentage=0.25", + "Output near zero boundary: subsidiePercentage=0.25", + "Output minimum: subsidieMinimum=750", + "Output maximum: subsidieMinimum=750", + "Output near zero boundary: subsidieMinimum=750", + "Output minimum: subsidieMaximum=1250", + "Output maximum: subsidieMaximum=1250", + "Output near zero boundary: subsidieMaximum=1250", + "Output minimum: minimaleNoodzakelijkeKosten=750", + "Output maximum: minimaleNoodzakelijkeKosten=750", + "Output near zero boundary: minimaleNoodzakelijkeKosten=750" + ] + } + }, + { + "name": "TC_093 Jaar Gebonden Budget - MC/DC - no matching rule", + "decisionId": "jaarGebondenBudget", + "decisionName": "Jaar Gebonden Budget", + "decisionTableId": "DecisionTable_1bhjh1w", + "evaluationMode": "direct-table-inputs", + "expected": "plafondEigenaren=null, plafondHuurders=null", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2025-12-31", + "type": "String" + } + } + }, + "coverage": { + "selectedRuleId": null, + "selectedRuleIndex": null, + "reasons": [ + "MC/DC: rule 1 DecisionRule_0nfr6kl, condition [date(aanvraagDatum).year == 2026] baseline", + "MC/DC: rule 2 DecisionRule_1e5agpr, condition [date(aanvraagDatum).year == 2027] baseline", + "Boundary/domain value: aanvraagDatum='2025-12-31'" + ] + } + }, + { + "name": "TC_094 Jaar Gebonden Budget - MC/DC - DecisionRule_0nfr6kl", + "decisionId": "jaarGebondenBudget", + "decisionName": "Jaar Gebonden Budget", + "decisionTableId": "DecisionTable_1bhjh1w", + "evaluationMode": "direct-table-inputs", + "expected": "plafondEigenaren=437500.0, plafondHuurders=437500.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-01-01", + "type": "String" + } + } + }, + "coverage": { + "selectedRuleId": "DecisionRule_0nfr6kl", + "selectedRuleIndex": 1, + "reasons": [ + "MC/DC: rule 1 DecisionRule_0nfr6kl, condition [date(aanvraagDatum).year == 2026] flipped", + "Boundary/domain value: aanvraagDatum='2026-01-01'", + "Representative selected rule 1: DecisionRule_0nfr6kl", + "Output minimum: plafondEigenaren=437500", + "Output near zero boundary: plafondEigenaren=437500", + "Output minimum: plafondHuurders=437500", + "Output near zero boundary: plafondHuurders=437500" + ] + } + }, + { + "name": "TC_095 Jaar Gebonden Budget - Boundary/domain value - DecisionRule_0nfr6kl", + "decisionId": "jaarGebondenBudget", + "decisionName": "Jaar Gebonden Budget", + "decisionTableId": "DecisionTable_1bhjh1w", + "evaluationMode": "direct-table-inputs", + "expected": "plafondEigenaren=437500.0, plafondHuurders=437500.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2026-12-31", + "type": "String" + } + } + }, + "coverage": { + "selectedRuleId": "DecisionRule_0nfr6kl", + "selectedRuleIndex": 1, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2026-12-31'" + ] + } + }, + { + "name": "TC_096 Jaar Gebonden Budget - MC/DC - DecisionRule_1e5agpr", + "decisionId": "jaarGebondenBudget", + "decisionName": "Jaar Gebonden Budget", + "decisionTableId": "DecisionTable_1bhjh1w", + "evaluationMode": "direct-table-inputs", + "expected": "plafondEigenaren=437500.0, plafondHuurders=437500.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2027-01-01", + "type": "String" + } + } + }, + "coverage": { + "selectedRuleId": "DecisionRule_1e5agpr", + "selectedRuleIndex": 2, + "reasons": [ + "MC/DC: rule 2 DecisionRule_1e5agpr, condition [date(aanvraagDatum).year == 2027] flipped", + "Boundary/domain value: aanvraagDatum='2027-01-01'", + "Representative selected rule 2: DecisionRule_1e5agpr" + ] + } + }, + { + "name": "TC_097 Jaar Gebonden Budget - Boundary/domain value - DecisionRule_1e5agpr", + "decisionId": "jaarGebondenBudget", + "decisionName": "Jaar Gebonden Budget", + "decisionTableId": "DecisionTable_1bhjh1w", + "evaluationMode": "direct-table-inputs", + "expected": "plafondEigenaren=437500.0, plafondHuurders=437500.0", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2027-12-31", + "type": "String" + } + } + }, + "coverage": { + "selectedRuleId": "DecisionRule_1e5agpr", + "selectedRuleIndex": 2, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2027-12-31'", + "Output maximum: plafondEigenaren=437500", + "Output maximum: plafondHuurders=437500" + ] + } + }, + { + "name": "TC_098 Jaar Gebonden Budget - Boundary/domain value - no matching rule", + "decisionId": "jaarGebondenBudget", + "decisionName": "Jaar Gebonden Budget", + "decisionTableId": "DecisionTable_1bhjh1w", + "evaluationMode": "direct-table-inputs", + "expected": "plafondEigenaren=null, plafondHuurders=null", + "requestBody": { + "variables": { + "aanvraagDatum": { + "value": "2028-01-01", + "type": "String" + } + } + }, + "coverage": { + "selectedRuleId": null, + "selectedRuleIndex": null, + "reasons": [ + "Boundary/domain value: aanvraagDatum='2028-01-01'" + ] + } + } +] diff --git a/examples/organizations/flevoland/thuisbatterij/thuisbatterij-subsidie-flevoland-main.bpmn b/examples/organizations/flevoland/thuisbatterij/thuisbatterij-subsidie-flevoland-main.bpmn new file mode 100644 index 0000000..4d5e1fe --- /dev/null +++ b/examples/organizations/flevoland/thuisbatterij/thuisbatterij-subsidie-flevoland-main.bpmn @@ -0,0 +1,341 @@ + + + + Main AWB-style process for applications for the Flevoland thuisbatterij subsidy. It preserves the phase structure of the provided example and delegates substantive Recht/Hoogte decisioning to the called process ThuisbatterijSubsidieDecisionSubProcess, which can be deployed separately or included in the combined BPMN file. + + Flow_Start_Phase1 + + + Flow_Start_Phase1 + Flow_Phase1_Phase2 + + + + Flow_Phase1_Phase2 + Flow_Phase2_Phase3 + + + + Flow_Phase2_Phase3 + Flow_Phase3_Gateway + + + Flow_Phase3_Gateway + Flow_Complete_Yes + Flow_Complete_No + + + Flow_Complete_No + Flow_MissingInfo_Recheck + + + Flow_MissingInfo_Recheck + Flow_Recheck_Process + Flow_Recheck_Refuse + + + Flow_Recheck_Refuse + Flow_Refuse_Notify + + + + + + + + Flow_Complete_Yes + Flow_Recheck_Process + Flow_Phase45_Phase6 + + + Flow_Phase45_Phase6 + Flow_Refuse_Notify + Flow_Phase6_PaymentGateway + + + Flow_Phase6_PaymentGateway + Flow_Payment_Yes + Flow_Payment_No + + + Flow_Payment_Yes + Flow_PaymentTask_Chain + + + + Flow_Payment_No + Flow_PaymentTask_Chain + Flow_Chain_Yes + Flow_Chain_No + + + Flow_Chain_Yes + Flow_ChainTask_Archives + + + + Flow_Chain_No + Flow_ChainTask_Archives + Flow_ArchivesDMN_Record + + + Flow_ArchivesDMN_Record + Flow_Archive_End + + + + Flow_Archive_End + + + + + + + ${completenessResult.isComplete == true} + + + ${completenessResult.isComplete == false} + + + + ${supplementReceived == true} + + + ${supplementReceived == false} + + + + + + ${paymentRequired == true} + + + ${paymentRequired != true} + + + + ${chainProcessRequired == true} + + + ${chainProcessRequired != true} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/organizations/flevoland/thuisbatterij/thuisbatterij-subsidie-flevoland-subprocess.bpmn b/examples/organizations/flevoland/thuisbatterij/thuisbatterij-subsidie-flevoland-subprocess.bpmn new file mode 100644 index 0000000..6aee8fe --- /dev/null +++ b/examples/organizations/flevoland/thuisbatterij/thuisbatterij-subsidie-flevoland-subprocess.bpmn @@ -0,0 +1,314 @@ + + + + Called subprocess for the substantive subsidy decision. It mirrors the tree-felling permit subprocess pattern: DMN assessment, case review, final decision resolution, and setting process variables. First the DMN decision RechtOpSubsidieThuisbatterij decides eligibility/recht. Only if eligible, the DMN decision BehaalbareHoogteSubsidie calculates the amount/hoogte. + + Flow_Sub_Start_Prepare + + + Flow_Sub_Start_Prepare + Flow_Sub_Prepare_AssessRight + + + + Flow_Sub_Prepare_AssessRight + Flow_Sub_AssessRight_Interpret + + + Flow_Sub_AssessRight_Interpret + Flow_Sub_Interpret_Eligible + + + + Flow_Sub_Interpret_Eligible + Flow_Sub_Eligible_Yes + Flow_Sub_Eligible_No + + + Flow_Sub_Eligible_Yes + Flow_Sub_Amount_Review + + + Flow_Sub_Amount_Review + Flow_Sub_Eligible_No + Flow_Sub_Review_Resolve + + + Flow_Sub_Review_Resolve + Flow_Sub_Resolve_Gateway + + + + Flow_Sub_Resolve_Gateway + Flow_Sub_Final_Yes + Flow_Sub_Final_No + + + Flow_Sub_Final_Yes + Flow_Sub_Granted_End + 0); +execution.setVariable("chainProcessRequired", true); +execution.setVariable("finalMessage", "Uw aanvraag voor subsidie voor een thuisbatterij is toegekend. Het subsidiebedrag bedraagt EUR " + amount + ".");]]> + + + Flow_Sub_Final_No + Flow_Sub_Rejected_End + + + + Flow_Sub_Granted_End + Flow_Sub_Rejected_End + + + + + + + ${eligible == true} + + + ${eligible != true} + + + + + + ${subsidyGranted == true} + + + ${subsidyGranted != true} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/package-lock.json b/package-lock.json index 4db6682..f4cd737 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2418,6 +2418,39 @@ "rdfjs-data-model-test": "bin/test.js" } }, + "node_modules/@rdfjs/dataset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@rdfjs/dataset/-/dataset-2.0.2.tgz", + "integrity": "sha512-6YJx+5n5Uxzq9dd9I0GGcIo6eopZOPfcsAfxSGX5d+YBzDgVa1cbtEBFnaPyPKiQsOm4+Cr3nwypjpg02YKPlA==", + "license": "MIT", + "bin": { + "rdfjs-dataset-test": "bin/test.js" + } + }, + "node_modules/@rdfjs/environment": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rdfjs/environment/-/environment-1.0.0.tgz", + "integrity": "sha512-+S5YjSvfoQR5r7YQCRCCVHvIEyrWia7FJv2gqM3s5EDfotoAQmFeBagApa9c/eQFi5EiNhmBECE5nB8LIxTaHg==", + "license": "MIT" + }, + "node_modules/@rdfjs/namespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@rdfjs/namespace/-/namespace-2.0.1.tgz", + "integrity": "sha512-U85NWVGnL3gWvOZ4eXwUcv3/bom7PAcutSBQqmVWvOaslPy+kDzAJCH1WYBLpdQd4yMmJ+bpJcDl9rcHtXeixg==", + "license": "MIT", + "dependencies": { + "@rdfjs/data-model": "^2.0.1" + } + }, + "node_modules/@rdfjs/namespace/node_modules/@rdfjs/data-model": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@rdfjs/data-model/-/data-model-2.1.1.tgz", + "integrity": "sha512-6mcOI4DjIPS6MOZw23H8oAdujHCk5gippVNQ7mKwliYTvTNh+uqRM91B9OLqhoAoNcQ3t49Dx2ooIMRG9/6ooA==", + "license": "MIT", + "bin": { + "rdfjs-data-model-test": "bin/test.js" + } + }, "node_modules/@rdfjs/parser-n3": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@rdfjs/parser-n3/-/parser-n3-1.1.4.tgz", @@ -2440,6 +2473,36 @@ "node": ">=6" } }, + "node_modules/@rdfjs/term-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@rdfjs/term-map/-/term-map-2.0.2.tgz", + "integrity": "sha512-EJ2FmmdEUsSR/tU1nrizRLWzH24YzhuvesrbUWxC3Fs0ilYNdtTbg0RaFJDUnJF3HkbNBQe8Zrt/uvU/hcKnHg==", + "license": "MIT", + "dependencies": { + "@rdfjs/to-ntriples": "^3.0.1" + } + }, + "node_modules/@rdfjs/term-map/node_modules/@rdfjs/to-ntriples": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@rdfjs/to-ntriples/-/to-ntriples-3.0.1.tgz", + "integrity": "sha512-gjoPAvh4j7AbGMjcDn/8R4cW+d/FPtbfbMM0uQXkyfBFtNUW2iVgrqsgJ65roLc54Y9A2TTFaeeTGSvY9a0HCQ==", + "license": "MIT" + }, + "node_modules/@rdfjs/term-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@rdfjs/term-set/-/term-set-2.0.3.tgz", + "integrity": "sha512-DyXrKWEx+mtAFUZVU7bc3Va6/KZ8PsIp0RVdyWT9jfDgI/HCvNisZaBtAcm+SYTC45o+7WLkbudkk1bfaKVB0A==", + "license": "MIT", + "dependencies": { + "@rdfjs/to-ntriples": "^3.0.1" + } + }, + "node_modules/@rdfjs/term-set/node_modules/@rdfjs/to-ntriples": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@rdfjs/to-ntriples/-/to-ntriples-3.0.1.tgz", + "integrity": "sha512-gjoPAvh4j7AbGMjcDn/8R4cW+d/FPtbfbMM0uQXkyfBFtNUW2iVgrqsgJ65roLc54Y9A2TTFaeeTGSvY9a0HCQ==", + "license": "MIT" + }, "node_modules/@rdfjs/to-ntriples": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@rdfjs/to-ntriples/-/to-ntriples-1.0.2.tgz", @@ -3257,6 +3320,28 @@ "url": "https://github.com/sponsors/ueberdosis" } }, + "node_modules/@tpluscode/rdf-ns-builders": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@tpluscode/rdf-ns-builders/-/rdf-ns-builders-5.0.0.tgz", + "integrity": "sha512-rtMFbArdief+s0z2A3TOb/gNe5O5xn9LDiEpilCf6lGYCUIfyqoOvZY80fS/eILwcF2Mj6cUQN1WBQ+1neJmaw==", + "license": "MIT", + "dependencies": { + "@rdfjs/data-model": "^2.1.0", + "@rdfjs/namespace": "^2.0.1", + "@rdfjs/types": "^2", + "@types/rdfjs__namespace": "^2.0.10", + "@zazuko/prefixes": "^2.3.0" + } + }, + "node_modules/@tpluscode/rdf-ns-builders/node_modules/@rdfjs/data-model": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@rdfjs/data-model/-/data-model-2.1.1.tgz", + "integrity": "sha512-6mcOI4DjIPS6MOZw23H8oAdujHCk5gippVNQ7mKwliYTvTNh+uqRM91B9OLqhoAoNcQ3t49Dx2ooIMRG9/6ooA==", + "license": "MIT", + "bin": { + "rdfjs-data-model-test": "bin/test.js" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", @@ -3800,6 +3885,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/n3": { + "version": "1.26.1", + "resolved": "https://registry.npmjs.org/@types/n3/-/n3-1.26.1.tgz", + "integrity": "sha512-TilYHzpU6ecXVJAbV+6o17Z8ZkWLWx6ZJD3IluaU4RiGHxqjU2or9fopxFHS6iXS6qcl5Mg1K3wSx9L8xxJaJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rdfjs/types": "*", + "@types/node": "*" + } + }, "node_modules/@types/node": { "version": "20.19.27", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz", @@ -3835,6 +3931,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/rdfjs__namespace": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/rdfjs__namespace/-/rdfjs__namespace-2.0.10.tgz", + "integrity": "sha512-xoVzEIOxcpyteEmzaj94MSBbrBFs+vqv05joMhzLEiPRwsBBDnhkdBCaaDxR1Tf7wOW0kB2R1IYe4C3vEBFPgA==", + "license": "MIT", + "dependencies": { + "@rdfjs/types": "*" + } + }, "node_modules/@types/react": { "version": "19.2.7", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", @@ -4245,6 +4350,15 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@vocabulary/sh": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@vocabulary/sh/-/sh-1.1.6.tgz", + "integrity": "sha512-8IfAQoKh57THz8LA2+n1jaY/VC2XaqMNSsJgzBKSSrj20y5PSMAawb6dMsxoLxqDIPBDs1TFRl/9CijUnwbBUA==", + "license": "MIT", + "peerDependencies": { + "@rdfjs/types": "^2.0.0" + } + }, "node_modules/@zazuko/node-fetch": { "version": "2.6.6", "resolved": "https://registry.npmjs.org/@zazuko/node-fetch/-/node-fetch-2.6.6.tgz", @@ -4257,6 +4371,12 @@ "node": "4.x || >=6.0.0" } }, + "node_modules/@zazuko/prefixes": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@zazuko/prefixes/-/prefixes-2.6.1.tgz", + "integrity": "sha512-fbOadP7twxt0ZYT9mgIC+xQMk6f3pYYLI5a/2UJ/mc/ygqb/NoVv2ryK3lTtoi74xwkdpUeDwIuFQSosowzUgg==", + "license": "MIT" + }, "node_modules/abbrev": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", @@ -5564,6 +5684,26 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/clownface": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/clownface/-/clownface-2.0.3.tgz", + "integrity": "sha512-E76TBJ7CgU9+/5paSAvuNdMO+fzFThnvRVtidosktYppYkXM8V7tid8Ezzo8S1OmoWxKUam3yfkZlfCid4OiJQ==", + "license": "MIT", + "dependencies": { + "@rdfjs/data-model": "^2.0.1", + "@rdfjs/environment": "0 - 1", + "@rdfjs/namespace": "^2.0.0" + } + }, + "node_modules/clownface/node_modules/@rdfjs/data-model": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@rdfjs/data-model/-/data-model-2.1.1.tgz", + "integrity": "sha512-6mcOI4DjIPS6MOZw23H8oAdujHCk5gippVNQ7mKwliYTvTNh+uqRM91B9OLqhoAoNcQ3t49Dx2ooIMRG9/6ooA==", + "license": "MIT", + "bin": { + "rdfjs-data-model-test": "bin/test.js" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -12110,6 +12250,55 @@ "node": ">=0.10.0" } }, + "node_modules/rdf-canonize": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/rdf-canonize/-/rdf-canonize-3.4.0.tgz", + "integrity": "sha512-fUeWjrkOO0t1rg7B2fdyDTvngj+9RlUyL92vOdiB7c0FPguWVsniIMjEtHH+meLBO9rzkUlUzBVXgWrjI8P9LA==", + "license": "BSD-3-Clause", + "dependencies": { + "setimmediate": "^1.0.5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/rdf-data-factory": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/rdf-data-factory/-/rdf-data-factory-2.0.2.tgz", + "integrity": "sha512-WzPoYHwQYWvIP9k+7IBLY1b4nIDitzAK4mA37WumAF/Cjvu/KOtYJH9IPZnUTWNSd5K2+pq4vrcE9WZC4sRHhg==", + "license": "MIT", + "dependencies": { + "@rdfjs/types": "^2.0.0" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/rubensworks/" + } + }, + "node_modules/rdf-dataset-ext": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/rdf-dataset-ext/-/rdf-dataset-ext-1.1.0.tgz", + "integrity": "sha512-CH85RfRKN9aSlbju8T7aM8hgCSWMBsh2eh/tGxUUtWMN+waxi6iFDt8/r4PAEmKaEA82guimZJ4ISbmJ2rvWQg==", + "deprecated": "rdf-dataset-ext is deprecated. Switching to rdf-ext is recommended.", + "license": "MIT", + "dependencies": { + "rdf-canonize": "^3.0.0", + "readable-stream": "3 - 4" + } + }, + "node_modules/rdf-literal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/rdf-literal/-/rdf-literal-2.0.0.tgz", + "integrity": "sha512-jlQ+h7EvnXmncmk8OzOYR8T3gNfd4g0LQXbflHkEkancic8dh0Tdt5RiRq8vUFndjIeNHt1RWeA5TAj6rgrtng==", + "license": "MIT", + "dependencies": { + "rdf-data-factory": "^2.0.0" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/rubensworks/" + } + }, "node_modules/rdf-transform-triple-to-quad": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/rdf-transform-triple-to-quad/-/rdf-transform-triple-to-quad-1.0.2.tgz", @@ -12120,6 +12309,45 @@ "readable-stream": "^3.5.0" } }, + "node_modules/rdf-validate-datatype": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/rdf-validate-datatype/-/rdf-validate-datatype-0.2.2.tgz", + "integrity": "sha512-mH9qL8i0WBbZ6HJCA26BB6V+WV2MraKvitez3SV0QegBWVQ4wYO49CgfFBzoAYg6tlnhFXl9MkrOAQ07X2N1FA==", + "license": "MIT", + "dependencies": { + "@rdfjs/term-map": "^2.0.0", + "@tpluscode/rdf-ns-builders": "3 - 5" + } + }, + "node_modules/rdf-validate-shacl": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/rdf-validate-shacl/-/rdf-validate-shacl-0.6.5.tgz", + "integrity": "sha512-rwIibopSixDE8ecA9x0c7oTVxdMWxGiJh7h3uJ+WS2h4lq2nx3DZVO7rJvwa5kZpDq9QEFPoyZINAUyfaaoN4Q==", + "license": "MIT", + "dependencies": { + "@rdfjs/data-model": "^2.1.0", + "@rdfjs/dataset": "^2.0.2", + "@rdfjs/environment": "^1.0.0", + "@rdfjs/namespace": "^2.0.1", + "@rdfjs/term-set": "^2.0.3", + "@rdfjs/types": "1 - 2", + "@vocabulary/sh": "^1.1.6", + "clownface": "^2.0.3", + "debug": "^4.3.2", + "rdf-dataset-ext": "^1.1.0", + "rdf-literal": "^2.0.0", + "rdf-validate-datatype": "^0.2.2" + } + }, + "node_modules/rdf-validate-shacl/node_modules/@rdfjs/data-model": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@rdfjs/data-model/-/data-model-2.1.1.tgz", + "integrity": "sha512-6mcOI4DjIPS6MOZw23H8oAdujHCk5gippVNQ7mKwliYTvTNh+uqRM91B9OLqhoAoNcQ3t49Dx2ooIMRG9/6ooA==", + "license": "MIT", + "bin": { + "rdfjs-data-model-test": "bin/test.js" + } + }, "node_modules/react": { "version": "19.2.3", "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", @@ -14849,6 +15077,7 @@ "version": "0.1.0", "license": "EUPL-1.2", "dependencies": { + "@rdfjs/dataset": "^2.0.2", "axios": "^1.6.5", "cors": "^2.8.5", "dotenv": "^16.3.1", @@ -14856,7 +15085,9 @@ "fast-xml-parser": "^5.3.5", "helmet": "^7.1.0", "libxmljs2": "^0.37.0", + "n3": "^1.26.0", "pg": "^8", + "rdf-validate-shacl": "^0.6.5", "sparql-http-client": "^2.4.1", "winston": "^3.11.0" }, @@ -14865,6 +15096,7 @@ "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/jest": "^29.5.11", + "@types/n3": "^1.26.1", "@types/node": "^20.10.6", "@types/pg": "^8", "@types/supertest": "^6.0.2", diff --git a/packages/backend/eslint.config.js b/packages/backend/eslint.config.js index 5d61067..da8f0bf 100644 --- a/packages/backend/eslint.config.js +++ b/packages/backend/eslint.config.js @@ -21,7 +21,7 @@ module.exports = tseslint.config( languageOptions: { parser: tseslint.parser, parserOptions: { - project: './tsconfig.json', + project: './tsconfig.eslint.json', tsconfigRootDir: __dirname, }, }, diff --git a/packages/backend/package.json b/packages/backend/package.json index 535275d..d02dad8 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -27,6 +27,7 @@ "author": "RONL - Regels Overheid Nederland", "license": "EUPL-1.2", "dependencies": { + "@rdfjs/dataset": "^2.0.2", "axios": "^1.6.5", "cors": "^2.8.5", "dotenv": "^16.3.1", @@ -34,7 +35,9 @@ "fast-xml-parser": "^5.3.5", "helmet": "^7.1.0", "libxmljs2": "^0.37.0", + "n3": "^1.26.0", "pg": "^8", + "rdf-validate-shacl": "^0.6.5", "sparql-http-client": "^2.4.1", "winston": "^3.11.0" }, @@ -43,6 +46,7 @@ "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/jest": "^29.5.11", + "@types/n3": "^1.26.1", "@types/node": "^20.10.6", "@types/pg": "^8", "@types/supertest": "^6.0.2", @@ -68,4 +72,4 @@ "node": ">=20.0.0", "npm": ">=10.0.0" } -} +} \ No newline at end of file diff --git a/packages/backend/scripts/shacl-smoke-merge.ts b/packages/backend/scripts/shacl-smoke-merge.ts new file mode 100644 index 0000000..92288af --- /dev/null +++ b/packages/backend/scripts/shacl-smoke-merge.ts @@ -0,0 +1,68 @@ +// packages/backend/scripts/shacl-smoke-merge.ts +// +// Deterministic smoke for merge-simulated validation. Rather than hitting the live +// SPARQL endpoint (whose data drifts), it injects a fixed "already-published" graph +// via the service's GraphFetcher, so the outcome is stable. Demonstrates the core +// value of merge mode: it catches a collision spread ACROSS publications that +// file-local validation cannot see, because the offending values live in different +// files (one uploaded, one already in the store). +// +// Run from packages/backend: npx ts-node scripts/shacl-smoke-merge.ts + +import { ShaclValidationService } from '../src/services/shacl-validation.service'; + +// Uploaded file: organisation with a SINGLE homepage — clean on its own. +const LOCAL_ORG = ` +@prefix cv: . +@prefix foaf: . +@prefix skos: . +@prefix dct: . + + a cv:PublicOrganisation ; + dct:identifier "Provincie_Flevoland" ; + skos:prefLabel "Provincie Flevoland"@nl ; + foaf:homepage ; + dct:spatial . +`; + +// Already-published graph for the same subject — a DIFFERENT (www.) homepage. +// In production this comes back from the SPARQL CONSTRUCT; here it is fixed. +const PUBLISHED_ORG = ` +@prefix cv: . +@prefix foaf: . + + a cv:PublicOrganisation ; + foaf:homepage . +`; + +const ronlErrors = (r: Awaited>) => + r.layers['ronl-custom'].issues.filter((i) => i.severity === 'error').length; + +async function main(): Promise { + // Inject a fixed published graph instead of calling TriplyDB. + const svc = new ShaclValidationService(async () => PUBLISHED_ORG); + + // Control: file-local sees only the single homepage in the file -> RONL clean. + const local = await svc.validateFile(LOCAL_ORG); + console.log('file-local -> RONL errors:', ronlErrors(local)); + + // Merge: union with the published www. homepage -> two homepages -> maxCount fires. + const merged = await svc.validateMerged(LOCAL_ORG, 'https://example.test/sparql'); + console.log('merge-sim -> RONL errors:', ronlErrors(merged)); + for (const issue of merged.layers['ronl-custom'].issues) { + console.log(` [${issue.severity}] ${issue.code} — ${issue.message}`); + if (issue.location) console.log(` @ ${issue.location}`); + } + + const ok = ronlErrors(local) === 0 && ronlErrors(merged) === 1; + console.log( + `\n${ok ? 'PASS' : 'FAIL'}: file-local clean (got ${ronlErrors(local)}), ` + + `merge catches the cross-publication homepage collision (got ${ronlErrors(merged)}).` + ); + process.exit(ok ? 0 : 1); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/packages/backend/scripts/shacl-smoke-rules.ts b/packages/backend/scripts/shacl-smoke-rules.ts new file mode 100644 index 0000000..e4109a5 --- /dev/null +++ b/packages/backend/scripts/shacl-smoke-rules.ts @@ -0,0 +1,54 @@ +// packages/backend/scripts/shacl-smoke-rules.ts +// +// Smoke test for the rule shapes. Loads fixtures from tests/fixtures/shacl/ and +// asserts on the RONL Custom layer specifically (so it is independent of whether the +// CPSV-AP shapes are vendored — those add their own, separate findings): +// - rule-collision-fail.ttl : three cpsv:Rule blocks under ONE subject URI -> +// 2 RONL errors (dct:title + dct:description uniqueLang). +// - rule-collision-pass.ttl : the same three rules under unique URIs -> 0 RONL errors. +// - cpsv-ap-conformant.ttl : a fully conformant Rule -> valid, 0 errors in both layers. +// +// Run from packages/backend: npx ts-node scripts/shacl-smoke-rules.ts + +import { readFileSync } from 'fs'; +import path from 'path'; +import { shaclValidationService } from '../src/services/shacl-validation.service'; + +const FIXTURES = path.resolve(__dirname, '../tests/fixtures/shacl'); + +async function main(): Promise { + const fail = readFileSync(path.join(FIXTURES, 'rule-collision-fail.ttl'), 'utf8'); + const pass = readFileSync(path.join(FIXTURES, 'rule-collision-pass.ttl'), 'utf8'); + const conformant = readFileSync(path.join(FIXTURES, 'cpsv-ap-conformant.ttl'), 'utf8'); + + console.log('### rule-collision-fail.ttl (must FAIL) ###'); + const rf = await shaclValidationService.validateFile(fail); + console.log('valid:', rf.valid, '| summary:', JSON.stringify(rf.summary)); + for (const issue of rf.layers['ronl-custom'].issues) { + console.log(` [${issue.severity}] ${issue.code} — ${issue.message}`); + if (issue.location) console.log(` @ ${issue.location}`); + } + + console.log('\n### rule-collision-pass.ttl (must PASS the RONL layer) ###'); + const rp = await shaclValidationService.validateFile(pass); + console.log('valid:', rp.valid, '| summary:', JSON.stringify(rp.summary)); + + console.log('\n### cpsv-ap-conformant.ttl (must be valid in both layers) ###'); + const rc = await shaclValidationService.validateFile(conformant); + console.log('valid:', rc.valid, '| summary:', JSON.stringify(rc.summary)); + + const ronlErrors = (r: Awaited>) => + r.layers['ronl-custom'].issues.filter((i) => i.severity === 'error').length; + + const ok = ronlErrors(rf) === 2 && ronlErrors(rp) === 0 && rc.valid && rc.summary.errors === 0; + console.log( + `\n${ok ? 'PASS' : 'FAIL'}: expected RONL fail=2, RONL pass=0, conformant clean ` + + `(got fail=${ronlErrors(rf)}, pass=${ronlErrors(rp)}, conformant valid=${rc.valid}/${rc.summary.errors}E).` + ); + process.exit(ok ? 0 : 1); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/packages/backend/scripts/shacl-smoke.ts b/packages/backend/scripts/shacl-smoke.ts new file mode 100644 index 0000000..1d40c42 --- /dev/null +++ b/packages/backend/scripts/shacl-smoke.ts @@ -0,0 +1,54 @@ +// packages/backend/scripts/shacl-smoke.ts +// +// Milestone-1 smoke test for the SHACL validation service, runnable before the +// HTTP route exists. Feeds the merged Flevoland fixture (one organisation subject +// carrying two divergent foaf:homepage values — the result of unioning File A and +// File B) through validateFile and prints the report. +// +// Asserts on the RONL Custom layer specifically: exactly one error on focus node +// Provincie_Flevoland, path foaf:homepage. Independent of whether the CPSV-AP layer +// is vendored (which, once present, also flags this fixture for missing dct:spatial). +// +// Run from packages/backend: npx ts-node scripts/shacl-smoke.ts + +import { shaclValidationService } from '../src/services/shacl-validation.service'; + +const MERGED_FIXTURE = ` +@prefix cv: . +@prefix foaf: . +@prefix skos: . +@prefix dct: . + + a cv:PublicOrganisation ; + dct:identifier "Provincie_Flevoland" ; + skos:prefLabel "Provincie Flevoland"@nl ; + foaf:homepage ; + foaf:homepage ; + cv:spatial . +`; + +async function main(): Promise { + const result = await shaclValidationService.validateFile(MERGED_FIXTURE); + + console.log('valid :', result.valid); + console.log('parseError:', result.parseError); + console.log('summary :', JSON.stringify(result.summary)); + for (const [key, layer] of Object.entries(result.layers)) { + if (layer.issues.length === 0) continue; + console.log(`\nlayer ${key} (${layer.label}):`); + for (const issue of layer.issues) { + console.log(` [${issue.severity}] ${issue.code} — ${issue.message}`); + if (issue.location) console.log(` @ ${issue.location}`); + } + } + + const ronlErrors = result.layers['ronl-custom'].issues.filter((i) => i.severity === 'error').length; + const ok = ronlErrors === 1; + console.log(`\n${ok ? 'PASS' : 'FAIL'}: expected exactly one error in the RONL Custom layer (got ${ronlErrors}).`); + process.exit(ok ? 0 : 1); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/packages/backend/shapes/cprmv/0.4.1/cprmv.shacl.ttl b/packages/backend/shapes/cprmv/0.4.1/cprmv.shacl.ttl new file mode 100644 index 0000000..108d924 --- /dev/null +++ b/packages/backend/shapes/cprmv/0.4.1/cprmv.shacl.ttl @@ -0,0 +1,284 @@ +@prefix sh: . +@prefix rdf: . +@prefix rdfs: . +@prefix xsd: . +@prefix cpsv: . +@prefix cprmv: . +@prefix skos: . +@prefix schema: . +@prefix dct: . + +cprmv:RuleSetShape + a sh:NodeShape ; + sh:targetClass cprmv:RuleSet ; + sh:property [ + sh:path cprmv:id ; + sh:minCount 1 ; + ] ; + sh:property [ + sh:path cprmv:comment ; + sh:minCount 0 ; + sh:maxCount 1 ; + sh:datatype xsd:string ; + ] ; + sh:property [ + sh:path cprmv:validFrom ; + sh:minCount 1 ; + sh:maxCount 1 ; + sh:datatype xsd:date ; + ] ; + sh:property [ + sh:path cprmv:validUntil ; + sh:minCount 0 ; + sh:maxCount 1 ; + sh:datatype xsd:date ; + ] ; + sh:property [ + sh:path cprmv:publishedOn ; + sh:minCount 0 ; + sh:maxCount 1 ; + sh:datatype xsd:date ; + ] ; + sh:property [ + sh:path cprmv:isBasedOn ; + sh:class cprmv:RuleSet ; + sh:minCount 0 ; + ] ; + sh:property [ + sh:path cprmv:isOutputOf ; + sh:class cpsv:PublicService ; + sh:minCount 1 ; + ] ; + sh:property [ + sh:path cprmv:hasMethod ; + sh:class cprmv:RuleMethod ; + sh:minCount 1 ; + ] ; + sh:property [ + sh:path cprmv:hasPart ; + sh:node cprmv:hasPartListShape ; + sh:minCount 1 ; + sh:maxCount 1 ; + ] . + +cprmv:DecisionModelShape + a sh:NodeShape ; + sh:targetClass cprmv:DecisionModel ; + sh:property [ + sh:path cprmv:hasAnalysis ; + sh:class cprmv:Analysis ; + sh:minCount 0 ; + ] . + +cprmv:RuleShape + a sh:NodeShape ; + sh:targetClass cprmv:Rule ; + sh:property [ + sh:path cprmv:id ; + sh:minCount 1 ; + ] ; + sh:property [ + sh:path cprmv:definition ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:path cprmv:postDefinition ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:path cprmv:sourceQuote ; + sh:minCount 0 ; + sh:maxCount 1 ; + sh:datatype xsd:string ; + ] ; + sh:property [ + sh:path cprmv:comment ; + sh:minCount 0 ; + sh:maxCount 1 ; + sh:datatype xsd:string ; + ] ; + sh:property [ + sh:path cprmv:isBasedOn ; + sh:class cprmv:Rule ; + sh:minCount 0 ; + ] ; + sh:property [ + sh:path cprmv:hasPart ; + sh:node cprmv:hasPartListShape ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] . + +cprmv:RuleMethodShape + a sh:NodeShape ; + sh:targetClass cprmv:RuleMethod ; + sh:property [ + sh:path cprmv:id ; + sh:minCount 1 ; + ] ; + sh:property [ + sh:path cprmv:comment ; + sh:minCount 0 ; + sh:maxCount 1 ; + sh:datatype xsd:string ; + ] . + +cprmv:AnalysisMethod + a sh:NodeShape ; + sh:targetClass cprmv:AnalysisMethod . + +cprmv:FormalisationMethod + a sh:NodeShape ; + sh:targetClass cprmv:FormalisationMethod . + +cprmv:CodificationMethod + a sh:NodeShape ; + sh:targetClass cprmv:CodificationMethod . + +cprmv:ExecutionMethod + a sh:NodeShape ; + sh:targetClass cprmv:ExecutionMethod. + +cprmv:ExplanationMethod + a sh:NodeShape ; + sh:targetClass cprmv:ExplanationMethod . + +cprmv:TestMethod + a sh:NodeShape ; + sh:targetClass cprmv:TestMethod . + +cprmv:PublicationMethod + a sh:NodeShape ; + sh:targetClass cprmv:PublicationMethod . + +cprmv:ReferenceMethod + a sh:NodeShape ; + sh:targetClass cprmv:ReferenceMethod . + +cprmv:hasPartListShape a sh:NodeShape ; + sh:or ( + [ + sh:hasValue rdf:nil ; + ] + [ + sh:property [ + sh:path rdf:first ; + sh:minCount 1 ; + sh:maxCount 1 ; + # Optional: Further constraints on the list members can be added here + sh:class cprmv:Rule ; + ] ; + sh:property [ + sh:path rdf:rest ; + sh:minCount 1 ; + sh:maxCount 1 ; + # Recursively apply ex:ListShape to the rest of the list (see https://www.w3.org/TR/shacl/#shapes-recursion) + # comment the line below if the system processing this shape has no recursive shapes support + sh:node cprmv:hasPartListShape ; + ] ; + ] + ) . + +cprmv:Explanation + a sh:NodeShape ; + sh:targetClass cprmv:Explanation ; + sh:property [ + sh:path cprmv:explains ; + sh:or ( + [ sh:class cprmv:Rule ] + [ sh:class cprmv:RuleSet ] + ) ; + sh:minCount 1 ; + sh:maxCount 1 ; + ] . + +cprmv:TestCase + a sh:NodeShape ; + sh:targetClass cprmv:TestCase . + +cprmv:TestSet + a sh:NodeShape ; + sh:targetClass cprmv:TestSet. + +cprmv:Decision + a sh:NodeShape ; + sh:targetClass cprmv:Decision . + +cprmv:Case + a sh:NodeShape ; + sh:targetClass cprmv:Case . + +cprmv:ParameterWaardeShape + a sh:NodeShape ; + sh:targetClass cprmv:ParameterWaarde ; + sh:property [ + sh:path skos:notation ; + sh:minCount 1 ; + sh:maxCount 1 ; + sh:datatype xsd:string ; + ] ; + sh:property [ + sh:path skos:prefLabel ; + sh:minCount 1 ; + sh:datatype rdf:langString ; + ] ; + sh:property [ + sh:path schema:value ; + sh:minCount 0 ; + sh:maxCount 1 ; + sh:datatype xsd:decimal ; + ] ; + sh:property [ + sh:path schema:unitCode ; + sh:minCount 0 ; + sh:maxCount 1 ; + sh:datatype xsd:string ; + ] ; + sh:property [ + sh:path dct:description ; + sh:minCount 0 ; + sh:maxCount 1 ; + sh:datatype rdf:langString ; + ] ; + sh:property [ + sh:path cprmv:validFrom ; + sh:minCount 0 ; + sh:maxCount 1 ; + sh:datatype xsd:date ; + ] ; + sh:property [ + sh:path cprmv:validUntil ; + sh:minCount 0 ; + sh:maxCount 1 ; + sh:datatype xsd:date ; + ] . + +cprmv:TemporalRuleShape + a sh:NodeShape ; + sh:targetClass cprmv:TemporalRule ; + sh:property [ + sh:path cprmv:validFrom ; + sh:minCount 0 ; + sh:maxCount 1 ; + sh:datatype xsd:date ; + ] ; + sh:property [ + sh:path cprmv:validUntil ; + sh:minCount 0 ; + sh:maxCount 1 ; + sh:datatype xsd:date ; + ] ; + sh:property [ + sh:path cprmv:confidenceLevel ; + sh:minCount 0 ; + sh:maxCount 1 ; + sh:datatype xsd:string ; + ] ; + sh:property [ + sh:path cprmv:isBasedOn ; + sh:class cpsv:Rule ; + sh:minCount 0 ; + ] . + diff --git a/packages/backend/shapes/cpsv-ap/3.2.0/cpsv-ap-SHACL.ttl b/packages/backend/shapes/cpsv-ap/3.2.0/cpsv-ap-SHACL.ttl new file mode 100644 index 0000000..15be211 --- /dev/null +++ b/packages/backend/shapes/cpsv-ap/3.2.0/cpsv-ap-SHACL.ttl @@ -0,0 +1,1264 @@ +@prefix dc: . +@prefix dcat: . +@prefix foaf: . +@prefix org: . +@prefix rdf: . +@prefix rdfs: . +@prefix shacl: . +@prefix skos: . +@prefix xsd: . + + rdfs:member , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + . + + a shacl:NodeShape; + shacl:closed false; + shacl:property , + , + , + , + , + , + ; + shacl:targetClass . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Address.thoroughfare"; + shacl:datatype rdf:langString; + shacl:description "The name of a passage or way through from one location to another."@en; + shacl:name "thoroughfare"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Address.administrativeunitlevel1"; + shacl:datatype rdf:langString; + shacl:description "The name of the uppermost level of the address, almost always a country."@en; + shacl:name "administrative unit level 1"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Address.addressarea"; + shacl:datatype rdf:langString; + shacl:description "The name of a geographic area that groups Addresses."@en; + shacl:name "address area"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Address.administrativeunitlevel2"; + shacl:datatype rdf:langString; + shacl:description "The name of a secondary level/region of the address, usually a county, state or other such area that typically encompasses several localities."@en; + shacl:name "administrative unit level 2"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Address.postname"; + shacl:datatype rdf:langString; + shacl:description "A name created and maintained for postal purposes to identify a subdivision of addresses and postal delivery points."@en; + shacl:name "post name"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Address.locatorname"; + shacl:datatype rdf:langString; + shacl:description "Proper noun(s) applied to the real world entity identified by the locator."@en; + shacl:name "locator name"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Address.fulladdress"; + shacl:datatype rdf:langString; + shacl:description "The complete address written as a string."@en; + shacl:name "full address"@en; + shacl:path . + + a shacl:NodeShape; + shacl:closed false; + shacl:property , + , + , + , + , + ; + shacl:targetClass foaf:Agent . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Agent.name"; + shacl:datatype rdf:langString; + shacl:description "The noun given to the Agent."@en; + shacl:name "name"@en; + shacl:path dc:title . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Agent.participates"; + shacl:class ; + shacl:description "The participation in which an Agent is involved."@en; + shacl:name "participates"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Agent.address"; + shacl:class ; + shacl:description "An Address related to an Agent. Asserting the address relationship implies that the Agent has an Address."@en; + shacl:name "address"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Agent.identifier"; + shacl:description "An Identifier for the Agent."@en; + shacl:minCount 1; + shacl:name "identifier"@en; + shacl:path dc:identifier . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Agent.name"; + shacl:description "The noun given to the Agent."@en; + shacl:minCount 1; + shacl:name "name"@en; + shacl:path dc:title . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Agent.identifier"; + shacl:description "An Identifier for the Agent."@en; + shacl:maxCount 1; + shacl:name "identifier"@en; + shacl:path dc:identifier . + + a shacl:NodeShape; + shacl:closed false; + shacl:property ; + shacl:targetClass . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#BusinessEvent.type"; + shacl:class skos:Concept; + shacl:description "It links an Event to a controlled vocabulary of event types."@en; + shacl:name "type"@en; + shacl:path dc:type . + + a shacl:NodeShape; + shacl:closed false; + shacl:property , + , + , + , + , + , + , + , + , + , + ; + shacl:targetClass . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Channel.type"; + shacl:description "The type of Channel as defined in a controlled vocabulary."@en; + shacl:maxCount 1; + shacl:name "type"@en; + shacl:path dc:type . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Channel.type"; + shacl:class skos:Concept; + shacl:description "The type of Channel as defined in a controlled vocabulary."@en; + shacl:name "type"@en; + shacl:path dc:type . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Channel.processingtime"; + shacl:datatype xsd:duration; + shacl:description "The (estimated) time needed for executing a Public Service which may depend on the Channel chosen."@en; + shacl:name "processing time"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Channel.identifier"; + shacl:description "An Identifier for the Channel."@en; + shacl:minCount 1; + shacl:name "identifier"@en; + shacl:path dc:identifier . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Channel.openinghours"; + shacl:class ; + shacl:description "A time at which the resource is normally available."@en; + shacl:name "opening hours"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Channel.ownedby"; + shacl:class org:Organization; + shacl:description "The owner of a specific Channel through which a Public Service is being delivered."@en; + shacl:name "owned by"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Channel.hasinput"; + shacl:class ; + shacl:description "The property links a Public Service directly to one or more pieces of Evidence."@en; + shacl:name "has input"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Channel.availabilityrestriction"; + shacl:class ; + shacl:description "A time during which the resource is not available."@en; + shacl:name "availability restriction"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Channel.processingtime"; + shacl:description "The (estimated) time needed for executing a Public Service which may depend on the Channel chosen."@en; + shacl:maxCount 1; + shacl:name "processing time"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Channel.identifier"; + shacl:description "An Identifier for the Channel."@en; + shacl:maxCount 1; + shacl:name "identifier"@en; + shacl:path dc:identifier . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Channel.description"; + shacl:datatype rdf:langString; + shacl:description "A free text description of the Channel, for example to describe conditions when to use it."@en; + shacl:name "description"@en; + shacl:path dc:description . + + a shacl:NodeShape; + shacl:closed false; + shacl:targetClass skos:Concept . + + a shacl:NodeShape; + shacl:closed false; + shacl:property ; + shacl:targetClass skos:Collection . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Collection.member"; + shacl:class skos:Concept; + shacl:description "It indicates the Concepts that are part of the Collection."@en; + shacl:name "member"@en; + shacl:path skos:member . + + a shacl:NodeShape; + shacl:closed false; + shacl:targetClass skos:Concept . + + a shacl:NodeShape; + shacl:closed false; + shacl:targetClass . + + a shacl:NodeShape; + shacl:closed false; + shacl:property , + , + ; + shacl:targetClass . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#ContactPoint.openinghours"; + shacl:class ; + shacl:description "A time at which the the resource is normally available."@en; + shacl:name "opening hours"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#ContactPoint.contactpage"; + shacl:class foaf:Document; + shacl:description "A web page that could be used to reach out the Contact Point."@en; + shacl:name "contact page"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#ContactPoint.availabilityrestriction"; + shacl:class ; + shacl:description "A time during which the resource is not available."@en; + shacl:name "availability restriction"@en; + shacl:path . + + a shacl:NodeShape; + shacl:closed false; + shacl:property , + , + , + , + , + , + , + , + , + ; + shacl:targetClass . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Cost.ifaccessedthrough"; + shacl:description "The costs created by the use of different Channels."@en; + shacl:maxCount 1; + shacl:name "if accessed through"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Cost.ifaccessedthrough"; + shacl:class ; + shacl:description "The costs created by the use of different Channels."@en; + shacl:name "if accessed through"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Cost.hasvalue"; + shacl:datatype xsd:double; + shacl:description "A numeric value indicating the amount of the Cost."@en; + shacl:name "has value"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Cost.currency"; + shacl:class skos:Concept; + shacl:description "The currency in which the Cost needs to be paid and the value of the Cost is expressed."@en; + shacl:name "currency"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Cost.isdefinedby"; + shacl:class org:Organization; + shacl:description "It links the Cost class to one or more instances of the Organization class."@en; + shacl:name "is defined by"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Cost.hasvalue"; + shacl:description "A numeric value indicating the amount of the Cost."@en; + shacl:maxCount 1; + shacl:name "has value"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Cost.identifier"; + shacl:description "An Identifier for the Cost."@en; + shacl:minCount 1; + shacl:name "identifier"@en; + shacl:path dc:identifier . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Cost.identifier"; + shacl:description "An Identifier for the Cost."@en; + shacl:maxCount 1; + shacl:name "identifier"@en; + shacl:path dc:identifier . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Cost.currency"; + shacl:description "The currency in which the Cost needs to be paid and the value of the Cost is expressed."@en; + shacl:maxCount 1; + shacl:name "currency"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Cost.description"; + shacl:datatype rdf:langString; + shacl:description "A free text description of the Cost."@en; + shacl:name "description"@en; + shacl:path dc:description . + + a shacl:NodeShape; + shacl:closed false; + shacl:property , + , + , + , + , + , + , + , + ; + shacl:targetClass dcat:Dataset . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Dataset.title"; + shacl:datatype rdf:langString; + shacl:description "A name given to the Dataset."@en; + shacl:name "title"@en; + shacl:path dc:title . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Dataset.publisher"; + shacl:description "The Publisher of the Dataset, i.e. an entity (organisation) responsible for making the Dataset available."@en; + shacl:maxCount 1; + shacl:name "publisher"@en; + shacl:path dc:publisher . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Dataset.description"; + shacl:description "A free-text account of the Dataset. This property can be repeated for parallel language versions of the description."@en; + shacl:minCount 1; + shacl:name "description"@en; + shacl:path dc:description . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Dataset.publisher"; + shacl:description "The Publisher of the Dataset, i.e. an entity (organisation) responsible for making the Dataset available."@en; + shacl:minCount 1; + shacl:name "publisher"@en; + shacl:path dc:publisher . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Dataset.publisher"; + shacl:class foaf:Agent; + shacl:description "The Publisher of the Dataset, i.e. an entity (organisation) responsible for making the Dataset available."@en; + shacl:name "publisher"@en; + shacl:path dc:publisher . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Dataset.haspart"; + shacl:class ; + shacl:description "A related resource that is included either physically or logically in the described resource."@en; + shacl:name "has part"@en; + shacl:path dc:hasPart . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Dataset.description"; + shacl:datatype rdf:langString; + shacl:description "A free-text account of the Dataset. This property can be repeated for parallel language versions of the description."@en; + shacl:name "description"@en; + shacl:path dc:description . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Dataset.title"; + shacl:description "A name given to the Dataset."@en; + shacl:minCount 1; + shacl:name "title"@en; + shacl:path dc:title . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Dataset.landingpage"; + shacl:class foaf:Document; + shacl:description "A web page that provides access to the Dataset, its Distributions and/or additional information."@en; + shacl:name "landing page"@en; + shacl:path dcat:landingPage . + + a shacl:NodeShape; + shacl:closed false; + shacl:targetClass foaf:Document . + + a shacl:NodeShape; + shacl:closed false; + shacl:targetClass xsd:double . + + a shacl:NodeShape; + shacl:closed false; + shacl:targetClass xsd:duration . + + a shacl:NodeShape; + shacl:closed false; + shacl:property , + , + , + , + , + ; + shacl:targetClass . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Event.name"; + shacl:datatype rdf:langString; + shacl:description "The Name (or title) of the Event."@en; + shacl:name "name"@en; + shacl:path dc:title . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Event.identifier"; + shacl:description "An Identifier for the Event."@en; + shacl:minCount 1; + shacl:name "identifier"@en; + shacl:path dc:identifier . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Event.name"; + shacl:description "The Name (or title) of the Event."@en; + shacl:minCount 1; + shacl:name "name"@en; + shacl:path dc:title . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Event.hasrelatedservice"; + shacl:class ; + shacl:description "The Public Service related to the Event."@en; + shacl:name "has related service"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Event.identifier"; + shacl:description "An Identifier for the Event."@en; + shacl:maxCount 1; + shacl:name "identifier"@en; + shacl:path dc:identifier . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Event.description"; + shacl:datatype rdf:langString; + shacl:description "A free text description of the Event."@en; + shacl:name "description"@en; + shacl:path dc:description . + + a shacl:NodeShape; + shacl:closed false; + shacl:property , + , + , + , + , + , + , + , + , + , + , + ; + shacl:targetClass . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Evidence.isconformantto"; + shacl:class ; + shacl:description "Evidence Type that specifies characteristics of the Evidence."@en; + shacl:name "is conformant to"@en; + shacl:path dc:conformsTo . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Evidence.type"; + shacl:description "The type of Evidence as described in a controlled vocabulary."@en; + shacl:maxCount 1; + shacl:name "type"@en; + shacl:path dc:type . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Evidence.name"; + shacl:datatype rdf:langString; + shacl:description "The official Name of the piece of Evidence."@en; + shacl:name "name"@en; + shacl:path dc:title . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Evidence.language"; + shacl:class dc:LinguisticSystem; + shacl:description "The language(s) of the piece of Evidence."@en; + shacl:name "language"@en; + shacl:path dc:language . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Evidence.type"; + shacl:class skos:Concept; + shacl:description "The type of Evidence as described in a controlled vocabulary."@en; + shacl:name "type"@en; + shacl:path dc:type . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Evidence.supportsrequirement"; + shacl:class ; + shacl:description "Requirement for which the Evidence provides proof."@en; + shacl:name "supports requirement"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Evidence.ispartof"; + shacl:class dcat:Dataset; + shacl:description "A related resource in which the described resource is physically or logically included."@en; + shacl:name "is part of"@en; + shacl:path dc:isPartOf . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Evidence.identifier"; + shacl:description "An Identifier for the piece of Evidence."@en; + shacl:minCount 1; + shacl:name "identifier"@en; + shacl:path dc:identifier . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Evidence.name"; + shacl:description "The official Name of the piece of Evidence."@en; + shacl:minCount 1; + shacl:name "name"@en; + shacl:path dc:title . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Evidence.identifier"; + shacl:description "An Identifier for the piece of Evidence."@en; + shacl:maxCount 1; + shacl:name "identifier"@en; + shacl:path dc:identifier . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Evidence.description"; + shacl:datatype rdf:langString; + shacl:description "A free text Description of the piece of Evidence."@en; + shacl:name "description"@en; + shacl:path dc:description . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Evidence.relateddocumentation"; + shacl:class foaf:Document; + shacl:description "The documentation that contains information."@en; + shacl:name "related documentation"@en; + shacl:path foaf:page . + + a shacl:NodeShape; + shacl:closed false; + shacl:property , + , + ; + shacl:targetClass . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#EvidenceType.identifier"; + shacl:description "Unambiguous reference to the Evidence Type."@en; + shacl:minCount 1; + shacl:name "identifier"@en; + shacl:path dc:identifier . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#EvidenceType.identifier"; + shacl:description "Unambiguous reference to the Evidence Type."@en; + shacl:maxCount 1; + shacl:name "identifier"@en; + shacl:path dc:identifier . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#EvidenceType.evidencetypeclassification"; + shacl:class skos:Concept; + shacl:description "Category to which the Evidence Type belongs."@en; + shacl:name "evidence type classification"@en; + shacl:path . + + a shacl:NodeShape; + shacl:closed false; + shacl:property ; + shacl:targetClass . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#LegalResource.related"; + shacl:class ; + shacl:description "Another instance of the Legal Resource class that is related to the particular Legal Resource being described."@en; + shacl:name "related"@en; + shacl:path dc:relation . + + a shacl:NodeShape; + shacl:closed false; + shacl:property ; + shacl:targetClass . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#LifeEvent.type"; + shacl:class skos:Concept; + shacl:description "It links an Event to a controlled vocabulary of event types."@en; + shacl:name "type"@en; + shacl:path dc:type . + + a shacl:NodeShape; + shacl:closed false; + shacl:targetClass dc:LinguisticSystem . + + a shacl:NodeShape; + shacl:closed false; + shacl:targetClass rdfs:Literal . + + a shacl:NodeShape; + shacl:closed false; + shacl:targetClass dc:Location . + + a shacl:NodeShape; + shacl:closed false; + shacl:targetClass org:Organization . + + a shacl:NodeShape; + shacl:closed false; + shacl:property , + , + , + , + , + , + , + ; + shacl:targetClass . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Output.name"; + shacl:datatype rdf:langString; + shacl:description "The official Name of the Output."@en; + shacl:name "name"@en; + shacl:path dc:title . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Output.language"; + shacl:class dc:LinguisticSystem; + shacl:description "The language(s) in which the Output is available. "@en; + shacl:name "language"@en; + shacl:path dc:language . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Output.type"; + shacl:class skos:Concept; + shacl:description "The type of Output as defined in a controlled vocabulary."@en; + shacl:name "type"@en; + shacl:path dc:type . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Output.ispartof"; + shacl:class dcat:Dataset; + shacl:description "A related resource in which the described resource is physically or logically included."@en; + shacl:name "is part of"@en; + shacl:path dc:isPartOf . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Output.identifier"; + shacl:description "An Identifier for the Output."@en; + shacl:minCount 1; + shacl:name "identifier"@en; + shacl:path dc:identifier . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Output.name"; + shacl:description "The official Name of the Output."@en; + shacl:minCount 1; + shacl:name "name"@en; + shacl:path dc:title . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Output.identifier"; + shacl:description "An Identifier for the Output."@en; + shacl:maxCount 1; + shacl:name "identifier"@en; + shacl:path dc:identifier . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Output.description"; + shacl:datatype rdf:langString; + shacl:description "A free text Description of the Output."@en; + shacl:name "description"@en; + shacl:path dc:description . + + a shacl:NodeShape; + shacl:closed false; + shacl:property , + , + , + , + , + , + , + , + ; + shacl:targetClass . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Participation.hasparticipant"; + shacl:description "The Agent involved in the Participation."@en; + shacl:maxCount 1; + shacl:name "has participant"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Participation.identifier"; + shacl:description "The unambiguous structured reference for the Participation."@en; + shacl:minCount 1; + shacl:name "identifier"@en; + shacl:path dc:identifier . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Participation.description"; + shacl:description "A textual explanation of the Participation."@en; + shacl:minCount 1; + shacl:name "description"@en; + shacl:path dc:description . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Participation.role"; + shacl:description "The function of an Agent within a Participation."@en; + shacl:minCount 1; + shacl:name "role"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Participation.hasparticipant"; + shacl:class foaf:Agent; + shacl:description "The Agent involved in the Participation."@en; + shacl:name "has participant"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Participation.identifier"; + shacl:description "The unambiguous structured reference for the Participation."@en; + shacl:maxCount 1; + shacl:name "identifier"@en; + shacl:path dc:identifier . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Participation.role"; + shacl:class skos:Concept; + shacl:description "The function of an Agent within a Participation."@en; + shacl:name "role"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Participation.description"; + shacl:datatype rdf:langString; + shacl:description "A textual explanation of the Participation."@en; + shacl:name "description"@en; + shacl:path dc:description . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Participation.hasparticipant"; + shacl:description "The Agent involved in the Participation."@en; + shacl:minCount 1; + shacl:name "has participant"@en; + shacl:path . + + a shacl:NodeShape; + shacl:closed false; + shacl:property , + , + , + ; + shacl:targetClass . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicOrganization.spatial"; + shacl:description "It links an Organization to the Administrative Region(s) that it covers."@en; + shacl:minCount 1; + shacl:name "spatial"@en; + shacl:path dc:spatial . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicOrganization.preferredlabel"; + shacl:datatype rdf:langString; + shacl:description "A preferred label is used to provide the primary, legally recognised name of the Public Organization, as defined in the ORG Ontology."@en; + shacl:name "preferred label"@en; + shacl:path skos:prefLabel . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicOrganization.spatial"; + shacl:class dc:Location; + shacl:description "It links an Organization to the Administrative Region(s) that it covers."@en; + shacl:name "spatial"@en; + shacl:path dc:spatial . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicOrganization.preferredlabel"; + shacl:description "A preferred label is used to provide the primary, legally recognised name of the Public Organization, as defined in the ORG Ontology."@en; + shacl:minCount 1; + shacl:name "preferred label"@en; + shacl:path skos:prefLabel . + + a shacl:NodeShape; + shacl:closed false; + shacl:property , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + ; + shacl:targetClass . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicService.hasinputtype"; + shacl:class ; + shacl:description "It links a Public Service to one or more instances of the EvidenceType class. "@en; + shacl:name "has input type"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicService.haschannel"; + shacl:class ; + shacl:description "It links the Public Service to any Channel through which an Agent provides, uses or otherwise interacts with the Public Service, such as an online service, phone number or office."@en; + shacl:name "has channel"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicService.status"; + shacl:class skos:Concept; + shacl:description "It indicates whether a Public Service is active, inactive, under development etc. according to a controlled vocabulary."@en; + shacl:name "status"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicService.hascompetentauthority"; + shacl:class ; + shacl:description "It links a Public Service to a Public Organization, which is the responsible Agent for the delivery of the Public Service."@en; + shacl:name "has competent authority"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicService.name"; + shacl:datatype rdf:langString; + shacl:description "The official Name of the Public Service."@en; + shacl:name "name"@en; + shacl:path dc:title . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicService.language"; + shacl:class dc:LinguisticSystem; + shacl:description "The language(s) in which the Public Service is available."@en; + shacl:name "language"@en; + shacl:path dc:language . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicService.status"; + shacl:description "It indicates whether a Public Service is active, inactive, under development etc. according to a controlled vocabulary."@en; + shacl:maxCount 1; + shacl:name "status"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicService.relatedservice"; + shacl:class ; + shacl:description "A Public Service related to the particular instance of the Public Service class."@en; + shacl:name "related service"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicService.sector"; + shacl:class skos:Concept; + shacl:description "The industry or sector a Public Service relates to, or is intended for."@en; + shacl:name "sector"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicService.keyword"; + shacl:datatype rdf:langString; + shacl:description "A keyword, term or phrase to describe the Public Service."@en; + shacl:name "keyword"@en; + shacl:path dcat:keyword . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicService.processingtime"; + shacl:datatype xsd:duration; + shacl:description "The (estimated) time needed for executing a Public Service. The actual information is provided using the ISO8601 syntax for durations. "@en; + shacl:name "processing time"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicService.produces"; + shacl:class ; + shacl:description "It links a Public Service to one or more instances of the Output class, describing the actual result of executing a given Public Service."@en; + shacl:name "produces"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicService.ispartof"; + shacl:class dcat:Dataset; + shacl:description "A related resource in which the described resource is physically or logically included."@en; + shacl:name "is part of"@en; + shacl:path dc:isPartOf . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicService.identifier"; + shacl:description "A formally-issued Identifier for the Public Service."@en; + shacl:minCount 1; + shacl:name "identifier"@en; + shacl:path dc:identifier . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicService.hascost"; + shacl:class ; + shacl:description "It indicates the costs related to the execution of a Public Service for the citizen or business related to the execution of the particular Public Service."@en; + shacl:name "has cost"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicService.contactpoint"; + shacl:class ; + shacl:description "The main contact information of the resource."@en; + shacl:name "contact point"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicService.isclassifiedby"; + shacl:class skos:Concept; + shacl:description "It allows to classify the Public Service with any Concept other than those already foreseen and defined explicitly in the CPSV-AP (Thematic Area, Sector, etc.)."@en; + shacl:name "is classified by"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicService.hasinput"; + shacl:class ; + shacl:description "It links a Public Service to one or more instances of the Evidence class."@en; + shacl:name "has input"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicService.description"; + shacl:description "A free text Description of the Public Service."@en; + shacl:minCount 1; + shacl:name "description"@en; + shacl:path dc:description . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicService.spatial"; + shacl:class dc:Location; + shacl:description "A Public Service is likely to be available only within a given area, typically the area covered by a particular public authority."@en; + shacl:name "spatial"@en; + shacl:path dc:spatial . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicService.addressee"; + shacl:class skos:Concept; + shacl:description "The target recipient of the Public Service."@en; + shacl:name "addressee"@en; + shacl:path dc:audience . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicService.name"; + shacl:description "The official Name of the Public Service."@en; + shacl:minCount 1; + shacl:name "name"@en; + shacl:path dc:title . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicService.processingtime"; + shacl:description "The (estimated) time needed for executing a Public Service. The actual information is provided using the ISO8601 syntax for durations. "@en; + shacl:maxCount 1; + shacl:name "processing time"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicService.requires"; + shacl:class ; + shacl:description "The way a Public Service makes use of other Public Services."@en; + shacl:name "requires"@en; + shacl:path dc:requires . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicService.thematicarea"; + shacl:class skos:Concept; + shacl:description "The Thematic Area of a Public Service."@en; + shacl:name "thematic area"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicService.holdsrequirement"; + shacl:class ; + shacl:description "It links a Public Service to a class that describes the Requirement."@en; + shacl:name "holds requirement"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicService.identifier"; + shacl:description "A formally-issued Identifier for the Public Service."@en; + shacl:maxCount 1; + shacl:name "identifier"@en; + shacl:path dc:identifier . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicService.follows"; + shacl:class ; + shacl:description "It links a Public Service to the Rule(s) under which it operates."@en; + shacl:name "follows"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicService.isgroupedby"; + shacl:class ; + shacl:description "It links the Public Service to the Event class."@en; + shacl:name "is grouped by"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicService.description"; + shacl:datatype rdf:langString; + shacl:description "A free text Description of the Public Service."@en; + shacl:name "description"@en; + shacl:path dc:description . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicService.functionsofgovernment"; + shacl:class skos:Concept; + shacl:description "The purpose of a government activity, which the public service is intended for."@en; + shacl:name "functions of government"@en; + shacl:path dc:type . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicService.hascompetentauthority"; + shacl:description "It links a Public Service to a Public Organization, which is the responsible Agent for the delivery of the Public Service."@en; + shacl:minCount 1; + shacl:name "has competent authority"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicService.hasparticipation"; + shacl:class ; + shacl:description "The way how a resource is organized."@en; + shacl:name "has participation"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#PublicService.haslegalresource"; + shacl:class ; + shacl:description """It indicates the Legal Resource (e.g. legislation) to which the Public Service relates, operates or has its legal basis. """@en; + shacl:name "has legal resource"@en; + shacl:path . + + a shacl:NodeShape; + shacl:closed false; + shacl:property , + , + , + , + , + , + ; + shacl:targetClass . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Requirement.fulfils"; + shacl:class ; + shacl:description "The Rules that the requirements fulfils."@en; + shacl:name "fulfils"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Requirement.name"; + shacl:datatype rdf:langString; + shacl:description "Name of the Requirement."@en; + shacl:name "name"@en; + shacl:path dc:title . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Requirement.type"; + shacl:class skos:Concept; + shacl:description "Category to which the Requirement belongs."@en; + shacl:name "type"@en; + shacl:path dc:type . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Requirement.identifier"; + shacl:description "Unambiguous reference to a Requirement."@en; + shacl:minCount 1; + shacl:name "identifier"@en; + shacl:path dc:identifier . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Requirement.name"; + shacl:description "Name of the Requirement."@en; + shacl:minCount 1; + shacl:name "name"@en; + shacl:path dc:title . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Requirement.hassupportingevidence"; + shacl:class ; + shacl:description "Evidence that supplies information, proof or support for the Requirement."@en; + shacl:name "has supporting evidence"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Requirement.identifier"; + shacl:description "Unambiguous reference to a Requirement."@en; + shacl:maxCount 1; + shacl:name "identifier"@en; + shacl:path dc:identifier . + + a shacl:NodeShape; + shacl:closed false; + shacl:property , + , + , + , + , + , + , + , + , + ; + shacl:targetClass . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Rule.type"; + shacl:description "Type of Rule."@en; + shacl:maxCount 1; + shacl:name "type"@en; + shacl:path dc:type . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Rule.name"; + shacl:datatype rdf:langString; + shacl:description "The Name of the Rule."@en; + shacl:name "name"@en; + shacl:path dc:title . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Rule.language"; + shacl:class dc:LinguisticSystem; + shacl:description "The language(s) in which the Rule is available."@en; + shacl:name "language"@en; + shacl:path dc:language . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Rule.type"; + shacl:class skos:Concept; + shacl:description "Type of Rule."@en; + shacl:name "type"@en; + shacl:path dc:type . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Rule.implements"; + shacl:class ; + shacl:description """It links a Rule to relevant legislation or policy documents i.e. +the Legal Resource under which the Rules are being defined."""@en; + shacl:name "implements"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Rule.identifier"; + shacl:description "An Identifier for the Rule."@en; + shacl:minCount 1; + shacl:name "identifier"@en; + shacl:path dc:identifier . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Rule.description"; + shacl:description "A free text Description of the Rule."@en; + shacl:minCount 1; + shacl:name "description"@en; + shacl:path dc:description . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Rule.name"; + shacl:description "The Name of the Rule."@en; + shacl:minCount 1; + shacl:name "name"@en; + shacl:path dc:title . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Rule.identifier"; + shacl:description "An Identifier for the Rule."@en; + shacl:maxCount 1; + shacl:name "identifier"@en; + shacl:path dc:identifier . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#Rule.description"; + shacl:datatype rdf:langString; + shacl:description "A free text Description of the Rule."@en; + shacl:name "description"@en; + shacl:path dc:description . + + a shacl:NodeShape; + shacl:closed false; + shacl:property , + , + , + , + , + , + , + , + , + , + ; + shacl:targetClass . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#ServiceConcessionContract.hascontractingauthority"; + shacl:class ; + shacl:description "It links a Service Concession Contract with the Contracting Authority which is ultimately responsible for a public service."@en; + shacl:name "has contracting authority"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#ServiceConcessionContract.name"; + shacl:datatype rdf:langString; + shacl:description "The Name of the Service Concession Contract."@en; + shacl:name "name"@en; + shacl:path dc:title . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#ServiceConcessionContract.haseconomicoperator"; + shacl:description "It links a Service Concession Contract with the Economic Operator in charge for the provision and the management of public services."@en; + shacl:minCount 1; + shacl:name "has economic operator"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#ServiceConcessionContract.identifier"; + shacl:description "An Identifier for the Service Concession Contract."@en; + shacl:minCount 1; + shacl:name "identifier"@en; + shacl:path dc:identifier . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#ServiceConcessionContract.description"; + shacl:description "A free text description of the Service Concession Contract."@en; + shacl:minCount 1; + shacl:name "description"@en; + shacl:path dc:description . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#ServiceConcessionContract.name"; + shacl:description "The Name of the Service Concession Contract."@en; + shacl:minCount 1; + shacl:name "name"@en; + shacl:path dc:title . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#ServiceConcessionContract.haseconomicoperator"; + shacl:class org:Organization; + shacl:description "It links a Service Concession Contract with the Economic Operator in charge for the provision and the management of public services."@en; + shacl:name "has economic operator"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#ServiceConcessionContract.identifier"; + shacl:description "An Identifier for the Service Concession Contract."@en; + shacl:maxCount 1; + shacl:name "identifier"@en; + shacl:path dc:identifier . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#ServiceConcessionContract.description"; + shacl:datatype rdf:langString; + shacl:description "A free text description of the Service Concession Contract."@en; + shacl:name "description"@en; + shacl:path dc:description . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#ServiceConcessionContract.establishedunder"; + shacl:class ; + shacl:description """It links a Service Concession Contract to relevant legislation or policy documents i.e. +the Legal Resource under which the Service Concession Contracts are being defined."""@en; + shacl:name "established under"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#ServiceConcessionContract.hascontractingauthority"; + shacl:description "It links a Service Concession Contract with the Contracting Authority which is ultimately responsible for a public service."@en; + shacl:minCount 1; + shacl:name "has contracting authority"@en; + shacl:path . + + a shacl:NodeShape; + shacl:closed false; + shacl:property , + ; + shacl:targetClass . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#TemporalEntity.frequency"; + shacl:class skos:Concept; + shacl:description "The recurrence of an instant or period."@en; + shacl:name "frequency"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/CPSV-AP/releases/3.2.0/#TemporalEntity.description"; + shacl:datatype rdf:langString; + shacl:description "A textual representation of the Temporal Entity."@en; + shacl:name "description"@en; + shacl:path dc:description . + + a shacl:NodeShape; + shacl:closed false; + shacl:targetClass rdf:langString . diff --git a/packages/backend/shapes/ronl/org-uniqueness.ttl b/packages/backend/shapes/ronl/org-uniqueness.ttl new file mode 100644 index 0000000..b4caa60 --- /dev/null +++ b/packages/backend/shapes/ronl/org-uniqueness.ttl @@ -0,0 +1,52 @@ +@prefix sh: . +@prefix cv: . +@prefix foaf: . +@prefix skos: . +@prefix dct: . +@prefix ronl: . + +# Uniqueness constraints on cv:PublicOrganisation identity properties. +# +# Motivation: the same organisation subject URI is declared across multiple +# published TTL files. When those files are merged into TriplyDB, divergent +# values for a single-valued property silently produce multiple triples on the +# same subject, which then fan out into duplicate rows in any SPARQL result that +# selects that property. The concrete case this guards against is two different +# foaf:homepage values (with and without "www.") on Provincie_Flevoland. +# +# foaf:homepage / dct:identifier / cv:spatial are genuinely single-valued, so +# sh:maxCount 1 applies. skos:prefLabel is multilingual in this project (EN + NL), +# so the correct constraint is sh:uniqueLang true — at most one label per language +# — rather than sh:maxCount 1, which would reject legitimate bilingual labels. + +ronl:PublicOrganisationUniquenessShape + a sh:NodeShape ; + sh:targetClass cv:PublicOrganisation ; + + sh:property [ + sh:path foaf:homepage ; + sh:maxCount 1 ; + sh:severity sh:Violation ; + sh:message "A public organisation must have at most one foaf:homepage. Divergent homepage values across publications fan out into duplicate rows when merged."@en + ] ; + + sh:property [ + sh:path dct:identifier ; + sh:maxCount 1 ; + sh:severity sh:Violation ; + sh:message "A public organisation must have at most one dct:identifier."@en + ] ; + + sh:property [ + sh:path cv:spatial ; + sh:maxCount 1 ; + sh:severity sh:Violation ; + sh:message "A public organisation must have at most one cv:spatial."@en + ] ; + + sh:property [ + sh:path skos:prefLabel ; + sh:uniqueLang true ; + sh:severity sh:Violation ; + sh:message "A public organisation must have at most one skos:prefLabel per language."@en + ] . diff --git a/packages/backend/shapes/ronl/rule-uniqueness.ttl b/packages/backend/shapes/ronl/rule-uniqueness.ttl new file mode 100644 index 0000000..5c3896c --- /dev/null +++ b/packages/backend/shapes/ronl/rule-uniqueness.ttl @@ -0,0 +1,30 @@ +@prefix sh: . +@prefix cpsv: . +@prefix dct: . +@prefix ronl: . + +# A cpsv:Rule with more than one dct:title or dct:description in the same language +# means distinct rules were authored under the same subject URI and got merged. In +# LDE SPARQL this fans out as (number of titles × number of descriptions) duplicate +# rows. dct:title / dct:description use sh:uniqueLang true (at most one value per +# language), consistent with skos:prefLabel in org-uniqueness.ttl: this catches the +# collision — whose merged duplicates share a language — while tolerating legitimate +# bilingual (@nl + @en) labels. + +ronl:RuleUniquenessShape + a sh:NodeShape ; + sh:targetClass cpsv:Rule ; + + sh:property [ + sh:path dct:title ; + sh:uniqueLang true ; + sh:severity sh:Violation ; + sh:message "A cpsv:Rule must have at most one dct:title per language. Multiple same-language titles indicate distinct rules merged under the same subject URI, fanning out in SPARQL results."@en + ] ; + + sh:property [ + sh:path dct:description ; + sh:uniqueLang true ; + sh:severity sh:Violation ; + sh:message "A cpsv:Rule must have at most one dct:description per language. Multiple same-language descriptions indicate distinct rules merged under the same subject URI."@en + ] . diff --git a/packages/backend/src/db/migrate.ts b/packages/backend/src/db/migrate.ts index 1783f7b..0904058 100644 --- a/packages/backend/src/db/migrate.ts +++ b/packages/backend/src/db/migrate.ts @@ -36,7 +36,8 @@ export async function migrate(): Promise { ADD COLUMN IF NOT EXISTS deployed_forms TEXT[] NOT NULL DEFAULT '{}', ADD COLUMN IF NOT EXISTS deployed_documents TEXT[] NOT NULL DEFAULT '{}', ADD COLUMN IF NOT EXISTS language VARCHAR(2), - ADD COLUMN IF NOT EXISTS organization VARCHAR(100); + ADD COLUMN IF NOT EXISTS organization VARCHAR(100), + ADD COLUMN IF NOT EXISTS board_owner VARCHAR(50); CREATE INDEX IF NOT EXISTS idx_pd_bpmn_process_id ON process_definitions (bpmn_process_id); diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index cf1002c..ef6ab1d 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -138,4 +138,4 @@ process.on('SIGINT', () => { // Start the server startServer(); -export default app; \ No newline at end of file +export default app; diff --git a/packages/backend/src/routes/assets.routes.ts b/packages/backend/src/routes/assets.routes.ts index cc9ba93..65e00b6 100644 --- a/packages/backend/src/routes/assets.routes.ts +++ b/packages/backend/src/routes/assets.routes.ts @@ -78,13 +78,15 @@ router.patch('/bpmn/:id/deploy', async (req: Request, res: Response) => { operatonUrl, formIds = [], documentIds = [], + boardOwner, } = req.body as { deploymentId: string; operatonUrl?: string; formIds?: string[]; documentIds?: string[]; + boardOwner?: string; }; - await markDeployed(req.params.id, deploymentId, operatonUrl, formIds, documentIds); + await markDeployed(req.params.id, deploymentId, operatonUrl, formIds, documentIds, boardOwner); res.json({ success: true }); } catch (err) { logger.error('[assets] markDeployed failed', { error: getErrorMessage(err) }); diff --git a/packages/backend/src/routes/dmn.routes.ts b/packages/backend/src/routes/dmn.routes.ts index 67fd70a..8b85c2a 100644 --- a/packages/backend/src/routes/dmn.routes.ts +++ b/packages/backend/src/routes/dmn.routes.ts @@ -44,11 +44,21 @@ router.get('/', async (req: Request, res: Response) => { // Pass endpoint and refresh to sparqlService const dmns = await sparqlService.getAllDmns(requestedEndpoint, refresh); + // Enrich each DMN with a relative URL clients can use to download the + // deployed DMN XML from Operaton. The handler lives at + // `GET /v1/dmns/:identifier/xml` (also reachable via the legacy + // `/api/dmns` alias). Relative URLs keep the response portable across + // local / ACC / PROD without the backend needing to know its own host. + const dmnsWithLinks = dmns.map((dmn) => ({ + ...dmn, + xmlUrl: `/v1/dmns/${encodeURIComponent(dmn.identifier)}/xml`, + })); + res.json({ success: true, data: { - total: dmns.length, - dmns, + total: dmnsWithLinks.length, + dmns: dmnsWithLinks, fromCache: !refresh, // Helpful for debugging }, timestamp: new Date().toISOString(), @@ -226,6 +236,7 @@ router.post('/process/deploy', async (req: Request, res: Response) => { operatonUrl, operatonUsername, operatonPassword, + boardOwner, } = req.body as { bpmnXml: string; deploymentName: string; @@ -235,6 +246,8 @@ router.post('/process/deploy', async (req: Request, res: Response) => { operatonUrl?: string; operatonUsername?: string; operatonPassword?: string; + /** Owning board for the deployed process; auto-derived from candidate groups when omitted. */ + boardOwner?: string; }; if (!bpmnXml?.trim()) { @@ -261,7 +274,8 @@ router.post('/process/deploy', async (req: Request, res: Response) => { documents, operatonUrl, operatonUsername, - operatonPassword + operatonPassword, + boardOwner ); res.json({ @@ -282,6 +296,60 @@ router.post('/process/deploy', async (req: Request, res: Response) => { } }); +/** + * GET /v1/dmns/:identifier/xml + * Fetch the deployed DMN XML content from Operaton. + * + * Mirrors the handler in `dmn-xml.routes.ts` (still mounted at the legacy + * `/api/dmns` path in `index.ts` for backward compatibility) but exposes the + * route under the canonical `/v1/dmns` mount via the registry, and uses the + * `:identifier` parameter name to match the convention of the surrounding + * routes. The `identifier` value is passed verbatim as the Operaton decision + * definition key — for RONL DMNs deployed via this platform these are + * equivalent by convention. + * + * The response is `application/xml` with `Content-Disposition: attachment` + * so browsers save the file as `.dmn`. Declared before the + * `GET /:identifier` route below to match the "more specific first" pattern + * used elsewhere in this file, though strictly speaking the segment counts + * differ so Express would not confuse them either way. + */ +router.get('/:identifier/xml', async (req: Request, res: Response) => { + const { identifier } = req.params; + + logger.info('DMN XML download request', { identifier }); + + try { + const dmnXml = await operatonService.fetchDmnXml(identifier); + + if (!dmnXml) { + return res.status(404).json({ + success: false, + error: { + code: 'DMN_NOT_FOUND', + message: `DMN definition not found in Operaton: ${identifier}`, + }, + timestamp: new Date().toISOString(), + } as ApiResponse); + } + + res.setHeader('Content-Type', 'application/xml'); + res.setHeader('Content-Disposition', `attachment; filename="${identifier}.dmn"`); + res.send(dmnXml); + } catch (error: unknown) { + const errorDetails = getErrorDetails(error); + logger.error('DMN XML download error', errorDetails); + res.status(500).json({ + success: false, + error: { + code: 'DMN_FETCH_FAILED', + message: getErrorMessage(error), + }, + timestamp: new Date().toISOString(), + } as ApiResponse); + } +}); + /** * GET /api/dmns/:identifier * Get a specific DMN by identifier @@ -311,9 +379,15 @@ router.get('/:identifier', async (req: Request, res: Response) => { } as ApiResponse); } + // Enrich with the XML download link (same convention as the list endpoint). + const dmnWithLink = { + ...dmn, + xmlUrl: `/v1/dmns/${encodeURIComponent(identifier)}/xml`, + }; + res.json({ success: true, - data: dmn, + data: dmnWithLink, timestamp: new Date().toISOString(), } as ApiResponse); } catch (error: unknown) { diff --git a/packages/backend/src/routes/dso.routes.ts b/packages/backend/src/routes/dso.routes.ts index 92200d1..05016f1 100644 --- a/packages/backend/src/routes/dso.routes.ts +++ b/packages/backend/src/routes/dso.routes.ts @@ -8,7 +8,8 @@ import packageJson from '../../package.json'; const router = Router(); const getEnv = (req: Request): 'pre' | 'prod' => { - return req.headers['x-dso-env'] === 'prod' ? 'prod' : 'pre'; + if (req.headers['x-dso-env'] === 'prod' || req.query['env'] === 'prod') return 'prod'; + return 'pre'; }; /** @@ -222,4 +223,90 @@ router.get('/werkzaamheden/:urn', async (req: Request, res: Response) => { } }); +/** + * GET /v1/dso/toepasbare-regels + * Fetch metadata for toepasbare regels by functioneleStructuurRef. + * + * Query params: + * functioneleStructuurRef — full concept URI (required) + */ +router.get('/toepasbare-regels', async (req: Request, res: Response) => { + res.set('API-Version', packageJson.version); + const { functioneleStructuurRef } = req.query; + if (!functioneleStructuurRef || typeof functioneleStructuurRef !== 'string') { + return res.status(400).json({ success: false, error: 'functioneleStructuurRef is required' }); + } + try { + const data = await dsoService.getToepasbareRegels(functioneleStructuurRef, getEnv(req)); + res.status(200).json({ success: true, data }); + } catch (error) { + const msg = error instanceof Error ? error.message : 'DSO request failed'; + const status = msg.includes('404') ? 404 : 502; + logger.error('[DSO Routes] GET /toepasbare-regels failed', { error: msg }); + res.status(status).json({ success: false, error: msg }); + } +}); + +/** + * GET /v1/dso/toepasbare-regels/:id/sttr + * Download the raw STTR XML for a toepasbare regel. + */ +router.get('/toepasbare-regels/:id/sttr', async (req: Request, res: Response) => { + res.set('API-Version', packageJson.version); + try { + const xml = await dsoService.getSttrBestand(req.params.id, getEnv(req)); + res.set('Content-Type', 'application/xml'); + res.set('Content-Disposition', `attachment; filename="sttr-${req.params.id}.xml"`); + res.status(200).send(xml); + } catch (error) { + const msg = error instanceof Error ? error.message : 'DSO request failed'; + const status = msg.includes('404') ? 404 : 502; + logger.error('[DSO Routes] GET /toepasbare-regels/:id/sttr failed', { error: msg }); + res.status(status).json({ success: false, error: msg }); + } +}); + +/** + * GET /v1/dso/toepasbare-regels/:id/dmn + * Extract the embedded DMN from a conclusie STTR and return it + * as a standalone DMN XML file ready for import into LDE or deployment to Operaton. + */ +router.get('/toepasbare-regels/:id/dmn', async (req: Request, res: Response) => { + res.set('API-Version', packageJson.version); + try { + const xml = await dsoService.getSttrBestand(req.params.id, getEnv(req)); + const dmn = dsoService.extractDmnFromSttr(xml); + res.set('Content-Type', 'application/xml'); + res.set('Content-Disposition', `attachment; filename="decision-${req.params.id}.dmn"`); + res.status(200).send(dmn); + } catch (error) { + const msg = error instanceof Error ? error.message : 'DMN extraction failed'; + const status = msg.includes('404') ? 404 : msg.includes('No DMN') ? 422 : 502; + logger.error('[DSO Routes] GET /toepasbare-regels/:id/dmn failed', { error: msg }); + res.status(status).json({ success: false, error: msg }); + } +}); + +/** + * GET /v1/dso/toepasbare-regels/:id/form-scaffold + * Generate a best-effort form-js field scaffold from an indieningsvereisten STTR. + * + * Query params: + * formId — desired form-js schema id (optional, defaults to the toepasbare-regel id) + */ +router.get('/toepasbare-regels/:id/form-scaffold', async (req: Request, res: Response) => { + res.set('API-Version', packageJson.version); + try { + const xml = await dsoService.getSttrBestand(req.params.id, getEnv(req)); + const formId = (req.query.formId as string | undefined) ?? req.params.id; + const scaffold = dsoService.extractFormScaffoldFromSttr(xml, formId); + res.status(200).json({ success: true, data: scaffold }); + } catch (error) { + const msg = error instanceof Error ? error.message : 'Form scaffold extraction failed'; + const status = msg.includes('404') ? 404 : 502; + logger.error('[DSO Routes] GET /toepasbare-regels/:id/form-scaffold failed', { error: msg }); + res.status(status).json({ success: false, error: msg }); + } +}); + export default router; diff --git a/packages/backend/src/routes/index.ts b/packages/backend/src/routes/index.ts index 32b0aa8..3521442 100644 --- a/packages/backend/src/routes/index.ts +++ b/packages/backend/src/routes/index.ts @@ -1,4 +1,3 @@ - // packages/backend/src/routes/index.ts // Mounts v1 routes from the shared registry plus legacy /api/* deprecation // aliases. The v1 list is intentionally not maintained here — see registry.ts. @@ -48,4 +47,4 @@ router.use('/api/chains', deprecationMiddleware('/v1/chains'), chainRoutes); router.use('/api/triplydb', deprecationMiddleware('/v1/triplydb'), triplydbRoutes); router.use('/api/vendors', deprecationMiddleware('/v1/vendors'), vendorRoutes); -export default router; \ No newline at end of file +export default router; diff --git a/packages/backend/src/routes/registry.ts b/packages/backend/src/routes/registry.ts index d530d4f..9868fab 100644 --- a/packages/backend/src/routes/registry.ts +++ b/packages/backend/src/routes/registry.ts @@ -20,12 +20,14 @@ import ropaPublicRoutes from './ropa.public.routes'; import assetsPublicRoutes from './assets.public.routes'; import dsoRoutes from './dso.routes'; import normsRoutes from './norms.routes'; +import shaclRoutes from './shacl.routes'; /** Logical grouping for the root page. New categories can be added; see * CATEGORY_ORDER in utils/rootView.ts for render order. */ export type RouteCategory = | 'Health & monitoring' | 'Discovery' + | 'Validation' | 'Execution' | 'Assets' | 'Integrations'; @@ -88,6 +90,14 @@ export const routeRegistry: ReadonlyArray = [ category: 'Discovery', }, + // Validation + { + mount: '/v1/shacl', + router: shaclRoutes, + summary: 'SHACL validation of CPSV-AP Turtle (file-local and merge-simulated)', + category: 'Validation', + }, + // Execution (templates registered before /v1/chains to win route precedence) { mount: '/v1/chains/templates', @@ -155,4 +165,4 @@ export const routeRegistry: ReadonlyArray = [ summary: 'Vendor implementation discovery', category: 'Integrations', }, -]; \ No newline at end of file +]; diff --git a/packages/backend/src/routes/shacl.routes.ts b/packages/backend/src/routes/shacl.routes.ts new file mode 100644 index 0000000..cac1408 --- /dev/null +++ b/packages/backend/src/routes/shacl.routes.ts @@ -0,0 +1,137 @@ +// packages/backend/src/routes/shacl.routes.ts +// SHACL validation endpoints. Validate CPSV-AP 3.2.0 (+ RONL custom) Turtle either +// stand-alone (file-local) or merged with the already-published graph for the +// file's subjects (catches multi-value fan-out before publish). The response shape +// mirrors /v1/dmns/validate so the frontend reuses the same layer/issue rendering. + +import { Router, Request, Response } from 'express'; +import logger from '../utils/logger'; +import { ApiResponse } from '../types/api.types'; +import { getErrorMessage, getErrorDetails } from '../utils/errors'; +import { shaclValidationService } from '../services/shacl-validation.service'; + +const router = Router(); + +/** + * POST /v1/shacl/validate + * Validate a Turtle file against the vendored shape set (file-local; no network). + * + * Request body (JSON): + * { "content": "" } + * + * Response (JSON): ApiResponse — same shape as + * /v1/dmns/validate: + * { + * "valid": boolean, + * "parseError": string | null, + * "layers": { + * "cpsv-ap-core": { "label": "CPSV-AP Core", "issues": Issue[] }, + * "cpsv-ap-vocab": { "label": "CPSV-AP Vocabularies", "issues": Issue[] }, + * "ronl-custom": { "label": "RONL Custom", "issues": Issue[] } + * }, + * "summary": { "errors": number, "warnings": number, "infos": number } + * } + * + * This endpoint is unauthenticated and performs no TriplyDB/Operaton calls. The + * 10 MB body limit is enforced by express.json() in index.ts. + */ +router.post('/validate', async (req: Request, res: Response) => { + try { + const { content } = req.body as { content?: string }; + + if (!content || typeof content !== 'string') { + return res.status(400).json({ + success: false, + error: { + code: 'INVALID_REQUEST', + message: 'Request body must contain a "content" field with the Turtle as a string.', + }, + timestamp: new Date().toISOString(), + } as ApiResponse); + } + + logger.info('[SHACL Validate] Validation requested', { contentLength: content.length }); + + const result = await shaclValidationService.validateFile(content); + + logger.info('[SHACL Validate] Complete', { + valid: result.valid, + errors: result.summary.errors, + warnings: result.summary.warnings, + }); + + res.json({ + success: true, + data: result, + timestamp: new Date().toISOString(), + } as ApiResponse); + } catch (error: unknown) { + logger.error('[SHACL Validate] Unexpected error', getErrorDetails(error)); + res.status(500).json({ + success: false, + error: { code: 'VALIDATION_ERROR', message: getErrorMessage(error) }, + timestamp: new Date().toISOString(), + } as ApiResponse); + } +}); + +/** + * POST /v1/shacl/validate-merged + * Validate a Turtle file unioned with the triples already published for its + * subjects on a SPARQL endpoint. This is the only mode that catches fan-out + * against live data (e.g. a divergent foaf:homepage already present in the store). + * + * Request body (JSON): + * { "content": "", "endpoint"?: "" } + * `endpoint` is optional and defaults to the configured TriplyDB endpoint. + * + * Response (JSON): ApiResponse (same shape as /validate). + * + * A parse failure of the uploaded `content` returns 200 with parseError set + * (valid:false); a failure fetching/parsing the remote graph is a 500, since that + * is not the caller's input fault. + */ +router.post('/validate-merged', async (req: Request, res: Response) => { + try { + const { content, endpoint } = req.body as { content?: string; endpoint?: string }; + + if (!content || typeof content !== 'string') { + return res.status(400).json({ + success: false, + error: { + code: 'INVALID_REQUEST', + message: 'Request body must contain a "content" field with the Turtle as a string.', + }, + timestamp: new Date().toISOString(), + } as ApiResponse); + } + + logger.info('[SHACL Validate] Merged validation requested', { + contentLength: content.length, + endpoint: endpoint ?? '(default)', + }); + + const result = await shaclValidationService.validateMerged(content, endpoint); + + logger.info('[SHACL Validate] Merged complete', { + valid: result.valid, + errors: result.summary.errors, + warnings: result.summary.warnings, + }); + + res.json({ + success: true, + data: result, + timestamp: new Date().toISOString(), + } as ApiResponse); + } catch (error: unknown) { + logger.error('[SHACL Validate] Merged unexpected error', getErrorDetails(error)); + res.status(500).json({ + success: false, + error: { code: 'VALIDATION_ERROR', message: getErrorMessage(error) }, + timestamp: new Date().toISOString(), + } as ApiResponse); + } +}); + +export default router; diff --git a/packages/backend/src/services/assets.service.ts b/packages/backend/src/services/assets.service.ts index 37d5413..630cca9 100644 --- a/packages/backend/src/services/assets.service.ts +++ b/packages/backend/src/services/assets.service.ts @@ -90,7 +90,8 @@ export async function markDeployed( deploymentId: string, operatonUrl: string | undefined, formIds: string[], - documentIds: string[] + documentIds: string[], + boardOwner?: string ): Promise { if (!pool) return; await pool.query( @@ -99,9 +100,10 @@ export async function markDeployed( operaton_deployment_id = $2, operaton_url = $3, deployed_forms = $4, - deployed_documents = $5 + deployed_documents = $5, + board_owner = COALESCE($6, board_owner) WHERE lde_id = $1`, - [ldeId, deploymentId, operatonUrl ?? null, formIds, documentIds] + [ldeId, deploymentId, operatonUrl ?? null, formIds, documentIds, boardOwner ?? null] ); } @@ -123,6 +125,7 @@ export async function listPublicBundles(): Promise { deployed_documents: string[]; language: string | null; organization: string | null; + board_owner: string | null; updated_at: Date; }>( `SELECT pd_shell.lde_id, @@ -139,6 +142,7 @@ export async function listPublicBundles(): Promise { pd_shell.deployed_documents, pd_shell.language, pd_shell.organization, + pd_shell.board_owner, pd_shell.updated_at, COALESCE( json_agg( @@ -183,6 +187,7 @@ export async function listPublicBundles(): Promise { pd_shell.deployed_documents, pd_shell.language, pd_shell.organization, + pd_shell.board_owner, pd_shell.updated_at ORDER BY pd_shell.deployed_at DESC` ); @@ -206,6 +211,7 @@ export async function listPublicBundles(): Promise { ).subprocesses, language: r.language ?? undefined, organization: r.organization ?? undefined, + boardOwner: r.board_owner ?? undefined, updatedAt: r.updated_at, })); } diff --git a/packages/backend/src/services/dmn-validation.service.ts b/packages/backend/src/services/dmn-validation.service.ts index 2ed17e8..c06f1f4 100644 --- a/packages/backend/src/services/dmn-validation.service.ts +++ b/packages/backend/src/services/dmn-validation.service.ts @@ -198,8 +198,86 @@ function get(node: XmlElement, xpath: string, ns?: Record): XmlE } } +/** + * FEEL keywords/operators and built-in function names that must not be treated + * as variable references. Listing the built-ins here is what transparently + * "unwraps" date(...) / date and time(...) / time(...) / duration(...) / + * number(...) / string(...) / not(...): their leading tokens are skipped while + * the inner identifier(s) survive tokenisation. + */ +const FEEL_RESERVED = new Set([ + 'and', + 'or', + 'not', + 'true', + 'false', + 'null', + 'if', + 'then', + 'else', + 'for', + 'in', + 'return', + 'some', + 'every', + 'between', + 'instance', + 'of', + 'function', + 'date', + 'time', + 'duration', + 'number', + 'string', +]); +/** + * Extract the variable identifiers referenced by a FEEL inputExpression text. + * + * INT-007 must check the variables an expression *references*, not the raw + * expression string. A bare reference ("treeDiameter") yields one identifier; + * a built-in-wrapped reference ("date and time(aanvraagDatum)") must yield the + * inner identifier ("aanvraagDatum"); an operator expression + * ("maandelijksBrutoInkomenAanvrager <= 1.1 * bijstandsNorm") must yield every + * referenced identifier. + * + * Pragmatic, regex-based to match the style used elsewhere in this file rather + * than embedding a full FEEL grammar: + * 1. strip single- and double-quoted string literals, + * 2. tokenise on the FEEL name character class, + * 3. drop FEEL keywords/operators and built-in function names (this also + * unwraps date(...) / date and time(...) / number(...) / not(...) etc.), + * 4. drop property/path segments: a token whose nearest preceding + * non-whitespace character is "." is a qualified-name / property access + * (e.g. the "year" in "date(dagVanAanvraag).year", or ".years" on a + * duration). These are never top-level inputData and must not be checked; + * the path *head* (e.g. "dagVanAanvraag") is still extracted normally. + * 5. numeric and quoted literals never survive (digit-leading tokens do not + * match the identifier pattern; strings were stripped in step 1). + * Returns a de-duplicated list in first-seen order. + */ +function extractFeelIdentifiers(text: string): string[] { + const stripped = text.replace(/'(?:[^'\\]|\\.)*'/g, ' ').replace(/"(?:[^"\\]|\\.)*"/g, ' '); + const out: string[] = []; + const seen = new Set(); + const tokenRe = /[A-Za-z_][A-Za-z0-9_]*/g; + let m: RegExpExecArray | null; + while ((m = tokenRe.exec(stripped)) !== null) { + const tok = m[0]; + if (FEEL_RESERVED.has(tok.toLowerCase())) continue; + // Skip qualified-name / property-access segments (preceded by "."). + let p = m.index - 1; + while (p >= 0 && /\s/.test(stripped[p])) p--; + if (p >= 0 && stripped[p] === '.') continue; + if (seen.has(tok)) continue; + seen.add(tok); + out.push(tok); + } + return out; +} + // ── Layer 1: Base DMN (well-formedness + namespace checks) ──────────────────── // +// // libxml2's XSD schema compiler (used internally by libxmljs2's .validate()) // rejects complex schemas with forward references and abstract types that are // otherwise valid XSD 1.0. Since layers 2–5 already cover all meaningful @@ -767,40 +845,82 @@ function validateInteractionLayer(doc: XmlElement): LayerResult { } } - // INT-007: inputExpression variable with no matching declaration. + // INT-007: inputExpression references a variable with no resolvable source. // - // Each whose text is a variable reference should have a - // corresponding top-level - // element. Without it the CPSV Editor cannot discover the input contract and - // will generate an empty request body on deploy. + // An may reference either (a) a declared , or + // (b) a value produced by another decision that is wired into the owning + // decision via . Both are valid + // intra-DRD references and need no element. INT-007 should fire + // only when a referenced identifier resolves to neither — otherwise the CPSV + // Editor cannot discover the input contract and generates an empty request + // body on deploy. // - // Skipped inputs: - // - empty text (no variable) - // - literal booleans ("true" / "false") — used as passthrough inputs in DRDs - // - numeric or quoted-string literals — e.g. "0", "1.5", "'foo'" — these are - // hardcoded values, not variable references, and need no inputData element + // Two false-positive classes are fixed here, both reproduced against a DMN + // that deploys and evaluates correctly on Operaton: + // 1. requiredDecision outputs were not resolved. A variable that is the + // or decision-table of a + // requiredDecision target is satisfied — the same resolution INT-001 + // applies to requiredInput → inputData. (Reference: + // RONL_Heusden_Heusdenpas.dmn — "aanmerkingHeusdenPas" is consumed by + // RONL_HeusdenpasEindresultaat purely via requiredDecision.) + // 2. the whole was treated as one variable name. FEEL expressions + // (e.g. "date and time(aanvraagDatum)" or operator expressions) are now + // parsed into identifier references via extractFeelIdentifiers(). + // + // Decision output variables are, by construction, never external inputs, so + // they are excluded from the "must have matching inputData" requirement. const inputDataNames = new Set(); for (const el of find(doc, '//d:inputData')) { const name = el.attr('name')?.value(); if (name) inputDataNames.add(name); } + // decisionId → output variable names that decision produces. A decision + // exposes its result via a direct child and/or via + // its / clauses. + const decisionOutputVars = new Map>(); + for (const [id, decisionEl] of decisionIds) { + const names = new Set(); + const decVarName = get(decisionEl, 'd:variable')?.attr('name')?.value(); + if (decVarName) names.add(decVarName); + for (const out of find(decisionEl, 'd:decisionTable/d:output')) { + const outName = out.attr('name')?.value(); + if (outName) names.add(outName); + } + decisionOutputVars.set(id, names); + } + for (const ie of find(doc, '//d:inputExpression')) { const textEl = get(ie, 'd:text'); - const varName = textEl?.text()?.trim() ?? ''; + const exprText = textEl?.text()?.trim() ?? ''; + if (!exprText) continue; + + const decision = get(ie, 'ancestor::d:decision'); - if (!varName) continue; - if (/^(true|false)$/.test(varName)) continue; - if (/^[0-9"']/.test(varName)) continue; + // Names satisfied for THIS inputExpression: every declared inputData, plus + // the output variables of every decision the owning decision requires via + // (mirrors the requiredInput → inputData resolution + // already performed for INT-001). + const satisfied = new Set(inputDataNames); + if (decision) { + for (const rd of find(decision, 'd:informationRequirement/d:requiredDecision')) { + const href = rd.attr('href')?.value() ?? ''; + const targetId = href.startsWith('#') ? href.slice(1) : href; + const produced = decisionOutputVars.get(targetId); + if (produced) for (const n of produced) satisfied.add(n); + } + } - if (!inputDataNames.has(varName)) { - const decision = get(ie, 'ancestor::d:decision'); + for (const ident of extractFeelIdentifiers(exprText)) { + if (satisfied.has(ident)) continue; issues.push( iss( 'warning', 'INT-007', - ` uses variable "${varName}" but no is declared at the definitions level. ` + - `Add a matching element with a child for CPSV Editor compatibility.`, + ` references variable "${ident}" but it is neither declared as ` + + ` nor produced by a requiredDecision target. ` + + `Add a matching element with a child, or wire the ` + + `producing decision via , for CPSV Editor compatibility.`, decision ? elLoc(decision) : undefined ) ); diff --git a/packages/backend/src/services/dso.service.ts b/packages/backend/src/services/dso.service.ts index ba3c628..ceaa8ad 100644 --- a/packages/backend/src/services/dso.service.ts +++ b/packages/backend/src/services/dso.service.ts @@ -1,5 +1,6 @@ // packages/backend/src/services/dso.service.ts +import { XMLParser } from 'fast-xml-parser'; import { config } from '../utils/config'; import { logger } from '../utils/logger'; @@ -99,7 +100,9 @@ export async function getActiviteitenByOin( const params = new URLSearchParams(); params.set('page', '1'); - params.set('pageSize', '100'); + // Full set in one call so the Activities tab can filter client-side. + // The API caps `size` to the actual count, so this never over-fetches. + params.set('pageSize', '200'); const body = { datum: effectiveDatum, @@ -336,3 +339,330 @@ export async function getWerkzaamheidDetail(urn: string, env: DsoEnv = 'pre'): P clearTimeout(timeoutId); } } + +// --------------------------------------------------------------------------- +// Uitvoeren Gegevens API (toepasbareregelsuitvoerengegevens v1) +// --------------------------------------------------------------------------- + +/** Fetch helper that returns raw XML text (used for STTR bestand downloads). */ +async function dsoFetchXml(url: string, env: DsoEnv = 'pre'): Promise { + const dsoConfig = getDsoConfig(env); + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), config.dso.timeout); + try { + const response = await fetch(url, { + headers: { 'x-api-key': dsoConfig.apiKey, Accept: 'application/xml' }, + signal: controller.signal, + }); + if (!response.ok) { + const body = await response.text(); + throw new Error(`DSO responded ${response.status}: ${body}`); + } + return response.text(); + } finally { + clearTimeout(timeoutId); + } +} + +/** + * GET /toepasbareRegels?functioneleStructuurRef=... + * Returns the metadata list for a given functioneleStructuurRef. + */ +export async function getToepasbareRegels( + functioneleStructuurRef: string, + env: DsoEnv = 'pre' +): Promise { + const params = new URLSearchParams({ functioneleStructuurRef }); + const url = `${getDsoConfig(env).uitvoerenGegevensBaseUrl}/toepasbareRegels?${params}`; + logger.info('[DSO] GET toepasbareRegels', { env, functioneleStructuurRef }); + return dsoFetch(url, env); +} + +/** + * GET /toepasbareRegels/:id/sttr + * Returns the raw STTR XML for a toepasbare regel by its generated id. + */ +export async function getSttrBestand(id: string, env: DsoEnv = 'pre'): Promise { + const url = `${getDsoConfig(env).uitvoerenGegevensBaseUrl}/toepasbareRegels/${encodeURIComponent(id)}/sttrBestand`; + logger.info('[DSO] GET STTR bestand', { env, id }); + return dsoFetchXml(url, env); +} + +// --------------------------------------------------------------------------- +// STTR parsing helpers +// --------------------------------------------------------------------------- + +/** + * Convert an arbitrary STTR variable name into a FEEL-safe identifier. + * FEEL treats `-` as subtraction and whitespace as token separators, so the + * STTR's hyphenated GUID names (`uitv__`) and spaced names + * (`Boom kappen … _cross`) are not resolvable as variable references. Mapping + * every non-`[A-Za-z0-9_]` char to `_` yields a usable identifier. + */ +function toFeelName(name: string): string { + const safe = name.replace(/[^A-Za-z0-9_]/g, '_'); + return /^[0-9]/.test(safe) ? `_${safe}` : safe; +} + +/** + * Rename every declared `` to a FEEL-safe identifier and + * rewrite the `` references that point at those names, so the + * DMN actually *evaluates* on Camunda/Operaton (not just deploys). References + * are matched on exact, full name equality — output value literals and the `?` + * rule expressions never reference names, so they are left untouched. Decision + * and inputData display names (`name=` on ``/``) are also + * left as-is; only `` names drive FEEL resolution. + */ +function sanitizeFeelNames(xml: string): string { + const names = new Set(); + for (const m of xml.matchAll(/<(?:\w+:)?variable\b[^>]*\bname="([^"]*)"/g)) names.add(m[1]); + + // Build name → FEEL-safe map, disambiguating any collisions. + const map = new Map(); + const used = new Set(); + for (const name of names) { + let safe = toFeelName(name); + if (safe !== name) { + let candidate = safe; + let i = 1; + while (used.has(candidate) || (candidate !== name && names.has(candidate))) + candidate = `${safe}_${i++}`; + safe = candidate; + } + used.add(safe); + map.set(name, safe); + } + + // Rename the variable declarations. + let out = xml.replace( + /(<(?:\w+:)?variable\b[^>]*\bname=")([^"]*)(")/g, + (full, pre: string, nm: string, post: string) => { + const safe = map.get(nm); + return safe ? `${pre}${safe}${post}` : full; + } + ); + + // Rewrite inputExpression bodies that reference a known variable name + // (handles both plain and CDATA-wrapped text). + out = out.replace( + /(<(?:\w+:)?inputExpression\b[^>]*>\s*<(?:\w+:)?text>)(?:)?(<\/(?:\w+:)?text>)/g, + (full, open: string, content: string, close: string) => { + const safe = map.get(content.trim()); + return safe ? `${open}${safe}${close}` : full; + } + ); + + return out; +} + +// Default Camunda history TTL (days) stamped on extracted DMN decisions, matching +// the LDE BPMN templates' `historyTimeToLive="180"` convention. This Operaton +// instance enforces HTTL at deploy, so without it the deploy fails. +const DSO_DMN_HISTORY_TTL = 180; + +/** + * Ensure the `camunda` namespace is declared and every `` carries a + * `camunda:historyTimeToLive`, so the DMN is deployable on an Operaton instance + * that enforces HTTL — making the extracted DMN handoff-ready without the + * deployer having to patch it. Decisions that already declare HTTL are left as-is. + */ +function ensureHistoryTimeToLive(xml: string): string { + let out = xml; + if (!/\bxmlns:camunda=/.test(out)) { + out = out.replace( + /<((?:\w+:)?definitions)\b/, + (full, tag: string) => `<${tag} xmlns:camunda="http://camunda.org/schema/1.0/dmn"` + ); + } + return out.replace(/<((?:\w+:)?decision)((?:\s[^>]*)?)>/g, (full, tag: string, attrs: string) => { + if (/\bcamunda:historyTimeToLive\s*=/.test(attrs)) return full; + return `<${tag} camunda:historyTimeToLive="${DSO_DMN_HISTORY_TTL}"${attrs}>`; + }); +} + +/** + * Give every `` that lacks a `typeRef` the type of its owning decision's + * result `` (defaulting to `string`). Clears the BIZ-004 validator + * warning and lets Camunda type-coerce output values correctly. + */ +function ensureOutputTypeRefs(xml: string): string { + return xml.replace( + /<(?:\w+:)?decision\b[^>]*>[\s\S]*?<\/(?:\w+:)?decision>/g, + (decBlock: string) => { + const tref = decBlock.match(/<(?:\w+:)?variable\b[^>]*\btypeRef="([^"]*)"/); + const type = tref ? tref[1] : 'string'; + return decBlock.replace( + /<((?:\w+:)?output)\b((?:\s[^>]*?)?)(\/?)>/g, + (full, tag: string, attrs: string, selfClose: string) => { + if (/\btypeRef\s*=/.test(attrs)) return full; + return `<${tag}${attrs} typeRef="${type}"${selfClose}>`; + } + ); + } + ); +} + +/** + * Normalises an STTR-extracted DMN so it both transforms AND evaluates on + * Operaton/Camunda. The Sogelink STTR Builder emits DMN the engine rejects + * (`ENGINE-22004`) and that, once deployable, still fails to evaluate. Fixes, + * verified against operaton.open-regels.nl: + * + * 1. DMN version: rewrite the four spec namespaces `…/20180521/…` (DMN 1.2) + * → `…/20191111/…` (DMN 1.3); the engine only transforms 1.3. + * 2. Missing ids: inject `id` on `` / `` (engine rejects + * inputs without one). + * 3. FEEL-safe names: rename `` names and their `` + * references to valid FEEL identifiers — without this the DMN deploys but + * fails to evaluate (hyphens parse as subtraction, spaces as separators). + * 4. Output types: give untyped `` columns a `typeRef` (BIZ-004). + * 5. History TTL: declare the `camunda` namespace and stamp + * `camunda:historyTimeToLive` on each ``; this Operaton enforces + * HTTL at deploy. With this the extracted DMN is deploy-ready as handed off + * (the CPSV Editor deploys it as-is — no patching required). + */ +export function normalizeDmnForOperaton(dmn: string): string { + // 1. DMN 1.2 → 1.3 (covers MODEL / DI / DMNDI / DC; no-op if already 1.3). + let out = dmn.replace( + /http:\/\/www\.omg\.org\/spec\/DMN\/20180521\//g, + 'https://www.omg.org/spec/DMN/20191111/' + ); + + // 2a. Add an id to every opening tag that lacks one. Matching `input` + // followed by whitespace-or-`>` keeps this off /. + let inputCount = 0; + out = out.replace(/<((?:\w+:)?input)((?:\s[^>]*)?)>/g, (full, tag: string, attrs: string) => { + if (/\bid\s*=/.test(attrs)) return full; + inputCount += 1; + return `<${tag} id="dsoInput_${inputCount}"${attrs}>`; + }); + + // 2b. Same for . + let exprCount = 0; + out = out.replace( + /<((?:\w+:)?inputExpression)((?:\s[^>]*)?)>/g, + (full, tag: string, attrs: string) => { + if (/\bid\s*=/.test(attrs)) return full; + exprCount += 1; + return `<${tag} id="dsoInputExpr_${exprCount}"${attrs}>`; + } + ); + + // 3. FEEL-safe variable names + reference rewrite (makes the DMN evaluatable). + out = sanitizeFeelNames(out); + + // 4. Ensure output columns are typed (BIZ-004). + out = ensureOutputTypeRefs(out); + + // 5. Stamp camunda:historyTimeToLive so the DMN deploys as handed off. + out = ensureHistoryTimeToLive(out); + + return out; +} + +/** + * Extracts the embedded DMN element from a conclusie STTR envelope + * and returns it as a standalone, Operaton-deployable DMN XML string. + */ +export function extractDmnFromSttr(sttrXml: string): string { + // The conclusie STTR wraps a complete DMN 1.2 element. + // Match including any namespace declarations and closing tag, + // handling both prefixed (dmn:definitions) and un-prefixed variants. + const match = sttrXml.match(/<(?:dmn:)?definitions[\s\S]*?<\/(?:dmn:)?definitions>/); + if (!match) throw new Error('No DMN element found in STTR XML'); + return `\n${normalizeDmnForOperaton(match[0])}`; +} + +export interface FormScaffoldField { + id: string; + type: string; + label: string; + key: string; + values?: { label: string; value: string }[]; + validate?: { required: boolean }; +} + +export interface FormScaffold { + schemaVersion: number; + id: string; + components: FormScaffoldField[]; + type: 'default'; +} + +/** + * Parses an indieningsvereisten STTR and generates a best-effort form-js field + * scaffold from the uitv:uitvoeringsregels questionnaire in dmn:extensionElements. + */ +export function extractFormScaffoldFromSttr(sttrXml: string, formId: string): FormScaffold { + const parser = new XMLParser({ + ignoreAttributes: false, + attributeNamePrefix: '@_', + isArray: (name) => ['uitv:uitvoeringsregel', 'uitv:optie'].includes(name), + }); + + const parsed = parser.parse(sttrXml); + + const defs = parsed?.['dmn:definitions'] ?? parsed?.['definitions'] ?? {}; + const ext = defs?.['dmn:extensionElements'] ?? {}; + const regels: unknown[] = ext?.['uitv:uitvoeringsregels']?.['uitv:uitvoeringsregel'] ?? []; + + const components: FormScaffoldField[] = []; + + for (const r of regels) { + const regel = r as Record; + const id = (regel['@_id'] as string) ?? ''; + const key = id.replace(/^uitv__/, '').replace(/[^a-zA-Z0-9_]/g, '_'); + + if (regel['uitv:vraag']) { + const vraag = regel['uitv:vraag'] as Record; + const gegevensType = (vraag['uitv:gegevensType'] as string) ?? 'string'; + const vraagTekst = (vraag['uitv:vraagTekst'] as string) ?? ''; + const inputType = vraag['inter:inputType'] as string | undefined; + + let fieldType: string; + let values: { label: string; value: string }[] | undefined; + + if (gegevensType === 'boolean') { + fieldType = 'checkbox'; + } else if (gegevensType === 'list') { + fieldType = 'select'; + const opties = (vraag['uitv:opties'] as Record)?.['uitv:optie']; + if (Array.isArray(opties)) { + values = opties.map((o: unknown) => { + const text = ((o as Record)['uitv:optieText'] as string) ?? ''; + return { label: text, value: text }; + }); + } + } else if (gegevensType === 'number') { + fieldType = 'number'; + } else if (inputType === 'textarea') { + fieldType = 'textarea'; + } else { + fieldType = 'textfield'; + } + + components.push({ + id, + type: fieldType, + label: vraagTekst, + key, + ...(values ? { values } : {}), + validate: { required: false }, + }); + } else if (regel['uitv:bijlage']) { + // Attachment requirement — emit as a labelled textfield placeholder + const bijlageType = + ((regel['uitv:bijlage'] as Record)['uitv:bijlageType'] as string) ?? ''; + components.push({ + id, + type: 'textfield', + label: `[Bijlage] ${bijlageType}`, + key, + validate: { required: false }, + }); + } + // uitv:geoVerwijzing — geo fields not representable in form-js, skip + } + + return { schemaVersion: 17, id: formId, components, type: 'default' }; +} diff --git a/packages/backend/src/services/operaton.service.ts b/packages/backend/src/services/operaton.service.ts index 939ce9c..b66d8d2 100644 --- a/packages/backend/src/services/operaton.service.ts +++ b/packages/backend/src/services/operaton.service.ts @@ -643,13 +643,23 @@ export class OperatonService { documents: { id: string; template: Record }[] = [], operatonUrl?: string, operatonUsername?: string, - operatonPassword?: string + operatonPassword?: string, + boardOwner?: string ): Promise<{ deploymentId: string; resourceCount: number }> { try { + // Stamp the owning board onto the process definition (deploy-time tag). + // boardOwner semantics: + // undefined → derive from the BPMN's candidate groups + // '' (empty) → caller explicitly opted out; deploy untagged + // value → use as-is (explicit override) + const owner = boardOwner === undefined ? this.deriveBoardOwner(bpmnXml) : boardOwner; + const taggedXml = this.injectBoardOwner(bpmnXml, owner); + logger.info('Deploying BPMN process to Operaton', { deploymentName, formCount: forms.length, subProcessCount: subProcesses.length, + boardOwner: owner ?? '(none)', }); const client = operatonUrl @@ -669,7 +679,7 @@ export class OperatonService { // Main BPMN const mainFilename = `${deploymentName}.bpmn`; - formData.append(mainFilename, Buffer.from(bpmnXml, 'utf-8'), { + formData.append(mainFilename, Buffer.from(taggedXml, 'utf-8'), { filename: mainFilename, contentType: 'application/xml', }); @@ -718,6 +728,101 @@ export class OperatonService { ); } } + + /** + * Candidate-group → board mapping used to derive a process's owning board when + * the deploy request doesn't pass one explicitly. Kept here as the single point + * of coupling to RONL's board taxonomy; extend as new boards/roles appear. + */ + private static readonly BOARD_BY_GROUP: { match: RegExp; board: string }[] = [ + { match: /^(infra-projectteam|infra-medewerker|rip-[\w-]+)$/i, board: 'infra-board' }, + { match: /^(caseworker|case-workers|hr-medewerker)$/i, board: 'caseworker' }, + ]; + + /** + * Derive the owning board from the candidate groups present in the BPMN. Returns + * undefined when no known group is found, so the process is left untagged (and the + * consumer falls back to its legacy split). Infra ownership wins over caseworker + * when both appear, since RIP processes also carry the broad `caseworker` role. + */ + private deriveBoardOwner(bpmnXml: string): string | undefined { + const groups = new Set(); + for (const m of bpmnXml.matchAll(/candidateGroups\s*=\s*["']([^"']+)["']/g)) { + for (const g of m[1].split(',')) groups.add(g.trim()); + } + let found: string | undefined; + for (const g of groups) { + for (const { match, board } of OperatonService.BOARD_BY_GROUP) { + if (match.test(g)) { + if (board === 'infra-board') return 'infra-board'; + found = board; + } + } + } + return found; + } + + /** + * Inject a process-level into the main + * BPMN process element. Conservative by design: idempotent, and if the BPMN shape + * is anything unexpected the original XML is returned untouched — deployment must + * never break because of tagging. + */ + private injectBoardOwner(bpmnXml: string, boardOwner?: string): string { + if (!boardOwner) return bpmnXml; + try { + if (/name\s*=\s*["']boardOwner["']/.test(bpmnXml)) return bpmnXml; // already tagged + + const processOpen = bpmnXml.match(/<([A-Za-z_][\w.-]*:)?process\b[^>]*?>/); + if (!processOpen || processOpen[0].endsWith('/>')) return bpmnXml; + const pfx = processOpen[1] ?? ''; + const insertAt = (processOpen.index as number) + processOpen[0].length; + + const property = ``; + const after = bpmnXml.slice(insertAt); + const extOpen = after.match(/^\s*<([A-Za-z_][\w.-]*:)?extensionElements\b[^>]*?>/); + + let tagged: string; + if (extOpen) { + // Process already opens with an extensionElements — merge into it, reusing an + // existing camunda:properties block when present, otherwise adding one. + const extEnd = insertAt + extOpen[0].length; + const propsOpen = bpmnXml.slice(extEnd).match(/^\s*]*?>/); + if (propsOpen) { + const at = extEnd + propsOpen[0].length; + tagged = bpmnXml.slice(0, at) + property + bpmnXml.slice(at); + } else { + const block = `${property}`; + tagged = bpmnXml.slice(0, extEnd) + block + bpmnXml.slice(extEnd); + } + } else { + const block = + `<${pfx}extensionElements>${property}` + + ``; + tagged = bpmnXml.slice(0, insertAt) + block + bpmnXml.slice(insertAt); + } + + return this.ensureCamundaNamespace(tagged); + } catch (err) { + logger.warn('boardOwner injection skipped — BPMN deployed untouched', { + error: err instanceof Error ? err.message : String(err), + }); + return bpmnXml; + } + } + + /** Ensure the camunda namespace is declared on so the property resolves. */ + private ensureCamundaNamespace(xml: string): string { + if (/xmlns:camunda\s*=/.test(xml)) return xml; + const defs = xml.match(/<([A-Za-z_][\w.-]*:)?definitions\b[^>]*?>/); + if (!defs) return xml; + const patched = defs[0].replace(/>$/, ` xmlns:camunda="http://camunda.org/schema/1.0/bpmn">`); + return ( + xml.slice(0, defs.index as number) + + patched + + xml.slice((defs.index as number) + defs[0].length) + ); + } } export const operatonService = new OperatonService(); diff --git a/packages/backend/src/services/shacl-validation.service.ts b/packages/backend/src/services/shacl-validation.service.ts new file mode 100644 index 0000000..91c65bf --- /dev/null +++ b/packages/backend/src/services/shacl-validation.service.ts @@ -0,0 +1,420 @@ +// eslint-disable-next-line @typescript-eslint/triple-slash-reference +/// +// packages/backend/src/services/shacl-validation.service.ts +// +// SHACL validation for CPSV-AP 3.2.0 (+ custom RONL) Turtle files. Mirrors the +// DMN validator's architecture and response shape so the frontend can reuse the +// existing LayerSection / IssueRow components: the result is +// `{ valid, parseError, layers: {...}, summary: { errors, warnings, infos } }`, +// with violations grouped into layers by the shape file they originated from. +// +// Two entry points: +// - validateFile(content) — validate the uploaded Turtle against the +// vendored shape set. Fast; no network. +// - validateMerged(content, endpoint) — fetch the already-published triples for +// the file's subjects from the SPARQL +// endpoint, union them with the uploaded +// content, then validate the result. This +// is the only mode that catches multi-value +// fan-out against live data (e.g. two +// divergent foaf:homepage values on the same +// organisation subject across publications). +// +// Library notes (verified against rdf-validate-shacl 0.6.5 / @rdfjs/dataset 2.0.2 / +// n3 1.26): the validator ships its own RDF environment — do NOT pass a foreign +// `factory`, or its `clownface` lookup breaks. `validate()` is async. Data graphs +// must be DatasetCore instances (with `.match`), so n3 quad arrays are wrapped via +// @rdfjs/dataset before use. Both RDF deps are ESM-only; they load through Node's +// require(esm) (Node >= 20.19 / 22). + +import { promises as fs } from 'fs'; +import path from 'path'; +import { Parser } from 'n3'; +import rdfDataset from '@rdfjs/dataset'; +import SHACLValidator from 'rdf-validate-shacl'; +import { constructGraph } from './triplydb.service'; +import { config } from '../utils/config'; +import logger from '../utils/logger'; + +// ── Response types (shape matches DmnValidator's so the UI components are shared) ── + +export type ShaclLayerKey = 'cpsv-ap' | 'ronl-custom' | 'cprmv'; + +export interface ShaclIssue { + severity: 'error' | 'warning' | 'info'; + code: string; + message: string; + location?: string; +} + +export interface ShaclLayerResult { + label: string; + /** false when no shape files were present for this layer (e.g. SEMIC shapes not vendored yet) — lets the UI distinguish "not evaluated" from "passed". */ + loaded: boolean; + issues: ShaclIssue[]; +} + +export interface ShaclValidationResult { + valid: boolean; + parseError: string | null; + layers: Record; + summary: { errors: number; warnings: number; infos: number }; +} + +// ── Shape layer configuration ───────────────────────────────────────────────── + +// shapes/ lives at the package root (packages/backend/shapes). __dirname is +// .../src/services under ts-node and .../dist/services after build; ../../shapes +// resolves to packages/backend/shapes in both. NOTE: the build only emits dist/, +// so the deploy workflow must also copy shapes/ into the deploy bundle (see the +// kickoff follow-up) or these reads return ENOENT in Azure. +const SHAPES_ROOT = path.resolve(__dirname, '../../shapes'); + +interface LayerSpec { + key: ShaclLayerKey; + label: string; + /** Explicit file list, relative to SHAPES_ROOT. */ + files?: string[]; + /** Directory (relative to SHAPES_ROOT) — every *.ttl inside is loaded. */ + dir?: string; +} + +const LAYER_SPECS: LayerSpec[] = [ + { + key: 'cpsv-ap', + label: 'CPSV-AP 3.2.0', + files: ['cpsv-ap/3.2.0/cpsv-ap-SHACL.ttl'], + }, + { + key: 'ronl-custom', + label: 'RONL Custom', + dir: 'ronl', + }, + { + key: 'cprmv', + label: 'CPRMV 0.4.1', + files: ['cprmv/0.4.1/cprmv.shacl.ttl'], + }, +]; + +interface LoadedLayer { + key: ShaclLayerKey; + label: string; + /** null when no shape files are present for this layer (e.g. not vendored yet). */ + validator: SHACLValidator | null; +} + +// ── Severity / code mapping ───────────────────────────────────────────────────── + +const SH = 'http://www.w3.org/ns/shacl#'; + +function severityFromTerm(term: { value: string } | null): 'error' | 'warning' | 'info' { + switch (term?.value) { + case `${SH}Warning`: + return 'warning'; + case `${SH}Info`: + return 'info'; + case `${SH}Violation`: + default: + return 'error'; + } +} + +// e.g. http://www.w3.org/ns/shacl#MaxCountConstraintComponent -> "SHACL-MAXCOUNT" +function codeFromComponent(term: { value: string } | null): string { + if (!term?.value) return 'SHACL-CONSTRAINT'; + const local = term.value.split(/[#/]/).pop() ?? ''; + const base = local.replace(/ConstraintComponent$/, ''); + return base ? `SHACL-${base.toUpperCase()}` : 'SHACL-CONSTRAINT'; +} + +const PREFIXES: ReadonlyArray<[string, string]> = [ + ['foaf', 'http://xmlns.com/foaf/0.1/'], + ['skos', 'http://www.w3.org/2004/02/skos/core#'], + ['dct', 'http://purl.org/dc/terms/'], + ['cv', 'http://data.europa.eu/m8g/'], + ['ronl', 'https://regels.overheid.nl/ontology#'], + ['sh', SH], +]; + +function compact(uri: string | undefined): string { + if (!uri) return ''; + for (const [prefix, ns] of PREFIXES) { + if (uri.startsWith(ns)) return `${prefix}:${uri.slice(ns.length)}`; + } + return uri; +} + +// ── Service ───────────────────────────────────────────────────────────────────── + +/** + * Fetches a SPARQL CONSTRUCT result as Turtle. Injectable so merge-mode can be + * exercised deterministically in tests with a fixed "already-published" graph; + * defaults to the real TriplyDB-backed implementation. + */ +type GraphFetcher = (endpoint: string, query: string) => Promise; + +export class ShaclValidationService { + // Shapes are read once and cached for the life of the process. Adding shape + // files requires a restart (which Azure does on deploy). + private layersPromise: Promise | null = null; + + /** @param fetchGraph SPARQL CONSTRUCT→Turtle fetcher; override in tests. */ + constructor(private readonly fetchGraph: GraphFetcher = constructGraph) {} + + private parse(ttl: string) { + return new Parser().parse(ttl); + } + + private toDataset(ttl: string) { + return rdfDataset.dataset(this.parse(ttl)); + } + + /** + * Load and cache one SHACLValidator per layer. Missing shape files / directories + * are tolerated — the corresponding layer simply has no validator and reports no + * issues (this is the expected state for the CPSV-AP layers until the SEMIC + * shapes are vendored). + */ + private loadLayers(): Promise { + if (this.layersPromise) return this.layersPromise; + + this.layersPromise = (async () => { + const layers: LoadedLayer[] = []; + + for (const spec of LAYER_SPECS) { + const ttls: string[] = []; + + if (spec.files) { + for (const rel of spec.files) { + try { + ttls.push(await fs.readFile(path.join(SHAPES_ROOT, rel), 'utf8')); + } catch { + logger.warn('[SHACL] Shape file not found (layer left empty)', { + layer: spec.key, + file: rel, + }); + } + } + } + + if (spec.dir) { + const dirPath = path.join(SHAPES_ROOT, spec.dir); + try { + const entries = await fs.readdir(dirPath); + for (const name of entries.filter((n) => n.endsWith('.ttl')).sort()) { + ttls.push(await fs.readFile(path.join(dirPath, name), 'utf8')); + } + } catch { + logger.warn('[SHACL] Shape directory not found (layer left empty)', { + layer: spec.key, + dir: spec.dir, + }); + } + } + + let validator: SHACLValidator | null = null; + if (ttls.length > 0) { + const shapes = rdfDataset.dataset(this.parse(ttls.join('\n'))); + validator = new SHACLValidator(shapes); + } + + layers.push({ key: spec.key, label: spec.label, validator }); + } + + const loaded = layers.filter((l) => l.validator).map((l) => l.key); + logger.info('[SHACL] Shape layers loaded', { withShapes: loaded }); + return layers; + })(); + + return this.layersPromise; + } + + private emptyLayers(): Record { + return { + cprmv: { label: 'CPRMV 0.4.1', loaded: false, issues: [] }, + 'cpsv-ap': { label: 'CPSV-AP 3.2.0', loaded: false, issues: [] }, + 'ronl-custom': { label: 'RONL Custom', loaded: false, issues: [] }, + }; + } + + /** + * List the object values present in `data` for a given (focusNode, path). Used to + * enrich cardinality violations (maxCount / uniqueLang) with the actual offending + * values, so the report names them rather than just stating the count is wrong. + */ + private offendingValues( + data: RdfDataset, + focusNode: RdfTerm | null, + path: RdfTerm | null + ): string[] { + if (!focusNode || !path || path.termType !== 'NamedNode') return []; + const values: string[] = []; + for (const quad of data.match(focusNode, path, null)) { + // Normalise whitespace only — the full value is reported so the user can see + // the complete offending value (the frontend wraps long messages). + values.push(quad.object.value.replace(/\s+/g, ' ').trim()); + } + return values; + } + + /** + * Run every loaded layer's validator against `data` and assemble the combined + * result. `parseError` is reserved for failures parsing the caller's content and + * is passed through unchanged here. + */ + private async runLayers( + data: RdfDataset, + parseError: string | null + ): Promise { + const layers = this.emptyLayers(); + const summary = { errors: 0, warnings: 0, infos: 0 }; + + const loaded = await this.loadLayers(); + + for (const layer of loaded) { + layers[layer.key].loaded = layer.validator !== null; + if (!layer.validator) continue; + const report = await layer.validator.validate(data); + + for (const result of report.results) { + const severity = severityFromTerm(result.severity); + const baseMessage = + result.message.map((m) => m.value).join('; ') || + codeFromComponent(result.sourceConstraintComponent); + + const values = this.offendingValues(data, result.focusNode, result.path); + const message = + values.length > 1 + ? `${baseMessage} Found ${values.length} values: ${values.join(', ')}.` + : baseMessage; + + const location = + result.focusNode || result.path + ? `${result.focusNode?.value ?? ''} ${compact(result.path?.value)}`.trim() + : undefined; + + layers[layer.key].issues.push({ + severity, + code: codeFromComponent(result.sourceConstraintComponent), + message, + location, + }); + + if (severity === 'error') summary.errors++; + else if (severity === 'warning') summary.warnings++; + else summary.infos++; + } + } + + return { + valid: parseError === null && summary.errors === 0, + parseError, + layers, + summary, + }; + } + + /** + * Validate the uploaded Turtle against the vendored shape set. No network access. + */ + async validateFile(content: string): Promise { + let data: RdfDataset; + try { + data = this.toDataset(content); + } catch (err) { + logger.warn('[SHACL] Turtle parse failed (validateFile)', { + error: err instanceof Error ? err.message : 'Unknown error', + }); + return { + valid: false, + parseError: err instanceof Error ? err.message : 'Failed to parse Turtle content.', + layers: this.emptyLayers(), + summary: { errors: 0, warnings: 0, infos: 0 }, + }; + } + + logger.info('[SHACL] validateFile', { contentLength: content.length, triples: data.size }); + return this.runLayers(data, null); + } + + /** + * Validate the uploaded Turtle unioned with the triples already published for its + * subjects on the given SPARQL endpoint. A parse failure of the *uploaded* content + * short-circuits to a parseError result; a failure fetching/parsing the remote + * graph throws (surfaced as a 500 by the route), since that is not the caller's + * input fault. + */ + async validateMerged(content: string, endpoint?: string): Promise { + let localQuads; + try { + localQuads = this.parse(content); + } catch (err) { + logger.warn('[SHACL] Turtle parse failed (validateMerged)', { + error: err instanceof Error ? err.message : 'Unknown error', + }); + return { + valid: false, + parseError: err instanceof Error ? err.message : 'Failed to parse Turtle content.', + layers: this.emptyLayers(), + summary: { errors: 0, warnings: 0, infos: 0 }, + }; + } + + // Distinct named subjects in the uploaded file — blank nodes can't be addressed + // across endpoints, and the fan-out we care about is on named subjects anyway. + const subjects = new Set(); + for (const quad of localQuads) { + if (quad.subject.termType === 'NamedNode') subjects.add(quad.subject.value); + } + + // No named subjects -> nothing to merge against; fall back to file-local. + if (subjects.size === 0) { + logger.info('[SHACL] validateMerged: no named subjects, falling back to file-local'); + return this.runLayers(rdfDataset.dataset(localQuads), null); + } + + const targetEndpoint = endpoint || config.triplydb.endpoint; + if (!targetEndpoint) { + throw new Error('No SPARQL endpoint configured — set TRIPLYDB_ENDPOINT or pass an endpoint.'); + } + + const values = Array.from(subjects) + .map((uri) => `<${uri}>`) + .join(' '); + + // Standard SPARQL 1.1 CONSTRUCT: the subjects' direct triples plus one level of + // forward closure on any blank-node objects (addresses, contact points, etc.), + // so nested shapes in the canonical CPSV-AP set evaluate against a complete + // bounded description rather than a truncated one. + const query = `CONSTRUCT { + ?s ?p ?o . + ?o ?p2 ?o2 . +} +WHERE { + VALUES ?s { ${values} } + ?s ?p ?o . + OPTIONAL { ?o ?p2 ?o2 . FILTER(isBlank(?o)) } +}`; + + logger.info('[SHACL] validateMerged: fetching published graph', { + endpoint: targetEndpoint, + subjects: subjects.size, + }); + + const remoteTtl = await this.fetchGraph(targetEndpoint, query); + const remoteQuads = this.parse(remoteTtl); + + const merged = rdfDataset.dataset([...localQuads, ...remoteQuads]); + logger.info('[SHACL] validateMerged: merged graph assembled', { + localTriples: localQuads.length, + remoteTriples: remoteQuads.length, + mergedTriples: merged.size, + }); + + return this.runLayers(merged, null); + } +} + +export const shaclValidationService = new ShaclValidationService(); +export default shaclValidationService; diff --git a/packages/backend/src/services/sparql.service.ts b/packages/backend/src/services/sparql.service.ts index 23d1163..434d801 100644 --- a/packages/backend/src/services/sparql.service.ts +++ b/packages/backend/src/services/sparql.service.ts @@ -163,6 +163,7 @@ export class SparqlService { const query = ` PREFIX cprmv: +PREFIX cprmv041: PREFIX cpsv: PREFIX dct: PREFIX ronl: @@ -171,37 +172,46 @@ PREFIX cv: PREFIX foaf: PREFIX skos: -SELECT ?dmn ?identifier ?title ?description ?deploymentId ?deployedAt - ?implementedBy ?lastTested ?testStatus +SELECT ?dmn ?identifier ?title ?description ?deploymentId ?deployedAt + ?implementedBy ?lastTested ?testStatus ?service ?serviceTitle ?organization ?orgName ?logo ?validationStatus ?validatedBy ?validatedByName ?validatedAt ?validationNote WHERE { - ?dmn a cprmv:DecisionModel ; - dct:identifier ?identifier ; + # DecisionModel type: support CPRMV 0.3.0 and CPRMV 0.4.1 namespaces + { ?dmn a cprmv:DecisionModel } UNION { ?dmn a cprmv041:DecisionModel } + ?dmn dct:identifier ?identifier ; dct:title ?title . - + OPTIONAL { ?dmn dct:description ?description } OPTIONAL { ?dmn cprmv:deploymentId ?deploymentId } + OPTIONAL { ?dmn cprmv041:deploymentId ?deploymentId } OPTIONAL { ?dmn cprmv:deployedAt ?deployedAt } - - # implementedBy: Support both old (ronl:) and new (cprmv:) + OPTIONAL { ?dmn cprmv041:deployedAt ?deployedAt } + + # implementedBy: support old (ronl:), CPRMV 0.3.0 (cprmv:) and CPRMV 0.4.1 (cprmv041:) OPTIONAL { ?dmn ronl:implementedBy ?implementedBy } OPTIONAL { ?dmn cprmv:implementedBy ?implementedBy } - + OPTIONAL { ?dmn cprmv041:implementedBy ?implementedBy } + OPTIONAL { ?dmn cprmv:lastTested ?lastTested } + OPTIONAL { ?dmn cprmv041:lastTested ?lastTested } OPTIONAL { ?dmn cprmv:testStatus ?testStatus } - + OPTIONAL { ?dmn cprmv041:testStatus ?testStatus } + # Traverse DMN → Service → Organization → Logo - # Support BOTH old (ronl:implements) and new (cprmv:implements) + # Support old (ronl:implements), CPRMV 0.3.0 (cprmv:implements) and CPRMV 0.4.1 (cprmv041:implements) OPTIONAL { { - # NEW namespace (facts endpoint uses this) + # CPRMV 0.4.1 namespace (canonical going forward) + ?dmn cprmv041:implements ?service . + } UNION { + # CPRMV 0.3.0 namespace (facts endpoint uses this) ?dmn cprmv:implements ?service . } UNION { # OLD namespace (RONL/DMN-discovery endpoints use this) ?dmn ronl:implements ?service . } - + ?service dct:title ?serviceTitle . OPTIONAL { @@ -468,21 +478,22 @@ ORDER BY ?identifier const query = ` PREFIX cprmv: +PREFIX cprmv041: PREFIX cpsv: PREFIX dct: SELECT ?dmn1Identifier ?dmn2Identifier ?variableId ?variableType WHERE { - # DMN 1 produces a variable - ?dmn1 a cprmv:DecisionModel ; - dct:identifier ?dmn1Identifier . + # DMN 1 produces a variable (DecisionModel type: CPRMV 0.3.0 or 0.4.1) + { ?dmn1 a cprmv:DecisionModel } UNION { ?dmn1 a cprmv041:DecisionModel } + ?dmn1 dct:identifier ?dmn1Identifier . ?output1 cpsv:produces ?dmn1 ; dct:identifier ?variableId ; dct:type ?variableType . - - # DMN 2 requires the same variable - ?dmn2 a cprmv:DecisionModel ; - dct:identifier ?dmn2Identifier . + + # DMN 2 requires the same variable (DecisionModel type: CPRMV 0.3.0 or 0.4.1) + { ?dmn2 a cprmv:DecisionModel } UNION { ?dmn2 a cprmv041:DecisionModel } + ?dmn2 dct:identifier ?dmn2Identifier . ?input2 cpsv:isRequiredBy ?dmn2 ; dct:identifier ?variableId . @@ -518,6 +529,7 @@ PREFIX skos: PREFIX dct: PREFIX cpsv: PREFIX cprmv: +PREFIX cprmv041: SELECT ?concept1 ?concept1Label ?concept1Notation ?variable1 ?variable1Id ?variable1Type ?concept2 ?concept2Label ?concept2Notation ?variable2 ?variable2Id ?variable2Type @@ -543,9 +555,9 @@ WHERE { cpsv:produces ?dmn1 . } - ?dmn1 a cprmv:DecisionModel ; - dct:title ?dmn1Title . - + { ?dmn1 a cprmv:DecisionModel } UNION { ?dmn1 a cprmv041:DecisionModel } + ?dmn1 dct:title ?dmn1Title . + # Second concept with same exactMatch ?concept2 a skos:Concept ; skos:exactMatch ?sharedConcept ; @@ -566,9 +578,9 @@ WHERE { cpsv:produces ?dmn2 . } - ?dmn2 a cprmv:DecisionModel ; - dct:title ?dmn2Title . - + { ?dmn2 a cprmv:DecisionModel } UNION { ?dmn2 a cprmv041:DecisionModel } + ?dmn2 dct:title ?dmn2Title . + # Ensure different concepts (but same sharedConcept URI) FILTER(?concept1 != ?concept2) FILTER(?dmn1 != ?dmn2) @@ -628,8 +640,9 @@ PREFIX skos: PREFIX dct: PREFIX cpsv: PREFIX cprmv: +PREFIX cprmv041: -SELECT DISTINCT ?dmn1 ?dmn1Identifier ?dmn1Title +SELECT DISTINCT ?dmn1 ?dmn1Identifier ?dmn1Title ?dmn2 ?dmn2Identifier ?dmn2Title ?outputVar ?outputVarId ?inputVar ?inputVarId ?variableType ?matchType ?sharedConcept @@ -650,13 +663,13 @@ WHERE { FILTER(?variableType = ?inputVarType) FILTER(?dmn1 != ?dmn2) - # Get DMN metadata - ?dmn1 a cprmv:DecisionModel ; - dct:identifier ?dmn1Identifier ; + # Get DMN metadata (DecisionModel type: CPRMV 0.3.0 or 0.4.1) + { ?dmn1 a cprmv:DecisionModel } UNION { ?dmn1 a cprmv041:DecisionModel } + ?dmn1 dct:identifier ?dmn1Identifier ; dct:title ?dmn1Title . - - ?dmn2 a cprmv:DecisionModel ; - dct:identifier ?dmn2Identifier ; + + { ?dmn2 a cprmv:DecisionModel } UNION { ?dmn2 a cprmv041:DecisionModel } + ?dmn2 dct:identifier ?dmn2Identifier ; dct:title ?dmn2Title . # Check for matching via identifier or concept @@ -773,6 +786,7 @@ PREFIX skos: PREFIX dct: PREFIX cpsv: PREFIX cprmv: +PREFIX cprmv041: SELECT DISTINCT ?dmn1 ?dmn1Title ?dmn2 ?dmn2Title ?dmn3 ?dmn3Title ?var1 ?var2 ?concept1 ?concept2 @@ -826,9 +840,12 @@ WHERE { dct:subject ?input1 . } - ?dmn1 a cprmv:DecisionModel ; dct:title ?dmn1Title . - ?dmn2 a cprmv:DecisionModel ; dct:title ?dmn2Title . - ?dmn3 a cprmv:DecisionModel ; dct:title ?dmn3Title . + { ?dmn1 a cprmv:DecisionModel } UNION { ?dmn1 a cprmv041:DecisionModel } + ?dmn1 dct:title ?dmn1Title . + { ?dmn2 a cprmv:DecisionModel } UNION { ?dmn2 a cprmv041:DecisionModel } + ?dmn2 dct:title ?dmn2Title . + { ?dmn3 a cprmv:DecisionModel } UNION { ?dmn3 a cprmv041:DecisionModel } + ?dmn3 dct:title ?dmn3Title . FILTER(?dmn1 != ?dmn2 && ?dmn2 != ?dmn3 && ?dmn3 != ?dmn1) } diff --git a/packages/backend/src/services/triplydb.service.ts b/packages/backend/src/services/triplydb.service.ts index 868cac3..f11030c 100644 --- a/packages/backend/src/services/triplydb.service.ts +++ b/packages/backend/src/services/triplydb.service.ts @@ -76,6 +76,59 @@ export async function executeQuery(endpoint: string, query: string): Promise { + logger.info('[TriplyDB Service] Executing SPARQL CONSTRUCT', { + endpoint: endpoint, + queryLength: query.length, + }); + + try { + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/sparql-query', + Accept: 'text/turtle', + }, + body: query, + }); + + if (!response.ok) { + const errorText = await response.text(); + logger.error('[TriplyDB Service] CONSTRUCT execution failed', { + status: response.status, + statusText: response.statusText, + error: errorText, + }); + throw new Error(`CONSTRUCT failed: ${response.status} ${errorText}`); + } + + const turtle = await response.text(); + + logger.info('[TriplyDB Service] CONSTRUCT executed successfully', { + bytes: turtle.length, + }); + + return turtle; + } catch (error) { + logger.error('[TriplyDB Service] Error executing CONSTRUCT', { + error: error instanceof Error ? error.message : 'Unknown error', + stack: error instanceof Error ? error.stack : undefined, + }); + throw new Error(`Failed to execute CONSTRUCT: ${(error as Error).message}`); + } +} + /** * List all graphs in a TriplyDB dataset */ diff --git a/packages/backend/src/types/dmn.types.ts b/packages/backend/src/types/dmn.types.ts index e0f00e1..4f992dd 100644 --- a/packages/backend/src/types/dmn.types.ts +++ b/packages/backend/src/types/dmn.types.ts @@ -26,6 +26,7 @@ export interface DmnModel { organization?: string; // NEW: Organization URI organizationName?: string; // NEW: Organization display name logoUrl?: string; // NEW: Full logo URL (resolved with version ID) + xmlUrl?: string; // NEW: Relative URL to download the deployed DMN XML from Operaton, e.g. "/v1/dmns//xml". Injected by the route handler, not sourced from SPARQL. inputs: DmnVariable[]; outputs: DmnVariable[]; diff --git a/packages/backend/src/types/shacl-rdf.d.ts b/packages/backend/src/types/shacl-rdf.d.ts new file mode 100644 index 0000000..f526c14 --- /dev/null +++ b/packages/backend/src/types/shacl-rdf.d.ts @@ -0,0 +1,54 @@ +// packages/backend/src/types/shacl-rdf.d.ts +// +// Ambient module declarations for the two RDF dependencies that ship as pure ESM +// (`"type": "module"`) without a `types` field: `rdf-validate-shacl` and +// `@rdfjs/dataset`. Under `moduleResolution: node10` + `noImplicitAny` these would +// otherwise fail to resolve at compile time. `n3` is covered by `@types/n3`. +// +// These declarations describe only the surface the SHACL validator uses — they are +// deliberately minimal, not a full typing of either library. + +interface RdfTerm { + termType: string; + value: string; +} + +interface RdfQuad { + subject: RdfTerm; + predicate: RdfTerm; + object: RdfTerm; + graph: RdfTerm; +} + +interface RdfDataset extends Iterable { + match(s?: RdfTerm | null, p?: RdfTerm | null, o?: RdfTerm | null, g?: RdfTerm | null): RdfDataset; + readonly size: number; +} + +declare module 'rdf-validate-shacl' { + interface ValidationResult { + focusNode: RdfTerm | null; + path: RdfTerm | null; + severity: RdfTerm | null; + sourceConstraintComponent: RdfTerm | null; + message: RdfTerm[]; + value: RdfTerm | null; + } + + interface ValidationReport { + conforms: boolean; + results: ValidationResult[]; + } + + export default class SHACLValidator { + constructor(shapes: Iterable, options?: { factory?: unknown }); + validate(data: Iterable): Promise; + } +} + +declare module '@rdfjs/dataset' { + const factory: { + dataset(quads?: Iterable): RdfDataset; + }; + export default factory; +} diff --git a/packages/backend/src/utils/config.ts b/packages/backend/src/utils/config.ts index 61c5b47..c6ce0a3 100644 --- a/packages/backend/src/utils/config.ts +++ b/packages/backend/src/utils/config.ts @@ -23,11 +23,7 @@ const getNestedProperty = (obj: Record, path: string): unknown * az webapp config appsettings set -g rg-... -n ronl-linkeddata-backend-prod --settings DEPLOYMENT_ENV=prod * Falls back to NODE_ENV when unset so local development stays zero-config. */ -const rawDeploymentEnv = ( - process.env.DEPLOYMENT_ENV || - process.env.NODE_ENV || - 'development' -) +const rawDeploymentEnv = (process.env.DEPLOYMENT_ENV || process.env.NODE_ENV || 'development') .toLowerCase() .trim(); @@ -89,6 +85,9 @@ export const config = { opvragenWerkzaamhedenBaseUrl: process.env.DSO_OPVRAGEN_WERKZAAMHEDEN_BASE_URL || 'https://service.pre.omgevingswet.overheid.nl/publiek/toepasbare-regels/api/opvragenwerkzaamheden/v1', + uitvoerenGegevensBaseUrl: + process.env.DSO_UITVOEREN_GEGEVENS_BASE_URL || + 'https://service.pre.omgevingswet.overheid.nl/publiek/toepasbare-regels/api/toepasbareregelsuitvoerengegevens/v1', apiKey: process.env.DSO_API_KEY || '', timeout: parseInt(process.env.DSO_TIMEOUT || '15000', 10), }, @@ -106,6 +105,9 @@ export const config = { opvragenWerkzaamhedenBaseUrl: process.env.DSO_OPVRAGEN_WERKZAAMHEDEN_BASE_URL_PROD || 'https://service.omgevingswet.overheid.nl/publiek/toepasbare-regels/api/opvragenwerkzaamheden/v1', + uitvoerenGegevensBaseUrl: + process.env.DSO_UITVOEREN_GEGEVENS_BASE_URL_PROD || + 'https://service.omgevingswet.overheid.nl/publiek/toepasbare-regels/api/toepasbareregelsuitvoerengegevens/v1', apiKey: process.env.DSO_API_KEY_PROD || '', }, diff --git a/packages/backend/src/utils/rootViews.ts b/packages/backend/src/utils/rootViews.ts index 57e0244..a2daf2a 100644 --- a/packages/backend/src/utils/rootViews.ts +++ b/packages/backend/src/utils/rootViews.ts @@ -12,28 +12,29 @@ import packageJson from '../../package.json'; // Stable group order on the root page. Categories not listed here render after // the listed ones in registry order. const CATEGORY_ORDER: ReadonlyArray = [ - 'Health & monitoring', - 'Discovery', - 'Execution', - 'Assets', - 'Integrations', + 'Health & monitoring', + 'Discovery', + 'Validation', + 'Execution', + 'Assets', + 'Integrations', ]; interface RootEndpoint { - mount: string; - summary: string; - publicCors: boolean; + mount: string; + summary: string; + publicCors: boolean; } interface RootPayload { - name: string; - version: string; - environment: string; - status: 'running'; - documentation: string; - health: string; - endpoints: Record; - legacy: Record; + name: string; + version: string; + environment: string; + status: 'running'; + documentation: string; + health: string; + endpoints: Record; + legacy: Record; } /** @@ -41,46 +42,46 @@ interface RootPayload { * data shape ensures both views always describe the same set of endpoints. */ function buildPayload(): RootPayload { - const endpoints: RootPayload['endpoints'] = {}; + const endpoints: RootPayload['endpoints'] = {}; - // Seed groups in the preferred order; unknown categories appended later. - for (const cat of CATEGORY_ORDER) { - endpoints[cat] = []; - } - for (const route of routeRegistry) { - const list = endpoints[route.category] ?? (endpoints[route.category] = []); - list.push({ - mount: route.mount, - summary: route.summary, - publicCors: route.publicCors === true, - }); - } - // Drop any seeded-but-empty groups (e.g. a category with no routes yet). - for (const cat of Object.keys(endpoints)) { - if (endpoints[cat].length === 0) delete endpoints[cat]; - } + // Seed groups in the preferred order; unknown categories appended later. + for (const cat of CATEGORY_ORDER) { + endpoints[cat] = []; + } + for (const route of routeRegistry) { + const list = endpoints[route.category] ?? (endpoints[route.category] = []); + list.push({ + mount: route.mount, + summary: route.summary, + publicCors: route.publicCors === true, + }); + } + // Drop any seeded-but-empty groups (e.g. a category with no routes yet). + for (const cat of Object.keys(endpoints)) { + if (endpoints[cat].length === 0) delete endpoints[cat]; + } - return { - name: 'Linked Data Explorer Backend', - version: packageJson.version, - environment: config.displayEnv, - status: 'running', - documentation: '/v1/openapi.json', - health: '/v1/health', - endpoints, - // Hand-listed legacy aliases. The /api/* routes are deprecated and - // intentionally not in the registry — keeping them visible here so clients - // polling the root can still discover the migration path. - legacy: { - health: '/api/health (deprecated)', - dmns: '/api/dmns (deprecated)', - cache: '/api/cache (deprecated)', - 'chains/templates': '/api/chains/templates (deprecated)', - chains: '/api/chains (deprecated)', - triplydb: '/api/triplydb (deprecated)', - vendors: '/api/vendors (deprecated)', - }, - }; + return { + name: 'Linked Data Explorer Backend', + version: packageJson.version, + environment: config.displayEnv, + status: 'running', + documentation: '/v1/openapi.json', + health: '/v1/health', + endpoints, + // Hand-listed legacy aliases. The /api/* routes are deprecated and + // intentionally not in the registry — keeping them visible here so clients + // polling the root can still discover the migration path. + legacy: { + health: '/api/health (deprecated)', + dmns: '/api/dmns (deprecated)', + cache: '/api/cache (deprecated)', + 'chains/templates': '/api/chains/templates (deprecated)', + chains: '/api/chains (deprecated)', + triplydb: '/api/triplydb (deprecated)', + vendors: '/api/vendors (deprecated)', + }, + }; } // Minimal HTML escaper. Registry strings are author-controlled, but the @@ -88,52 +89,50 @@ function buildPayload(): RootPayload { // is cheap to uphold and protects against future drift (e.g. if a route // summary ever pulls from a config source). function escapeHtml(s: string): string { - return s - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); + return s + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); } function renderHtml(payload: RootPayload): string { - const groups = Object.entries(payload.endpoints) - .map(([category, routes]) => { - const items = routes - .map((r) => { - const badge = r.publicCors - ? `public · open CORS` - : ''; - return ( - `
  • ` + - `
    ${escapeHtml(r.mount)}${badge}
    ` + - `${escapeHtml(r.summary)}` + - `
  • ` - ); - }) - .join('\n'); - return ( - `
    \n` + - `

    ${escapeHtml(category)}

    \n` + - `
      \n${items}\n
    \n` + - `
    ` - ); + const groups = Object.entries(payload.endpoints) + .map(([category, routes]) => { + const items = routes + .map((r) => { + const badge = r.publicCors ? `public · open CORS` : ''; + return ( + `
  • ` + + `
    ${escapeHtml(r.mount)}${badge}
    ` + + `${escapeHtml(r.summary)}` + + `
  • ` + ); }) .join('\n'); + return ( + `
    \n` + + `

    ${escapeHtml(category)}

    \n` + + `
      \n${items}\n
    \n` + + `
    ` + ); + }) + .join('\n'); - const legacyRows = Object.entries(payload.legacy) - .map(([key, value]) => { - const path = value.replace(' (deprecated)', ''); - return ( - `
  • ` + - `
    ${escapeHtml(path)}
    ` + - `replaced by /v1/${escapeHtml(key)}` + - `
  • ` - ); - }) - .join('\n'); + const legacyRows = Object.entries(payload.legacy) + .map(([key, value]) => { + const path = value.replace(' (deprecated)', ''); + return ( + `
  • ` + + `
    ${escapeHtml(path)}
    ` + + `replaced by /v1/${escapeHtml(key)}` + + `
  • ` + ); + }) + .join('\n'); - return ` + return ` @@ -254,13 +253,13 @@ ${legacyRows} * any existing programmatic poller of the root endpoint. */ export function rootHandler(req: Request, res: Response): void { - const payload = buildPayload(); - const accepted = req.accepts(['json', 'html']); + const payload = buildPayload(); + const accepted = req.accepts(['json', 'html']); - if (accepted === 'html') { - res.type('html').send(renderHtml(payload)); - return; - } + if (accepted === 'html') { + res.type('html').send(renderHtml(payload)); + return; + } - res.json(payload); -} \ No newline at end of file + res.json(payload); +} diff --git a/packages/backend/tests/fixtures/shacl/cprmv-conformant.ttl b/packages/backend/tests/fixtures/shacl/cprmv-conformant.ttl new file mode 100644 index 0000000..46f763e --- /dev/null +++ b/packages/backend/tests/fixtures/shacl/cprmv-conformant.ttl @@ -0,0 +1,77 @@ +@prefix ex: . +@prefix cpsv: . +@prefix xsd: . +@prefix cprmv: . + +ex:myRuleMethod a cprmv:RuleMethod ; + cprmv:id "test" ; + cprmv:comment "test comment" . + +ex:myAnalysisMethod a cprmv:AnalysisMethod ; + cprmv:id "test" . + +ex:myFormalisationMethod a cprmv:FormalisationMethod ; + cprmv:id "test" . + +ex:myCodificationMethod a cprmv:CodificationMethod ; + cprmv:id "test" . + +ex:myExecutionMethod a cprmv:ExecutionMethod ; + cprmv:id "test" . + +ex:myExplanationMethod a cprmv:ExplanationMethod ; + cprmv:id "test" . + +ex:myTestMethod a cprmv:TestMethod ; + cprmv:id "test" . + +ex:myPublicationMethod a cprmv:PublicationMethod ; + cprmv:id "test" . + +ex:myReferenceMethod a cprmv:ReferenceMethod ; + cprmv:id "test" . + +ex:myRuleSet a cprmv:RuleSet ; + cprmv:id "test" ; + cprmv:isOutputOf [a cpsv:PublicService] ; + cprmv:hasMethod ex:myRuleMethod ; + cprmv:hasPart ( ex:myRuleA ) ; + cprmv:validFrom "2026-01-01"^^xsd:date . + +ex:myAnalysis a cprmv:Analysis ; + cprmv:id "test" ; + cprmv:isOutputOf [a cpsv:PublicService] ; + cprmv:hasMethod ex:myRuleMethod ; + cprmv:hasPart ( ex:myRuleA ) ; + cprmv:validFrom "2026-01-01"^^xsd:date . + +ex:myDecisionModel a cprmv:DecisionModel ; + cprmv:id "test" ; + cprmv:hasAnalysis ex:myAnalysis ; + cprmv:isOutputOf [a cpsv:PublicService] ; + cprmv:hasMethod ex:myRuleMethod ; + cprmv:hasPart ( ex:myRuleA ) ; + cprmv:validFrom "2026-01-01"^^xsd:date . + +ex:myRuleA a cprmv:Rule ; + cprmv:id "test" . + +ex:myRuleB a cprmv:Rule ; + cprmv:id "test" ; + cprmv:isBasedOn ex:myRuleA . + +ex:myExplanation a cprmv:Explanation ; + cprmv:id "test" ; + cprmv:explains ex:myRuleB . + +ex:testSet a cprmv:testSet ; + cprmv:id "test" . + +ex:testCase a cprmv:testCase ; + cprmv:id "test" . + +ex:testDecision a cprmv:Decision ; + cprmv:id "test" . + +ex:Case a cprmv:Case ; + cprmv:id "test" . diff --git a/packages/backend/tests/fixtures/shacl/cprmv-nonconformant.ttl b/packages/backend/tests/fixtures/shacl/cprmv-nonconformant.ttl new file mode 100644 index 0000000..690ebed --- /dev/null +++ b/packages/backend/tests/fixtures/shacl/cprmv-nonconformant.ttl @@ -0,0 +1,27 @@ +@prefix ex: . +@prefix xsd: . +@prefix cprmv: . + +# The nonconformant counterpart to cprmv-conformant.ttl — every finding lands in the CPRMV +# 0.4.1 layer. No cpsv:* or cv:* subjects are present, so the CPSV-AP and RONL Custom +# layers stay clean and these errors are unambiguously CPRMV. +# +# cprmv:RuleSetShape violations on ex:badRuleSet: +# - cprmv:id absent .......................... sh:minCount 1 -> MinCount +# - two cprmv:comment values ................. sh:maxCount 1 -> MaxCount +# - cprmv:validFrom is a plain string, ....... sh:datatype xsd:date -> Datatype +# not an xsd:date (the value also satisfies min/maxCount 1) +# - cprmv:isOutputOf absent .................. sh:minCount 1 (cpsv:PublicService) +# - cprmv:hasMethod absent ................... sh:minCount 1 (cprmv:RuleMethod) +# - cprmv:hasPart absent ..................... sh:minCount 1 +# +# cprmv:RuleShape violation on ex:badRule: +# - cprmv:id absent .......................... sh:minCount 1 -> MinCount + +ex:badRuleSet a cprmv:RuleSet ; + cprmv:comment "first comment" ; + cprmv:comment "second comment" ; + cprmv:validFrom "2026-01-01" . + +ex:badRule a cprmv:Rule ; + cprmv:definition "A rule definition that is missing its required cprmv:id." . diff --git a/packages/backend/tests/fixtures/shacl/cpsv-ap-conformant.ttl b/packages/backend/tests/fixtures/shacl/cpsv-ap-conformant.ttl new file mode 100644 index 0000000..3c23b38 --- /dev/null +++ b/packages/backend/tests/fixtures/shacl/cpsv-ap-conformant.ttl @@ -0,0 +1,14 @@ +@prefix cpsv: . +@prefix dct: . + +# A fully CPSV-AP 3.2.0 conformant cpsv:Rule: carries the required dct:identifier, +# and bilingual (nl/en) title and description. Demonstrates the all-green case — +# clean against both the CPSV-AP layer and the RONL layer — and that sh:uniqueLang +# correctly allows one label per language (one @nl + one @en) rather than flagging it. + + a cpsv:Rule ; + dct:identifier "RONL-FL-SUBSIDIE-THUISBATTERIJ-001" ; + dct:title "Subsidie thuisbatterij — bepaling subsidiehoogte"@nl ; + dct:title "Home battery subsidy — amount determination"@en ; + dct:description "Bepaalt de hoogte van de subsidie voor een thuisbatterij, met inachtneming van het beschikbare budget."@nl ; + dct:description "Determines the home battery subsidy amount, taking the available budget into account."@en . diff --git a/packages/backend/tests/fixtures/shacl/cpsv-ap-rule-nonconformant.ttl b/packages/backend/tests/fixtures/shacl/cpsv-ap-rule-nonconformant.ttl new file mode 100644 index 0000000..573ae9f --- /dev/null +++ b/packages/backend/tests/fixtures/shacl/cpsv-ap-rule-nonconformant.ttl @@ -0,0 +1,13 @@ +@prefix cpsv: . +@prefix dct: . + +# A cpsv:Rule that breaks the canonical CPSV-AP 3.2.0 RuleShape — the non-conformant +# counterpart to cpsv-ap-conformant.ttl, exercising the CPSV-AP layer specifically: +# - dct:title is a plain string, not rdf:langString -> Datatype violation +# - dct:identifier is absent (sh:minCount 1) -> MinCount violation +# - dct:description is absent (sh:minCount 1) -> MinCount violation +# => 3 CPSV-AP errors. The RONL Custom layer stays clean: a single plain-language +# title cannot violate sh:uniqueLang, so this isolates the CPSV-AP findings. + + a cpsv:Rule ; + dct:title "Subsidie thuisbatterij — bepaling subsidiehoogte" . diff --git a/packages/backend/tests/fixtures/shacl/malformed.ttl b/packages/backend/tests/fixtures/shacl/malformed.ttl new file mode 100644 index 0000000..6c10bb3 --- /dev/null +++ b/packages/backend/tests/fixtures/shacl/malformed.ttl @@ -0,0 +1,11 @@ +# Deliberately malformed Turtle — exercises the parser branch of the validator, +# which must return { valid: false, parseError: } rather than throwing or +# reporting SHACL violations. The dct:title statement is missing its closing quote +# and terminating period, so n3's Parser.parse() raises. + +@prefix cpsv: . +@prefix dct: . + + a cpsv:Rule ; + dct:identifier "RONL-FL-001" ; + dct:title "Subsidie thuisbatterij diff --git a/packages/backend/tests/fixtures/shacl/org-collision-fail.ttl b/packages/backend/tests/fixtures/shacl/org-collision-fail.ttl new file mode 100644 index 0000000..568ce47 --- /dev/null +++ b/packages/backend/tests/fixtures/shacl/org-collision-fail.ttl @@ -0,0 +1,20 @@ +@prefix cv: . +@prefix foaf: . +@prefix skos: . +@prefix dct: . + +# A single cv:PublicOrganisation subject carrying DIVERGENT single-valued identity +# properties — the result of merging two published TTL files that disagree. This is +# the organisation-level counterpart to rule-collision-fail.ttl, and exercises the +# RONL Custom layer (PublicOrganisationUniquenessShape): +# - two foaf:homepage values (with / without "www.") -> sh:maxCount 1 -> 1 error +# - two skos:prefLabel@nl values -> sh:uniqueLang -> 1 error +# => exactly 2 RONL Custom errors. (Independent of the CPSV-AP layer, which adds its +# own separate finding here for the missing dct:spatial.) + + a cv:PublicOrganisation ; + dct:identifier "Provincie_Flevoland" ; + skos:prefLabel "Provincie Flevoland"@nl ; + skos:prefLabel "Provincie Flevoland (gefuseerd)"@nl ; + foaf:homepage ; + foaf:homepage . diff --git a/packages/backend/tests/fixtures/shacl/org-collision-pass.ttl b/packages/backend/tests/fixtures/shacl/org-collision-pass.ttl new file mode 100644 index 0000000..eb19c39 --- /dev/null +++ b/packages/backend/tests/fixtures/shacl/org-collision-pass.ttl @@ -0,0 +1,19 @@ +@prefix cv: . +@prefix foaf: . +@prefix skos: . +@prefix dct: . + +# A fully conformant cv:PublicOrganisation: one homepage, one identifier, and a +# bilingual (nl/en) preferred label. The all-green organisation case — clean against +# BOTH the CPSV-AP layer (dct:spatial present, pointing at a dct:Location; prefLabel +# is rdf:langString) and the RONL Custom layer (single homepage; sh:uniqueLang allows +# one label per language rather than flagging the @nl + @en pair). + + a cv:PublicOrganisation ; + dct:identifier "Provincie_Flevoland" ; + skos:prefLabel "Provincie Flevoland"@nl ; + skos:prefLabel "Province of Flevoland"@en ; + foaf:homepage ; + dct:spatial . + + a dct:Location . diff --git a/packages/backend/tests/fixtures/shacl/rule-collision-fail.ttl b/packages/backend/tests/fixtures/shacl/rule-collision-fail.ttl new file mode 100644 index 0000000..011cc59 --- /dev/null +++ b/packages/backend/tests/fixtures/shacl/rule-collision-fail.ttl @@ -0,0 +1,14 @@ +@prefix cpsv: . +@prefix dct: . + + a cpsv:Rule ; + dct:title "Subsidie aanvraag intake"@nl ; + dct:description "Beoordeelt of de subsidieaanvraag volledig is ingediend."@nl . + + a cpsv:Rule ; + dct:title "Subsidie inhoudelijke beoordeling"@nl ; + dct:description "Toetst de aanvraag aan de inhoudelijke subsidiecriteria."@nl . + + a cpsv:Rule ; + dct:title "Subsidie besluit"@nl ; + dct:description "Stelt het toekennings- of afwijzingsbesluit vast."@nl . diff --git a/packages/backend/tests/fixtures/shacl/rule-collision-pass.ttl b/packages/backend/tests/fixtures/shacl/rule-collision-pass.ttl new file mode 100644 index 0000000..6d8fe8c --- /dev/null +++ b/packages/backend/tests/fixtures/shacl/rule-collision-pass.ttl @@ -0,0 +1,14 @@ +@prefix cpsv: . +@prefix dct: . + + a cpsv:Rule ; + dct:title "Subsidie aanvraag intake"@nl ; + dct:description "Beoordeelt of de subsidieaanvraag volledig is ingediend."@nl . + + a cpsv:Rule ; + dct:title "Subsidie inhoudelijke beoordeling"@nl ; + dct:description "Toetst de aanvraag aan de inhoudelijke subsidiecriteria."@nl . + + a cpsv:Rule ; + dct:title "Subsidie besluit"@nl ; + dct:description "Stelt het toekennings- of afwijzingsbesluit vast."@nl . diff --git a/packages/backend/tsconfig.eslint.json b/packages/backend/tsconfig.eslint.json new file mode 100644 index 0000000..65a2f06 --- /dev/null +++ b/packages/backend/tsconfig.eslint.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*", "scripts/**/*", "tests/**/*", "*.ts", "*.tsx"] +} diff --git a/packages/backend/tsconfig.json b/packages/backend/tsconfig.json index 06cad77..3f007b8 100644 --- a/packages/backend/tsconfig.json +++ b/packages/backend/tsconfig.json @@ -31,6 +31,9 @@ "jest" ] }, + "ts-node": { + "files": true + }, "include": [ "src/**/*", "*.ts", diff --git a/packages/frontend/.env b/packages/frontend/.env deleted file mode 100644 index 6df67ea..0000000 --- a/packages/frontend/.env +++ /dev/null @@ -1,5 +0,0 @@ -# Local Development Environment -# Used when running: npm run dev - -VITE_API_BASE_URL=http://localhost:3001 -VITE_OPERATON_BASE_URL=https://operaton.open-regels.nl/engine-rest \ No newline at end of file diff --git a/packages/frontend/.env.acceptance b/packages/frontend/.env.acceptance index f4c3cd0..4d49e02 100644 --- a/packages/frontend/.env.acceptance +++ b/packages/frontend/.env.acceptance @@ -2,4 +2,5 @@ # Used when running: npm run build:acc or npm run build --mode acceptance VITE_API_BASE_URL=https://acc.backend.linkeddata.open-regels.nl -VITE_OPERATON_BASE_URL=https://operaton.open-regels.nl/engine-rest \ No newline at end of file +VITE_OPERATON_BASE_URL=https://operaton.open-regels.nl/engine-rest +VITE_CPSV_EDITOR_URL=https://acc.cpsv-editor.open-regels.nl diff --git a/packages/frontend/.env.development b/packages/frontend/.env.development index 6df67ea..8963dfc 100644 --- a/packages/frontend/.env.development +++ b/packages/frontend/.env.development @@ -2,4 +2,7 @@ # Used when running: npm run dev VITE_API_BASE_URL=http://localhost:3001 -VITE_OPERATON_BASE_URL=https://operaton.open-regels.nl/engine-rest \ No newline at end of file +VITE_OPERATON_BASE_URL=https://operaton.open-regels.nl/engine-rest +# CPSV Editor (DSO → DMN publish handoff). Run the CPSV Editor on a non-3000 +# port locally since LDE's dev server also uses 3000. +VITE_CPSV_EDITOR_URL=http://localhost:3002 diff --git a/packages/frontend/.env.production b/packages/frontend/.env.production index 24fc60e..af70ef5 100644 --- a/packages/frontend/.env.production +++ b/packages/frontend/.env.production @@ -2,4 +2,5 @@ # Used when running: npm run build:prod or npm run build (default) VITE_API_BASE_URL=https://backend.linkeddata.open-regels.nl -VITE_OPERATON_BASE_URL=https://operaton.open-regels.nl/engine-rest \ No newline at end of file +VITE_OPERATON_BASE_URL=https://operaton.open-regels.nl/engine-rest +VITE_CPSV_EDITOR_URL=https://cpsv-editor.open-regels.nl diff --git a/packages/frontend/public/examples/flevoland/TreeFellingPermitSubProcess.bpmn b/packages/frontend/public/examples/flevoland/TreeFellingPermitSubProcess.bpmn index bd2a57a..af41432 100644 --- a/packages/frontend/public/examples/flevoland/TreeFellingPermitSubProcess.bpmn +++ b/packages/frontend/public/examples/flevoland/TreeFellingPermitSubProcess.bpmn @@ -6,6 +6,7 @@ xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" + xmlns:ronl="http://ronl.nl/schema/1.0" id="Definitions_TreeFellingSubProcess" targetNamespace="http://example.com/tree-permit" exporter="Camunda Modeler" @@ -43,7 +44,7 @@ Legal basis: APV (Local Government By-law) - material law --> - + diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index f993016..fca6b22 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -1,5 +1,6 @@ import { AlertCircle, + BadgeCheck, BookOpen, Code2, Database, @@ -32,6 +33,7 @@ import FormEditor from './components/FormEditor/FormEditor'; import GraphView from './components/GraphView'; import ResultsTable from './components/ResultsTable'; import RopaEditor from './components/RopaEditor/RopaEditor'; +import ShaclValidator from './components/ShaclValidator'; import Tutorial from './components/Tutorial/Tutorial'; import { executeSparqlQuery } from './services/sparqlService'; import { SparqlResponse, ViewMode } from './types'; @@ -302,6 +304,14 @@ const App: React.FC = () => { + + + {/* Board ownership — deploy-time boardOwner tag */} +
    +
    +
    🏷️ Board ownership
    + {boardAuto ? ( + + auto-detected: {boardAuto} + + ) : ( + no board auto-detected + )} +
    +
    + {( + [ + { id: 'auto', label: boardAuto ? `Auto (${boardAuto})` : 'Auto (none)' }, + { id: 'infra-board', label: 'Infra-board' }, + { id: 'caseworker', label: 'Caseworker' }, + ] as { id: BoardChoice; label: string }[] + ).map((opt) => { + const active = boardChoice === opt.id; + return ( + + ); + })} +
    +
    + {boardChoice === 'auto' ? ( + boardAuto ? ( + <> + Auto-detected {boardAuto} from the + candidate groups. + + ) : ( + 'No board could be auto-detected — pick one to continue.' + ) + ) : ( + <> + Tagging as {boardChoice} — overrides + auto-detection. + + )} +
    + {!resolvedBoard && ( +
    + ⚠️ A board owner is required. Select{' '} + Infra-board or{' '} + Caseworker before deploying. +
    + )} +
    + {/* Resources preview */}
    🚀 Resources to deploy
    @@ -799,7 +921,7 @@ const BpmnCanvas: React.FC = ({
    ); +// ── Applicable Rules (STTR) ────────────────────────────────────────────────── + +const ApplicableRuleRow: React.FC<{ + regel: DsoRegelbeheerobject; + env: DsoEnv; + activityName?: string; + organization?: string; + activityUrn?: string; +}> = ({ regel, env, activityName, organization, activityUrn }) => { + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [scaffoldLoading, setScaffoldLoading] = useState(false); + const [scaffoldError, setScaffoldError] = useState(null); + const [importing, setImporting] = useState(false); + const [imported, setImported] = useState(false); + const [importError, setImportError] = useState(null); + + useEffect(() => { + if (!regel.functioneleStructuurRef) return; + setLoading(true); + setError(null); + fetchToepasbareRegels(regel.functioneleStructuurRef, env) + .then(setResult) + .catch((e) => setError(e instanceof Error ? e.message : 'Failed to load')) + .finally(() => setLoading(false)); + }, [regel.functioneleStructuurRef, env]); + + const meta = TYPERING_META[regel.typering] ?? { + label: regel.typering, + color: 'bg-slate-100 text-slate-600 border-slate-200', + }; + + const handleDownloadScaffold = async (item: DsoToepasbareRegel) => { + setScaffoldLoading(true); + setScaffoldError(null); + try { + const scaffold = await fetchFormScaffold(item.identifier, `form-${item.identifier}`, env); + const blob = new Blob([JSON.stringify(scaffold, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `form-scaffold-${item.identifier}.json`; + a.click(); + URL.revokeObjectURL(url); + } catch (e) { + setScaffoldError(e instanceof Error ? e.message : 'Scaffold generation failed'); + } finally { + setScaffoldLoading(false); + } + }; + + // Import the generated scaffold straight into the LDE form store so it shows + // up in the Form Editor. Mirrors FormEditor.handleImportForm: the DSO scaffold + // is already a form-js schema, we just stamp the execution-platform metadata + // the editor and Operaton deploy expect (the scaffold omits it). + const handleImportScaffold = async (item: DsoToepasbareRegel) => { + setImporting(true); + setImportError(null); + try { + const scaffold = await fetchFormScaffold(item.identifier, `form-${item.identifier}`, env); + const id = `form_dso_${item.identifier}`; + const now = new Date().toISOString(); + const newForm: FormSchema = { + id, + name: activityName + ? `${activityName} — Submission requirements` + : `DSO form ${item.identifier}`, + schema: { + ...scaffold, + id, + executionPlatform: 'Camunda Platform', + executionPlatformVersion: '7.21.0', + }, + createdAt: now, + updatedAt: now, + status: 'dso', + language: 'nl', + organization, + }; + FormService.saveForm(newForm); + setImported(true); + } catch (e) { + setImportError(e instanceof Error ? e.message : 'Import failed'); + } finally { + setImporting(false); + } + }; + + return ( +
    + + {meta.label} + + + {loading && ( +
    + + Loading… +
    + )} + {error &&

    {error}

    } + + {result?.items.map((item, idx) => ( +
    +
    + {item.begindatum && ( +

    + Valid from: + {item.begindatum} +

    + )} + {item.sttrVersie !== undefined && ( +

    + STTR version: + {item.sttrVersie} +

    + )} +

    id: {item.identifier}

    +
    + +
    + + ↓ STTR + + {regel.typering.toLowerCase() === 'conclusie' && ( + <> + + ↓ Extract DMN + + + Publish via CPSV Editor + + + )} + {regel.typering.toLowerCase() === 'indieningsvereisten' && ( + <> + + + + )} +
    + {scaffoldError &&

    {scaffoldError}

    } + {importError &&

    {importError}

    } + {imported && ( +

    Saved to Form Editor as a draft.

    + )} +
    + ))} + + {!loading && !error && result?.items.length === 0 && ( +

    No toepasbare regels found.

    + )} +
    + ); +}; + +const ApplicableRulesSection: React.FC<{ + regelBeheerObjecten: DsoRegelbeheerobject[]; + env: DsoEnv; + activityName?: string; + organization?: string; + activityUrn?: string; +}> = ({ regelBeheerObjecten, env, activityName, organization, activityUrn }) => { + const candidates = regelBeheerObjecten.filter( + (r) => + r.functioneleStructuurRef && + (r.typering.toLowerCase() === 'conclusie' || + r.typering.toLowerCase() === 'indieningsvereisten') + ); + if (candidates.length === 0) return null; + + return ( +
    +
    + {candidates.map((r) => ( + + ))} +
    +
    + ); +}; + const ActivityDetailPanel: React.FC<{ urn: string; datum?: string; @@ -678,6 +1004,17 @@ const ActivityDetailPanel: React.FC<{ )} + {/* Applicable rules (STTR) */} + {detail.regelBeheerObjecten && detail.regelBeheerObjecten.length > 0 && ( + + )} + {/* Child activities */} {detail._links?.onderliggendeActiviteiten && detail._links.onderliggendeActiviteiten.length > 0 && ( @@ -758,10 +1095,7 @@ const ActiviteitRow: React.FC<{ const ActiviteitenTab: React.FC<{ env: DsoEnv }> = ({ env }) => { // ── preset authorities ─────────────────────────────────────────────── - const PRESETS = [ - { label: 'Lelystad', oin: '00000001005024249000' }, - { label: 'Flevoland', oin: '00000001006203243000' }, - ] as const; + const PRESETS = LOCATION_PRESETS; const [datum, setDatum] = useState(''); const [activeDatum, setActiveDatum] = useState(undefined); @@ -773,6 +1107,9 @@ const ActiviteitenTab: React.FC<{ env: DsoEnv }> = ({ env }) => { const [selectedUrn, setSelectedUrn] = useState(null); const [urnInput, setUrnInput] = useState(''); const [activePreset, setActivePreset] = useState(null); + // Client-side name filter — only meaningful when a location preset is fixed, + // since OIN mode loads the authority's full activity set in one call. + const [nameFilter, setNameFilter] = useState(''); const toDsoDate = (iso: string) => { if (!iso) return undefined; @@ -785,6 +1122,7 @@ const ActiviteitenTab: React.FC<{ env: DsoEnv }> = ({ env }) => { setLoading(true); setError(null); setOinMode(false); + setNameFilter(''); try { const dsoDate = toDsoDate(d); const res = await getActiviteiten(dsoDate, p, env); @@ -840,6 +1178,7 @@ const ActiviteitenTab: React.FC<{ env: DsoEnv }> = ({ env }) => { const isActive = activePreset === preset.label; setActivePreset(isActive ? null : preset.label); setSelectedUrn(null); + setNameFilter(''); if (isActive) { setOinMode(false); setDatum(''); @@ -860,6 +1199,14 @@ const ActiviteitenTab: React.FC<{ env: DsoEnv }> = ({ env }) => { load(datum, p); }; + // In OIN mode the full authority set is loaded, so filter by name client-side. + const filteredItems = + oinMode && nameFilter.trim() + ? (result?.items ?? []).filter((a) => + (a.omschrijving ?? a.urn).toLowerCase().includes(nameFilter.trim().toLowerCase()) + ) + : (result?.items ?? []); + return (
    {/* Toolbar */} @@ -909,6 +1256,7 @@ const ActiviteitenTab: React.FC<{ env: DsoEnv }> = ({ env }) => { setOinMode(false); setSelectedUrn(null); setDatum(''); + setNameFilter(''); load('', 1); }} className="ml-auto text-[10px] text-slate-400 hover:text-slate-600 underline transition-colors" @@ -918,6 +1266,34 @@ const ActiviteitenTab: React.FC<{ env: DsoEnv }> = ({ env }) => { )}
    + {/* Row 2b: name search — only when a location is fixed */} + {oinMode && ( +
    +
    + + { + setNameFilter(e.target.value); + setSelectedUrn(null); + }} + placeholder={`Filter ${activePreset ?? 'location'} activities by name…`} + className="w-full pl-8 pr-3 py-1.5 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none" + /> +
    + {nameFilter && ( + + )} +
    + )} {/* Row 3: URN paste */}
    = ({ env }) => { {error}
    )} - {!loading && !error && result && result.items.length === 0 && ( + {!loading && !error && result && filteredItems.length === 0 && (

    - {oinMode - ? 'No activities found for this authority on the selected date.' - : 'No activities found.'} + {oinMode && nameFilter.trim() + ? `No activities matching “${nameFilter.trim()}”.` + : oinMode + ? 'No activities found for this authority on the selected date.' + : 'No activities found.'}

    )} {!loading && !error && - result?.items.map((a) => ( + filteredItems.map((a) => ( = ({ env }) => { {result && (result.items.length > 0 || page > 1) && (
    - Page {page} · {result.items.length} items + {oinMode + ? nameFilter.trim() + ? `${filteredItems.length} of ${result.items.length} activities` + : `${result.items.length} activities` + : `Page ${page} · ${result.items.length} items`} -
    - - -
    + {/* OIN mode loads the full set in one call — no pagination */} + {!oinMode && ( +
    + + +
    + )}
    )}
    diff --git a/packages/frontend/src/components/FormEditor/FormList.tsx b/packages/frontend/src/components/FormEditor/FormList.tsx index ee398f3..fe2a41b 100644 --- a/packages/frontend/src/components/FormEditor/FormList.tsx +++ b/packages/frontend/src/components/FormEditor/FormList.tsx @@ -186,6 +186,11 @@ const FormList: React.FC = ({ WIP )} + {form.status === 'dso' && ( + + DSO + + )}

    {new Date(form.updatedAt).toLocaleDateString()} diff --git a/packages/frontend/src/components/ShaclValidator.tsx b/packages/frontend/src/components/ShaclValidator.tsx new file mode 100644 index 0000000..e005723 --- /dev/null +++ b/packages/frontend/src/components/ShaclValidator.tsx @@ -0,0 +1,615 @@ +/** + * packages/frontend/src/components/ShaclValidator.tsx + * + * Multi-file SHACL Validator — drop any number of .ttl files, validate them all + * against the CPSV-AP 3.2.0 + RONL SHACL shapes, compare side-by-side. Cloned from + * DmnValidator.tsx; the issue/layer rendering is identical because the backend + * returns the same { valid, parseError, layers, summary } shape. + * + * Two modes: + * - File-local → POST /v1/shacl/validate + * - Merge-simulated → POST /v1/shacl/validate-merged (unions the file with the + * already-published triples for its subjects; catches fan-out + * against live data. Results depend on current published data.) + */ + +import { + AlertCircle, + AlertTriangle, + CheckCircle, + ChevronDown, + ChevronUp, + CircleDashed, + Database, + FileText, + Info, + Layers, + Plus, + ShieldCheck, + X, +} from 'lucide-react'; +import React, { useRef, useState } from 'react'; + +// ── Types ───────────────────────────────────────────────────────────────────── + +interface ValidationIssue { + severity: 'error' | 'warning' | 'info'; + code: string; + message: string; + location?: string; + line?: number; + column?: number; +} + +interface LayerResult { + label: string; + loaded: boolean; + issues: ValidationIssue[]; +} + +interface ValidationResult { + valid: boolean; + parseError: string | null; + layers: { + cprmv: LayerResult; + 'cpsv-ap': LayerResult; + 'ronl-custom': LayerResult; + }; + summary: { errors: number; warnings: number; infos: number }; +} + +type ValidationMode = 'file' | 'merged'; + +interface ShaclEntry { + id: string; + name: string; + size: number; + content: string; + isValidating: boolean; + result: ValidationResult | null; + error: string | null; +} + +// ── Sub-components ───────────────────────────────────────────────────────────── + +function SeverityIcon({ severity, size = 13 }: { severity: string; size?: number }) { + if (severity === 'error') + return ; + if (severity === 'warning') + return ; + return ; +} + +function IssueRow({ issue }: { issue: ValidationIssue }) { + const bg = + issue.severity === 'error' + ? 'bg-red-50 border-red-100' + : issue.severity === 'warning' + ? 'bg-yellow-50 border-yellow-100' + : 'bg-blue-50 border-blue-100'; + + return ( +

    + +
    + {issue.code} + + {issue.message} + + {issue.location && ( + + {issue.location} + + )} + {issue.line && ( + + Line {issue.line} + {issue.column ? `, col ${issue.column}` : ''} + + )} +
    +
    + ); +} + +function LayerSection({ layer }: { layer: LayerResult }) { + const [open, setOpen] = useState(false); + + const errorCount = layer.issues.filter((i) => i.severity === 'error').length; + const warningCount = layer.issues.filter((i) => i.severity === 'warning').length; + const infoCount = layer.issues.filter((i) => i.severity === 'info').length; + const notLoaded = !layer.loaded; + const allClear = layer.loaded && layer.issues.length === 0; + + return ( +
    + + + {open && ( +
    + {notLoaded ? ( +

    + No shapes are loaded for this layer, so nothing was validated against it. +

    + ) : layer.issues.length === 0 ? ( +

    No issues found.

    + ) : ( + layer.issues.map((issue, idx) => ( + + )) + )} +
    + )} +
    + ); +} + +// ── Entry card ──────────────────────────────────────────────────────────────── + +interface EntryCardProps { + entry: ShaclEntry; + onRemove: (id: string) => void; + onValidate: (id: string) => void; +} + +function EntryCard({ entry, onRemove, onValidate }: EntryCardProps) { + const { id, name, size, isValidating, result, error } = entry; + + return ( +
    + {/* Card header */} +
    + +
    +

    + {name} +

    +

    {(size / 1024).toFixed(1)} KB

    +
    + + +
    + + {/* Card body */} +
    + {/* Error */} + {error && ( +
    + + {error} +
    + )} + + {/* Not yet validated */} + {!result && !error && !isValidating && ( +

    Press Validate to run checks.

    + )} + + {/* Validating spinner */} + {isValidating && ( +
    +
    + Running validation… +
    + )} + + {/* Result */} + {result && !isValidating && ( + <> + {/* Summary */} +
    + {result.valid ? ( + + ) : ( + + )} + + {result.valid ? 'Valid' : 'Invalid'} + +
    + {result.summary.errors > 0 && ( + + {result.summary.errors}E + + )} + {result.summary.warnings > 0 && ( + + {result.summary.warnings}W + + )} + {result.summary.infos > 0 && ( + + {result.summary.infos}I + + )} + {result.valid && result.summary.warnings === 0 && ( + All checks passed + )} +
    +
    + + {result.parseError && ( +
    + + {result.parseError} +
    + )} + + {/* Layers */} +
    + {Object.values(result.layers).map((layer) => ( + + ))} +
    + + )} +
    +
    + ); +} + +// ── Main component ───────────────────────────────────────────────────────────── + +interface ShaclValidatorProps { + apiBaseUrl: string; +} + +const ShaclValidator: React.FC = ({ apiBaseUrl }) => { + const [entries, setEntries] = useState([]); + const [isDragging, setIsDragging] = useState(false); + const [dropError, setDropError] = useState(null); + const [mode, setMode] = useState('file'); + const [endpoint, setEndpoint] = useState(''); + const fileInputRef = useRef(null); + + // ── File handling ─────────────────────────────────────────────────────────── + + const addFiles = (files: FileList | File[]) => { + const arr = Array.from(files); + const rejected = arr.filter((f) => !f.name.endsWith('.ttl')); + + if (rejected.length > 0) { + setDropError(`Skipped ${rejected.length} file(s) — only .ttl is accepted.`); + setTimeout(() => setDropError(null), 4000); + } + + arr + .filter((f) => f.name.endsWith('.ttl')) + .forEach((file) => { + const reader = new FileReader(); + reader.onload = (e) => { + setEntries((prev) => [ + ...prev, + { + id: `${Date.now()}-${Math.random().toString(36).slice(2)}`, + name: file.name, + size: file.size, + content: e.target?.result as string, + isValidating: false, + result: null, + error: null, + }, + ]); + }; + reader.readAsText(file); + }); + }; + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + addFiles(e.dataTransfer.files); + }; + + const handleRemove = (id: string) => setEntries((prev) => prev.filter((e) => e.id !== id)); + + // ── Validation ────────────────────────────────────────────────────────────── + + const validateEntry = async (id: string) => { + const entry = entries.find((e) => e.id === id); + if (!entry) return; + + setEntries((prev) => + prev.map((e) => (e.id === id ? { ...e, isValidating: true, result: null, error: null } : e)) + ); + + const url = + mode === 'merged' + ? `${apiBaseUrl}/v1/shacl/validate-merged` + : `${apiBaseUrl}/v1/shacl/validate`; + const trimmedEndpoint = endpoint.trim(); + const body = + mode === 'merged' + ? JSON.stringify({ + content: entry.content, + ...(trimmedEndpoint ? { endpoint: trimmedEndpoint } : {}), + }) + : JSON.stringify({ content: entry.content }); + + try { + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body, + }); + const data = (await response.json()) as { + success: boolean; + data?: ValidationResult; + error?: { message: string }; + }; + if (!response.ok || !data.success) { + throw new Error(data.error?.message ?? `Server error: ${response.status}`); + } + setEntries((prev) => + prev.map((e) => + e.id === id ? { ...e, isValidating: false, result: data.data!, error: null } : e + ) + ); + } catch (err) { + setEntries((prev) => + prev.map((e) => + e.id === id + ? { + ...e, + isValidating: false, + error: err instanceof Error ? err.message : 'Validation request failed.', + } + : e + ) + ); + } + }; + + const validateAll = () => + entries.forEach((e) => { + if (!e.isValidating) validateEntry(e.id); + }); + + // ── Render ────────────────────────────────────────────────────────────────── + + const hasEntries = entries.length > 0; + const allDone = hasEntries && entries.every((e) => e.result !== null || e.error !== null); + + return ( +
    + {/* Header */} +
    +
    + +
    +

    SHACL Validator

    +

    + Validate CPSV-AP Turtle against the CPSV-AP and RONL SHACL shapes before publishing. +

    +
    +
    + {hasEntries && ( +
    + {!allDone && ( + + )} + +
    + )} +
    + + {/* Body */} +
    + {/* Mode selector */} +
    +
    + + +
    + + {mode === 'merged' && ( +
    + + setEndpoint(e.target.value)} + placeholder="SPARQL endpoint (blank = server default)" + className="flex-1 min-w-0 px-3 py-1.5 text-xs border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-200 focus:border-blue-400" + /> +
    + )} +
    + + {mode === 'merged' && ( +

    + Merge-simulated validation unions each file with the already-published triples for its + subjects, then validates the result. Outcomes depend on current published data. +

    + )} + + {/* Drop zone — compact when files are loaded, full-height when empty */} +
    { + e.preventDefault(); + setIsDragging(true); + }} + onDragLeave={() => setIsDragging(false)} + onDrop={handleDrop} + onClick={() => fileInputRef.current?.click()} + className={`flex-shrink-0 border-2 border-dashed rounded-xl transition-colors cursor-pointer ${ + hasEntries ? 'py-3' : 'flex-1 flex items-center justify-center' + } ${ + isDragging + ? 'border-blue-400 bg-blue-50' + : 'border-slate-300 bg-white hover:border-blue-400 hover:bg-blue-50' + }`} + > +
    + + {hasEntries ? ( + + Drop more files or click to browse — .ttl + + ) : ( + <> +

    Drop Turtle files here

    +

    + or click to browse — .ttl, multiple files supported +

    + + )} +
    + { + if (e.target.files) addFiles(e.target.files); + e.target.value = ''; + }} + className="hidden" + /> +
    + + {/* Drop error toast */} + {dropError && ( +
    + + {dropError} +
    + )} + + {/* Entry cards — scrollable horizontal row */} + {hasEntries && ( +
    +
    1 ? `${entries.length * 340}px` : undefined }} + > + {entries.map((entry) => ( + + ))} +
    +
    + )} + + {/* Legend */} + {hasEntries && ( +

    + E = error · W = warning · I = informational. Click a layer header to expand its issues. +

    + )} +
    +
    + ); +}; + +export default ShaclValidator; diff --git a/packages/frontend/src/services/dsoService.ts b/packages/frontend/src/services/dsoService.ts index ff2b114..4d33dbc 100644 --- a/packages/frontend/src/services/dsoService.ts +++ b/packages/frontend/src/services/dsoService.ts @@ -25,9 +25,17 @@ export interface DsoActiviteit { } export interface DsoRegelbeheerobject { - urn: string; + urn?: string; omschrijving?: string; - typering: 'conclusie' | 'indieningsvereisten' | 'maatregelen'; + // RTR API returns capitalized ("Conclusie"); accept both forms. + typering: + | 'conclusie' + | 'indieningsvereisten' + | 'maatregelen' + | 'Conclusie' + | 'Indieningsvereisten' + | 'Maatregelen'; + functioneleStructuurRef?: string; } export interface DsoActiviteitDetail extends DsoActiviteit { @@ -264,3 +272,63 @@ export async function getActiviteiten( const raw = await get>(`/v1/dso/activiteiten?${params}`, env); return parseActiviteitenResult(raw); } + +// --------------------------------------------------------------------------- +// Uitvoeren Gegevens API — Toepasbare Regels +// --------------------------------------------------------------------------- + +export interface DsoToepasbareRegel { + identifier: number; + functioneleStructuurRef?: string; + begindatum?: string; + sttrVersie?: number; + oin?: string; + _links?: Record; +} + +export interface ToepasbareRegelsResult { + items: DsoToepasbareRegel[]; +} + +function parseToepasbareRegelsResult(raw: Record): ToepasbareRegelsResult { + // HAL envelope: _embedded.toepasbareRegelsList or similar; fall back to array at root + const embedded = (raw as { _embedded?: Record })._embedded; + const list: unknown = + embedded?.toepasbareRegelsList ?? embedded?.toepasbareRegels ?? (Array.isArray(raw) ? raw : []); + return { items: Array.isArray(list) ? (list as DsoToepasbareRegel[]) : [] }; +} + +export async function fetchToepasbareRegels( + functioneleStructuurRef: string, + env: DsoEnv = 'pre' +): Promise { + const params = new URLSearchParams({ functioneleStructuurRef }); + const raw = await get>(`/v1/dso/toepasbare-regels?${params}`, env); + return parseToepasbareRegelsResult(raw); +} + +/** Returns a URL to download the raw STTR XML for a given toepasbare-regel identifier. */ +export function sttrDownloadUrl(identifier: number, env: DsoEnv): string { + return `${API_BASE}/v1/dso/toepasbare-regels/${identifier}/sttr${env === 'prod' ? '?env=prod' : ''}`; +} + +/** Returns a URL to download the extracted DMN for a given toepasbare-regel identifier. */ +export function dmnDownloadUrl(identifier: number, env: DsoEnv): string { + return `${API_BASE}/v1/dso/toepasbare-regels/${identifier}/dmn${env === 'prod' ? '?env=prod' : ''}`; +} + +export interface FormScaffold { + schemaVersion: number; + id: string; + components: unknown[]; + type: string; +} + +export async function fetchFormScaffold( + identifier: number, + formId: string, + env: DsoEnv = 'pre' +): Promise { + const params = new URLSearchParams({ formId }); + return get(`/v1/dso/toepasbare-regels/${identifier}/form-scaffold?${params}`, env); +} diff --git a/packages/frontend/src/types/index.ts b/packages/frontend/src/types/index.ts index ffa4295..709b795 100644 --- a/packages/frontend/src/types/index.ts +++ b/packages/frontend/src/types/index.ts @@ -55,6 +55,7 @@ export enum ViewMode { TUTORIAL = 'TUTORIAL', BPMN = 'BPMN', VALIDATE = 'VALIDATE', + SHACL = 'SHACL', FORM = 'FORM', DOCUMENT = 'DOCUMENT', ROPA = 'ROPA', @@ -265,7 +266,7 @@ export interface FormSchema { createdAt: string; updatedAt: string; readonly?: boolean; - status?: 'example' | 'wip'; + status?: 'example' | 'wip' | 'dso'; language?: 'en' | 'nl' | 'de'; organization?: string; } diff --git a/packages/frontend/src/utils/constants.ts b/packages/frontend/src/utils/constants.ts index 5bc3782..1f49b5c 100644 --- a/packages/frontend/src/utils/constants.ts +++ b/packages/frontend/src/utils/constants.ts @@ -28,6 +28,7 @@ PREFIX m8g: PREFIX eli: PREFIX ronl: PREFIX cprmv: +PREFIX cprmv041: PREFIX schema: `; @@ -201,7 +202,7 @@ WHERE { ?variable cpsv:produces ?dmn . } - ?dmn cprmv:implements ?service . + { ?dmn cprmv:implements ?service } UNION { ?dmn cprmv041:implements ?service } OPTIONAL { ?service dct:title ?serviceTitle . FILTER(LANG(?serviceTitle) = "nl" || LANG(?serviceTitle) = "") } } ORDER BY ?service ?subject`, @@ -232,13 +233,14 @@ SELECT DISTINCT ?dmn ?identifier ?title ?apiEndpoint ?deploymentId ?service ?inputUri ?inputId ?inputType ?inputTitle ?outputUri ?outputId ?outputType ?outputTitle WHERE { - # Core DMN properties - ?dmn a cprmv:DecisionModel ; - dct:identifier ?identifier ; - dct:title ?title ; - cprmv:implementedBy ?apiEndpoint . - + # Core DMN properties (DecisionModel type + implementedBy: CPRMV 0.3.0 or 0.4.1) + { ?dmn a cprmv:DecisionModel } UNION { ?dmn a cprmv041:DecisionModel } + ?dmn dct:identifier ?identifier ; + dct:title ?title . + { ?dmn cprmv:implementedBy ?apiEndpoint } UNION { ?dmn cprmv041:implementedBy ?apiEndpoint } + OPTIONAL { ?dmn cprmv:deploymentId ?deploymentId } + OPTIONAL { ?dmn cprmv041:deploymentId ?deploymentId } OPTIONAL { ?dmn cpsv:implements ?service } # Get all inputs @@ -269,10 +271,10 @@ ORDER BY ?title ?inputId ?outputId`, sparql: `${COMMON_PREFIXES} SELECT ?dmn ?dmnTitle ?service ?serviceTitle ?organization ?orgName ?logo WHERE { - # Start with DMN - ?dmn a cprmv:DecisionModel ; - dct:title ?dmnTitle ; - cprmv:implements ?service . + # Start with DMN (DecisionModel type + implements: CPRMV 0.3.0 or 0.4.1) + { ?dmn a cprmv:DecisionModel } UNION { ?dmn a cprmv041:DecisionModel } + ?dmn dct:title ?dmnTitle . + { ?dmn cprmv:implements ?service } UNION { ?dmn cprmv041:implements ?service } # Service details ?service a cpsv:PublicService ; @@ -298,9 +300,9 @@ ORDER BY ?dmnTtitle`, sparql: `${COMMON_PREFIXES} SELECT ?dmn ?dmnTitle ?inputUri ?inputId ?inputType ?outputUri ?outputId ?outputType ?outputValue WHERE { - ?dmn a cprmv:DecisionModel ; - dct:title ?dmnTitle . - + { ?dmn a cprmv:DecisionModel } UNION { ?dmn a cprmv041:DecisionModel } + ?dmn dct:title ?dmnTitle . + OPTIONAL { ?inputUri a cpsv:Input ; cpsv:isRequiredBy ?dmn ; @@ -338,13 +340,13 @@ WHERE { dct:identifier ?variableId ; dct:type ?variableType . - # Get DMN titles - ?dmn1 a cprmv:DecisionModel ; - dct:title ?dmn1Title . - - ?dmn2 a cprmv:DecisionModel ; - dct:title ?dmn2Title . - + # Get DMN titles (DecisionModel type: CPRMV 0.3.0 or 0.4.1) + { ?dmn1 a cprmv:DecisionModel } UNION { ?dmn1 a cprmv041:DecisionModel } + ?dmn1 dct:title ?dmn1Title . + + { ?dmn2 a cprmv:DecisionModel } UNION { ?dmn2 a cprmv041:DecisionModel } + ?dmn2 dct:title ?dmn2Title . + # Ensure different DMNs FILTER(?dmn1 != ?dmn2) FILTER(LANG(?dmn1Title) = "nl" || LANG(?dmn1Title) = "") @@ -385,11 +387,11 @@ WHERE { BIND(2 AS ?pathLength) } - ?startDmn a cprmv:DecisionModel ; - dct:title ?startTitle . - ?endDmn a cprmv:DecisionModel ; - dct:title ?endTitle . - + { ?startDmn a cprmv:DecisionModel } UNION { ?startDmn a cprmv041:DecisionModel } + ?startDmn dct:title ?startTitle . + { ?endDmn a cprmv:DecisionModel } UNION { ?endDmn a cprmv041:DecisionModel } + ?endDmn dct:title ?endTitle . + FILTER(?startDmn != ?endDmn) FILTER(LANG(?startTitle) = "nl" || LANG(?startTitle) = "") FILTER(LANG(?endTitle) = "nl" || LANG(?endTitle) = "") @@ -404,14 +406,16 @@ SELECT ?dmn ?identifier ?title ?apiEndpoint ?deploymentId ?service ?serviceTitle (GROUP_CONCAT(DISTINCT ?inputId; separator=", ") as ?inputs) (GROUP_CONCAT(DISTINCT ?outputId; separator=", ") as ?outputs) WHERE { - ?dmn a cprmv:DecisionModel ; - dct:identifier ?identifier ; - dct:title ?title ; - cprmv:implementedBy ?apiEndpoint . - + # DecisionModel type + implementedBy: CPRMV 0.3.0 or 0.4.1 + { ?dmn a cprmv:DecisionModel } UNION { ?dmn a cprmv041:DecisionModel } + ?dmn dct:identifier ?identifier ; + dct:title ?title . + { ?dmn cprmv:implementedBy ?apiEndpoint } UNION { ?dmn cprmv041:implementedBy ?apiEndpoint } + OPTIONAL { ?dmn cprmv:deploymentId ?deploymentId } - - OPTIONAL { + OPTIONAL { ?dmn cprmv041:deploymentId ?deploymentId } + + OPTIONAL { ?dmn cpsv:implements ?service . ?service dct:title ?serviceTitle . FILTER(LANG(?serviceTitle) = "nl" || LANG(?serviceTitle) = "") diff --git a/packages/frontend/src/utils/exampleVersions.ts b/packages/frontend/src/utils/exampleVersions.ts index 6165e1b..4b350aa 100644 --- a/packages/frontend/src/utils/exampleVersions.ts +++ b/packages/frontend/src/utils/exampleVersions.ts @@ -18,7 +18,7 @@ export const EXAMPLE_VERSIONS: Record = { // BPMN processes example_awb_process: 4, // v4: organization=flevoland tagging - example_tree_felling: 5, // v5: organization=flevoland tagging + example_tree_felling: 6, // v6: ronl:dsoActiviteitUrn=nl.imow-gm0995.activiteit.HoutopstandVellen example_awb_zorgtoeslag: 3, // v3: organization=toeslagen tagging example_zorgtoeslag_provisional: 4, // v4: organization=toeslagen tagging example_zorgtoeslag_final: 4, // v4: organization=toeslagen tagging diff --git a/packages/frontend/src/vite-env.d.ts b/packages/frontend/src/vite-env.d.ts index 7be53c6..8ee3f03 100644 --- a/packages/frontend/src/vite-env.d.ts +++ b/packages/frontend/src/vite-env.d.ts @@ -2,6 +2,8 @@ interface ImportMetaEnv { readonly VITE_API_BASE_URL: string; + // Base URL of the CPSV Editor, used for the DSO → DMN publish handoff deep-link. + readonly VITE_CPSV_EDITOR_URL: string; // Add more env variables here as needed } diff --git a/test.dmn b/test.dmn new file mode 100644 index 0000000..69f8055 --- /dev/null +++ b/test.dmn @@ -0,0 +1,188 @@ + + + + + + + + + + + productType + + + + + + decisionType + + + + + + + + + + + + "TreeFellingPermit" + "Granted" + 10 + "V" + true + "VNG Selection List 2020 - 7.1.3 Environmental permits - V 10 years after expiry date" + "Tree felling permit granted - destroy after 10 years" + + + + + "TreeFellingPermit" + "Rejected" + 10 + "V" + true + "VNG Selection List 2020 - 7.1.3 Environmental permits - V 10 years after decision date" + "Tree felling permit rejected - destroy after 10 years" + + + + + "TreeFellingPermit" + "Refused" + 5 + "V" + true + "VNG Selection List 2020 - 7.1.3 - V 5 years (not processed)" + "Tree felling permit not processed - destroy after 5 years" + + + + + "Zorgtoeslag" + "Granted" + 7 + "V" + true + "Selectielijst Belastingdienst 2012 - Toeslagen - V 7 jaar na definitieve vaststelling" + "Zorgtoeslag granted - destroy after 7 years from final settlement" + + + + + "Zorgtoeslag" + "Rejected" + 7 + "V" + true + "Selectielijst Belastingdienst 2012 - Toeslagen - V 7 jaar na besluitdatum" + "Zorgtoeslag rejected - destroy after 7 years from decision date" + + + + + "Zorgtoeslag" + "Refused" + 5 + "V" + true + "Selectielijst Belastingdienst 2012 - Toeslagen - V 5 jaar (niet in behandeling genomen)" + "Zorgtoeslag not processed - destroy after 5 years" + + + + + "BuildingPermit" + "Granted" + 10 + "V" + true + "VNG Selection List 2020 - 8.1.1 Environmental permit granted - V 10 years" + "Building permit granted - destroy after 10 years" + + + + + "BuildingPermit" + "Rejected" + 10 + "V" + true + "VNG Selection List 2020 - 8.1.1 Environmental permit rejected - V 10 years" + "Building permit rejected - destroy after 10 years" + + + + + - + - + 10 + "V" + true + "VNG Selection List 2020 - default rule V 10 years" + "Default retention period - destroy after 10 years" + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file