From dc3d8b6774fb700d98dffcd8e3f14f1b7e58e377 Mon Sep 17 00:00:00 2001 From: Steven Gort Date: Tue, 19 May 2026 15:25:27 +0200 Subject: [PATCH 01/41] chore: release v1.8.1 --- .../src/services/dmn-validation.service.ts | 170 +++++++++++++++--- packages/frontend/src/changelog.json | 22 +++ 2 files changed, 168 insertions(+), 24 deletions(-) diff --git a/packages/backend/src/services/dmn-validation.service.ts b/packages/backend/src/services/dmn-validation.service.ts index 2ed17e8..81465e7 100644 --- a/packages/backend/src/services/dmn-validation.service.ts +++ b/packages/backend/src/services/dmn-validation.service.ts @@ -198,8 +198,88 @@ 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 @@ -271,7 +351,7 @@ async function validateBaseLayer( 'error', 'BASE-NS', `Unrecognised DMN namespace: "${rootNs}". ` + - `Expected one of: ${KNOWN_DMN_NS.join(', ')}.` + `Expected one of: ${KNOWN_DMN_NS.join(', ')}.` ) ); } @@ -460,8 +540,8 @@ function validateBusinessLayer(doc: XmlElement): LayerResult { 'error', 'BIZ-008', `Duplicate rule rows in ${hp} table: rule "${ruleId}" has identical input entries ` + - `to rule "${firstRuleId}". Both will fire for the same input, ` + - `causing a DmnHitPolicyException at runtime.`, + `to rule "${firstRuleId}". Both will fire for the same input, ` + + `causing a DmnHitPolicyException at runtime.`, decisionLoc ) ); @@ -488,9 +568,9 @@ function validateBusinessLayer(doc: XmlElement): LayerResult { 'warning', 'BIZ-009', `Catch-all rule "${ruleId}" (all input entries are empty or "-") exists alongside ` + - `specific rules in a ${hp} table. For any input that matches a specific rule, ` + - `both the specific rule and the catch-all fire — violating the ${hp} hit policy. ` + - `Consider hitPolicy="FIRST", or move default logic to an else-branch.`, + `specific rules in a ${hp} table. For any input that matches a specific rule, ` + + `both the specific rule and the catch-all fire — violating the ${hp} hit policy. ` + + `Consider hitPolicy="FIRST", or move default logic to an else-branch.`, decisionLoc ) ); @@ -767,40 +847,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/frontend/src/changelog.json b/packages/frontend/src/changelog.json index a180a8c..f0ad336 100644 --- a/packages/frontend/src/changelog.json +++ b/packages/frontend/src/changelog.json @@ -1,5 +1,27 @@ { "versions": [ + { + "version": "1.8.1", + "date": "May 19, 2026", + "status": "Released", + "statusColor": "green", + "borderColor": "green", + "sections": [ + { + "icon": "🩹", + "iconColor": "green", + "title": "DMN validator: INT-007 false positives eliminated", + "items": [ + "INT-007 (Interaction Rules layer) no longer flags valid intra-DRD references. An may legitimately reference a value produced by another decision wired in via — that name is the producing decision's or /, never an . The rule now resolves requiredDecision targets and treats their output variables as satisfied, mirroring the requiredInput → inputData resolution INT-001 already performs.", + "INT-007 now parses FEEL expressions instead of whole-text matching. Previously the entire was treated as one variable name, so date and time(aanvraagDatum) demanded an and any operator expression false-fired. A new shared extractFeelIdentifiers() helper strips string literals, unwraps built-in calls (date(...), date and time(...), number(...), string(...), not(...), …), drops FEEL keywords/operators and qualified-name/property segments after a dot, and extracts each referenced identifier — every identifier is checked individually.", + "Decision outputs are excluded from the input-contract check entirely. Output variable names (decision and decision-table ) are, by construction, never external inputs and are no longer subject to the must-have-matching-inputData requirement.", + "Genuine gaps still flagged: an inputExpression identifier that resolves to neither a declared nor a requiredDecision-produced output continues to raise INT-007, now naming the specific identifier rather than the raw expression string.", + "Verified against real DMNs that deploy and evaluate correctly on Operaton: RONL_Heusden_Heusdenpas.dmn (aanmerkingHeusdenPas consumed purely via requiredDecision), RONL_SVB_Leeftijden.dmn (date(dagVanAanvraag).year property access), EmployeeRoleAssignment.dmn, tree-felling-decision.dmn and replacement-tree-decision.dmn all validate clean; backend tsc --noEmit passes.", + "No API or response-shape change. POST /v1/dmns/validate is unchanged; only the interaction-layer issue set for affected files differs (fewer false-positive warnings)." + ] + } + ] + }, { "version": "1.8.0", "date": "May 15, 2026", From f7326fd631c055d62f682166e19435de25cf6dae Mon Sep 17 00:00:00 2001 From: Steven Gort Date: Wed, 20 May 2026 15:30:12 +0200 Subject: [PATCH 02/41] chore: release v1.8.2 --- packages/backend/src/routes/dmn.routes.ts | 76 ++++++++- packages/backend/src/types/dmn.types.ts | 1 + packages/frontend/src/changelog.json | 19 +++ test.dmn | 188 ++++++++++++++++++++++ 4 files changed, 281 insertions(+), 3 deletions(-) create mode 100644 test.dmn diff --git a/packages/backend/src/routes/dmn.routes.ts b/packages/backend/src/routes/dmn.routes.ts index 67fd70a..e4cab4f 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(), @@ -282,6 +292,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 +375,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/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/frontend/src/changelog.json b/packages/frontend/src/changelog.json index f0ad336..7e7f525 100644 --- a/packages/frontend/src/changelog.json +++ b/packages/frontend/src/changelog.json @@ -1,5 +1,24 @@ { "versions": [ + { + "version": "1.8.2", + "status": "Latest", + "statusColor": "emerald", + "borderColor": "emerald", + "date": "May 20, 2026", + "sections": [ + { + "icon": "🚀", + "iconColor": "emerald", + "title": "New Features", + "items": [ + "Download the deployed DMN XML directly from Operaton via the new GET /v1/dmns/:identifier/xml endpoint — the file is streamed as .dmn with the correct Content-Type and Content-Disposition headers so browsers save it automatically.", + "DMN list and detail responses now include an xmlUrl field pointing to the new download endpoint, making the link self-discoverable for API clients without prior knowledge of the URL pattern.", + "Backward compatible: the legacy GET /api/dmns/:definitionKey/xml route remains available for existing consumers." + ] + } + ] + }, { "version": "1.8.1", "date": "May 19, 2026", 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 From c714015cd2e21336bd7705e712dc123344df1aad Mon Sep 17 00:00:00 2001 From: Steven Gort Date: Wed, 3 Jun 2026 14:55:57 +0200 Subject: [PATCH 03/41] chore: shacl validator backend [no ci] --- package-lock.json | 232 ++++++++++ packages/backend/package.json | 6 +- packages/backend/scripts/shacl-smoke-rules.ts | 42 ++ packages/backend/scripts/shacl-smoke.ts | 53 +++ .../backend/shapes/ronl/org-uniqueness.ttl | 52 +++ .../backend/shapes/ronl/rule-uniqueness.ttl | 30 ++ packages/backend/src/routes/registry.ts | 10 + packages/backend/src/routes/shacl.routes.ts | 137 ++++++ .../src/services/shacl-validation.service.ts | 399 ++++++++++++++++++ .../backend/src/services/triplydb.service.ts | 53 +++ packages/backend/src/utils/rootViews.ts | 1 + .../fixtures/shacl/rule-collision-fail.ttl | 14 + .../fixtures/shacl/rule-collision-pass.ttl | 14 + 13 files changed, 1042 insertions(+), 1 deletion(-) create mode 100644 packages/backend/scripts/shacl-smoke-rules.ts create mode 100644 packages/backend/scripts/shacl-smoke.ts create mode 100644 packages/backend/shapes/ronl/org-uniqueness.ttl create mode 100644 packages/backend/shapes/ronl/rule-uniqueness.ttl create mode 100644 packages/backend/src/routes/shacl.routes.ts create mode 100644 packages/backend/src/services/shacl-validation.service.ts create mode 100644 packages/backend/tests/fixtures/shacl/rule-collision-fail.ttl create mode 100644 packages/backend/tests/fixtures/shacl/rule-collision-pass.ttl 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/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-rules.ts b/packages/backend/scripts/shacl-smoke-rules.ts new file mode 100644 index 0000000..bc61290 --- /dev/null +++ b/packages/backend/scripts/shacl-smoke-rules.ts @@ -0,0 +1,42 @@ +// packages/backend/scripts/shacl-smoke-rules.ts +// +// Smoke test for the rule URI-collision shape (ronl:RuleUniquenessShape). Loads two +// fixtures from tests/fixtures/shacl/ and asserts the expected outcomes: +// - rule-collision-fail.ttl : three cpsv:Rule blocks under ONE subject URI -> +// 2 errors (one each on dct:title and dct:description), +// each naming the three collided values. +// - rule-collision-pass.ttl : the same three rules under unique URIs -> valid. +// +// 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'); + + 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) ###'); + const rp = await shaclValidationService.validateFile(pass); + console.log('valid:', rp.valid, '| summary:', JSON.stringify(rp.summary)); + + const ok = !rf.valid && rf.summary.errors === 2 && rp.valid && rp.summary.errors === 0; + console.log(`\n${ok ? 'PASS' : 'FAIL'}: expected fail=2 errors, pass=0 errors.`); + process.exit(ok ? 0 : 1); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); \ No newline at end of file diff --git a/packages/backend/scripts/shacl-smoke.ts b/packages/backend/scripts/shacl-smoke.ts new file mode 100644 index 0000000..7195664 --- /dev/null +++ b/packages/backend/scripts/shacl-smoke.ts @@ -0,0 +1,53 @@ +// 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. +// +// Expected: valid=false, one error on focus node Provincie_Flevoland, path +// foaf:homepage, in the ronl-custom layer (the CPSV-AP layers stay empty until the +// SEMIC shapes are vendored). +// +// 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 ok = !result.valid && result.summary.errors === 1; + console.log(`\n${ok ? 'PASS' : 'FAIL'}: expected exactly one error.`); + process.exit(ok ? 0 : 1); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); \ No newline at end of file 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/routes/registry.ts b/packages/backend/src/routes/registry.ts index d530d4f..dc5c7b7 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', diff --git a/packages/backend/src/routes/shacl.routes.ts b/packages/backend/src/routes/shacl.routes.ts new file mode 100644 index 0000000..46be457 --- /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; \ No newline at end of file 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..7bfed45 --- /dev/null +++ b/packages/backend/src/services/shacl-validation.service.ts @@ -0,0 +1,399 @@ +/// +// 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-core' | 'cpsv-ap-vocab' | 'ronl-custom'; + +export interface ShaclIssue { + severity: 'error' | 'warning' | 'info'; + code: string; + message: string; + location?: string; +} + +export interface ShaclLayerResult { + label: string; + 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-core', + label: 'CPSV-AP Core', + files: ['cpsv-ap/3.2.0/CPSV-AP_shacl_shapes.ttl'], + }, + { + key: 'cpsv-ap-vocab', + label: 'CPSV-AP Vocabularies', + files: ['cpsv-ap/3.2.0/CPSV-AP_shacl_cv_shapes.ttl'], + }, + { + key: 'ronl-custom', + label: 'RONL Custom', + dir: 'ronl', + }, +]; + +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 ───────────────────────────────────────────────────────────────────── + +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; + + 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 { + 'cpsv-ap-core': { label: 'CPSV-AP Core', issues: [] }, + 'cpsv-ap-vocab': { label: 'CPSV-AP Vocabularies', issues: [] }, + 'ronl-custom': { label: 'RONL Custom', 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 and cap length — published literals (e.g. long + // multi-line rule descriptions) would otherwise bloat the issue message. + const normalised = quad.object.value.replace(/\s+/g, ' ').trim(); + values.push(normalised.length > 60 ? `${normalised.slice(0, 60)}…` : normalised); + } + 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) { + 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, subjects: 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 constructGraph(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; \ No newline at end of file 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/utils/rootViews.ts b/packages/backend/src/utils/rootViews.ts index 57e0244..422f5e9 100644 --- a/packages/backend/src/utils/rootViews.ts +++ b/packages/backend/src/utils/rootViews.ts @@ -14,6 +14,7 @@ import packageJson from '../../package.json'; const CATEGORY_ORDER: ReadonlyArray = [ 'Health & monitoring', 'Discovery', + 'Validation', 'Execution', 'Assets', 'Integrations', 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 . From 94be893fe2cbf44c4135427f8b7d113d5496d7c2 Mon Sep 17 00:00:00 2001 From: Steven Gort Date: Wed, 3 Jun 2026 14:57:20 +0200 Subject: [PATCH 04/41] chore: shacl validator backend - formatted [no ci] --- packages/backend/src/index.ts | 2 +- packages/backend/src/routes/index.ts | 3 +- packages/backend/src/routes/registry.ts | 2 +- packages/backend/src/routes/shacl.routes.ts | 136 ++--- .../src/services/dmn-validation.service.ts | 22 +- .../src/services/shacl-validation.service.ts | 566 +++++++++--------- packages/backend/src/utils/config.ts | 6 +- packages/backend/src/utils/rootViews.ts | 202 ++++--- 8 files changed, 469 insertions(+), 470 deletions(-) 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/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 dc5c7b7..9868fab 100644 --- a/packages/backend/src/routes/registry.ts +++ b/packages/backend/src/routes/registry.ts @@ -165,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 index 46be457..cac1408 100644 --- a/packages/backend/src/routes/shacl.routes.ts +++ b/packages/backend/src/routes/shacl.routes.ts @@ -36,43 +36,43 @@ const router = Router(); * 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 }; + 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); - } + 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 }); + logger.info('[SHACL Validate] Validation requested', { contentLength: content.length }); - const result = await shaclValidationService.validateFile(content); + const result = await shaclValidationService.validateFile(content); - logger.info('[SHACL Validate] Complete', { - valid: result.valid, - errors: result.summary.errors, - warnings: result.summary.warnings, - }); + 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); - } + 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); + } }); /** @@ -92,46 +92,46 @@ router.post('/validate', async (req: Request, res: Response) => { * 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 }; + 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); - } + 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)', - }); + logger.info('[SHACL Validate] Merged validation requested', { + contentLength: content.length, + endpoint: endpoint ?? '(default)', + }); - const result = await shaclValidationService.validateMerged(content, endpoint); + const result = await shaclValidationService.validateMerged(content, endpoint); - logger.info('[SHACL Validate] Merged complete', { - valid: result.valid, - errors: result.summary.errors, - warnings: result.summary.warnings, - }); + 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); - } + 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; \ No newline at end of file +export default router; diff --git a/packages/backend/src/services/dmn-validation.service.ts b/packages/backend/src/services/dmn-validation.service.ts index 81465e7..c06f1f4 100644 --- a/packages/backend/src/services/dmn-validation.service.ts +++ b/packages/backend/src/services/dmn-validation.service.ts @@ -256,9 +256,7 @@ const FEEL_RESERVED = new Set([ * Returns a de-duplicated list in first-seen order. */ function extractFeelIdentifiers(text: string): string[] { - const stripped = text - .replace(/'(?:[^'\\]|\\.)*'/g, ' ') - .replace(/"(?:[^"\\]|\\.)*"/g, ' '); + const stripped = text.replace(/'(?:[^'\\]|\\.)*'/g, ' ').replace(/"(?:[^"\\]|\\.)*"/g, ' '); const out: string[] = []; const seen = new Set(); const tokenRe = /[A-Za-z_][A-Za-z0-9_]*/g; @@ -351,7 +349,7 @@ async function validateBaseLayer( 'error', 'BASE-NS', `Unrecognised DMN namespace: "${rootNs}". ` + - `Expected one of: ${KNOWN_DMN_NS.join(', ')}.` + `Expected one of: ${KNOWN_DMN_NS.join(', ')}.` ) ); } @@ -540,8 +538,8 @@ function validateBusinessLayer(doc: XmlElement): LayerResult { 'error', 'BIZ-008', `Duplicate rule rows in ${hp} table: rule "${ruleId}" has identical input entries ` + - `to rule "${firstRuleId}". Both will fire for the same input, ` + - `causing a DmnHitPolicyException at runtime.`, + `to rule "${firstRuleId}". Both will fire for the same input, ` + + `causing a DmnHitPolicyException at runtime.`, decisionLoc ) ); @@ -568,9 +566,9 @@ function validateBusinessLayer(doc: XmlElement): LayerResult { 'warning', 'BIZ-009', `Catch-all rule "${ruleId}" (all input entries are empty or "-") exists alongside ` + - `specific rules in a ${hp} table. For any input that matches a specific rule, ` + - `both the specific rule and the catch-all fire — violating the ${hp} hit policy. ` + - `Consider hitPolicy="FIRST", or move default logic to an else-branch.`, + `specific rules in a ${hp} table. For any input that matches a specific rule, ` + + `both the specific rule and the catch-all fire — violating the ${hp} hit policy. ` + + `Consider hitPolicy="FIRST", or move default logic to an else-branch.`, decisionLoc ) ); @@ -920,9 +918,9 @@ function validateInteractionLayer(doc: XmlElement): LayerResult { 'warning', 'INT-007', ` 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.`, + ` 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/shacl-validation.service.ts b/packages/backend/src/services/shacl-validation.service.ts index 7bfed45..ad14571 100644 --- a/packages/backend/src/services/shacl-validation.service.ts +++ b/packages/backend/src/services/shacl-validation.service.ts @@ -40,22 +40,22 @@ import logger from '../utils/logger'; export type ShaclLayerKey = 'cpsv-ap-core' | 'cpsv-ap-vocab' | 'ronl-custom'; export interface ShaclIssue { - severity: 'error' | 'warning' | 'info'; - code: string; - message: string; - location?: string; + severity: 'error' | 'warning' | 'info'; + code: string; + message: string; + location?: string; } export interface ShaclLayerResult { - label: string; - issues: ShaclIssue[]; + label: string; + issues: ShaclIssue[]; } export interface ShaclValidationResult { - valid: boolean; - parseError: string | null; - layers: Record; - summary: { errors: number; warnings: number; infos: number }; + valid: boolean; + parseError: string | null; + layers: Record; + summary: { errors: number; warnings: number; infos: number }; } // ── Shape layer configuration ───────────────────────────────────────────────── @@ -68,37 +68,37 @@ export interface ShaclValidationResult { 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; + 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-core', - label: 'CPSV-AP Core', - files: ['cpsv-ap/3.2.0/CPSV-AP_shacl_shapes.ttl'], - }, - { - key: 'cpsv-ap-vocab', - label: 'CPSV-AP Vocabularies', - files: ['cpsv-ap/3.2.0/CPSV-AP_shacl_cv_shapes.ttl'], - }, - { - key: 'ronl-custom', - label: 'RONL Custom', - dir: 'ronl', - }, + { + key: 'cpsv-ap-core', + label: 'CPSV-AP Core', + files: ['cpsv-ap/3.2.0/CPSV-AP_shacl_shapes.ttl'], + }, + { + key: 'cpsv-ap-vocab', + label: 'CPSV-AP Vocabularies', + files: ['cpsv-ap/3.2.0/CPSV-AP_shacl_cv_shapes.ttl'], + }, + { + key: 'ronl-custom', + label: 'RONL Custom', + dir: 'ronl', + }, ]; interface LoadedLayer { - key: ShaclLayerKey; - label: string; - /** null when no shape files are present for this layer (e.g. not vendored yet). */ - validator: SHACLValidator | null; + 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 ───────────────────────────────────────────────────── @@ -106,267 +106,275 @@ interface LoadedLayer { 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'; - } + 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'; + 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], + ['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; + if (!uri) return ''; + for (const [prefix, ns] of PREFIXES) { + if (uri.startsWith(ns)) return `${prefix}:${uri.slice(ns.length)}`; + } + return uri; } // ── Service ───────────────────────────────────────────────────────────────────── 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; - - private parse(ttl: string) { - return new Parser().parse(ttl); - } - - private toDataset(ttl: string) { - return rdfDataset.dataset(this.parse(ttl)); - } + // 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; + + 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, + }); + } + } + } - /** - * 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 }); + 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, + }); + } + } - const loaded = layers.filter((l) => l.validator).map((l) => l.key); - logger.info('[SHACL] Shape layers loaded', { withShapes: loaded }); - return layers; - })(); + let validator: SHACLValidator | null = null; + if (ttls.length > 0) { + const shapes = rdfDataset.dataset(this.parse(ttls.join('\n'))); + validator = new SHACLValidator(shapes); + } - return this.layersPromise; + 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 { + 'cpsv-ap-core': { label: 'CPSV-AP Core', issues: [] }, + 'cpsv-ap-vocab': { label: 'CPSV-AP Vocabularies', issues: [] }, + 'ronl-custom': { label: 'RONL Custom', 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 and cap length — published literals (e.g. long + // multi-line rule descriptions) would otherwise bloat the issue message. + const normalised = quad.object.value.replace(/\s+/g, ' ').trim(); + values.push(normalised.length > 60 ? `${normalised.slice(0, 60)}…` : normalised); } + 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) { + 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, + }); - private emptyLayers(): Record { - return { - 'cpsv-ap-core': { label: 'CPSV-AP Core', issues: [] }, - 'cpsv-ap-vocab': { label: 'CPSV-AP Vocabularies', issues: [] }, - 'ronl-custom': { label: 'RONL Custom', issues: [] }, - }; + if (severity === 'error') summary.errors++; + else if (severity === 'warning') summary.warnings++; + else summary.infos++; + } } - /** - * 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 and cap length — published literals (e.g. long - // multi-line rule descriptions) would otherwise bloat the issue message. - const normalised = quad.object.value.replace(/\s+/g, ' ').trim(); - values.push(normalised.length > 60 ? `${normalised.slice(0, 60)}…` : normalised); - } - return values; + 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 }, + }; } - /** - * 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) { - 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, - }; + logger.info('[SHACL] validateFile', { contentLength: content.length, subjects: 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 }, + }; } - /** - * 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, subjects: data.size }); - return this.runLayers(data, null); + // 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); } - /** - * 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); - } + // 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 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(' '); + 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 { + // 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 . } @@ -376,24 +384,24 @@ WHERE { OPTIONAL { ?o ?p2 ?o2 . FILTER(isBlank(?o)) } }`; - logger.info('[SHACL] validateMerged: fetching published graph', { - endpoint: targetEndpoint, - subjects: subjects.size, - }); + logger.info('[SHACL] validateMerged: fetching published graph', { + endpoint: targetEndpoint, + subjects: subjects.size, + }); - const remoteTtl = await constructGraph(targetEndpoint, query); - const remoteQuads = this.parse(remoteTtl); + const remoteTtl = await constructGraph(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, - }); + 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); - } + return this.runLayers(merged, null); + } } export const shaclValidationService = new ShaclValidationService(); -export default shaclValidationService; \ No newline at end of file +export default shaclValidationService; diff --git a/packages/backend/src/utils/config.ts b/packages/backend/src/utils/config.ts index 61c5b47..70b529c 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(); diff --git a/packages/backend/src/utils/rootViews.ts b/packages/backend/src/utils/rootViews.ts index 422f5e9..a2daf2a 100644 --- a/packages/backend/src/utils/rootViews.ts +++ b/packages/backend/src/utils/rootViews.ts @@ -12,29 +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', - 'Validation', - '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; } /** @@ -42,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 @@ -89,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 ` @@ -255,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); +} From f56def02468675c48c3689a2621635f89142dca3 Mon Sep 17 00:00:00 2001 From: Steven Gort Date: Thu, 4 Jun 2026 09:27:10 +0200 Subject: [PATCH 05/41] chore: add shacl validator frontend --- packages/frontend/src/App.tsx | 28 +- .../src/components/ShaclValidator.tsx | 597 ++++++++++++++++++ packages/frontend/src/types/index.ts | 1 + 3 files changed, 623 insertions(+), 3 deletions(-) create mode 100644 packages/frontend/src/components/ShaclValidator.tsx 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 = () => { + + + + {open && ( +
    + {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/types/index.ts b/packages/frontend/src/types/index.ts index ffa4295..3f9c51a 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', From 809a41425e63ef542c16d783a0f623bbd1cd397b Mon Sep 17 00:00:00 2001 From: Steven Gort Date: Thu, 4 Jun 2026 09:36:18 +0200 Subject: [PATCH 06/41] chore: fix lint issues [no ci] --- packages/backend/eslint.config.js | 2 +- packages/backend/src/services/shacl-validation.service.ts | 1 - packages/backend/tsconfig.eslint.json | 4 ++++ packages/backend/tsconfig.json | 3 +++ 4 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 packages/backend/tsconfig.eslint.json 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/src/services/shacl-validation.service.ts b/packages/backend/src/services/shacl-validation.service.ts index ad14571..cc2f172 100644 --- a/packages/backend/src/services/shacl-validation.service.ts +++ b/packages/backend/src/services/shacl-validation.service.ts @@ -1,4 +1,3 @@ -/// // packages/backend/src/services/shacl-validation.service.ts // // SHACL validation for CPSV-AP 3.2.0 (+ custom RONL) Turtle files. Mirrors the 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", From 5d18a0fa1706a7fec967116d8b07f3fcdbbf5e7d Mon Sep 17 00:00:00 2001 From: Steven Gort Date: Thu, 4 Jun 2026 12:50:29 +0200 Subject: [PATCH 07/41] chore: release 1.9.0 --- packages/frontend/src/changelog.json | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/frontend/src/changelog.json b/packages/frontend/src/changelog.json index 7e7f525..e4605b0 100644 --- a/packages/frontend/src/changelog.json +++ b/packages/frontend/src/changelog.json @@ -1,10 +1,32 @@ { "versions": [ { - "version": "1.8.2", + "version": "1.9.0", "status": "Latest", "statusColor": "emerald", "borderColor": "emerald", + "date": "June 4, 2026", + "sections": [ + { + "icon": "🛡️", + "iconColor": "emerald", + "title": "SHACL Validator", + "items": [ + "New SHACL Validator view in the sidebar: drop one or more CPSV-AP Turtle (.ttl) files and validate them against the bundled CPSV-AP 3.2.0 and RONL SHACL shapes before publishing. Results reuse the DMN Validator's layered layout, grouped into CPSV-AP Core, CPSV-AP Vocabularies, and RONL Custom with per-layer error/warning/info counts.", + "Two backend endpoints under a new Validation route category: POST /v1/shacl/validate (file-local) and POST /v1/shacl/validate-merged. Both return the same { valid, parseError, layers, summary } shape as /v1/dmns/validate, so the frontend renders them with the existing components.", + "Merge-simulated mode unions the uploaded file with the triples already published for its subjects — fetched from the configured SPARQL endpoint via a standard SPARQL 1.1 CONSTRUCT with one level of blank-node closure — then validates the union. This catches fan-out against live data that a file-local check cannot see; an optional endpoint field overrides the server default.", + "RONL shape PublicOrganisationUniquenessShape: at most one foaf:homepage, dct:identifier and cv:spatial per cv:PublicOrganisation, plus sh:uniqueLang on skos:prefLabel. Flags the duplicate-homepage fan-out where one organisation URI ends up with both a www. and non-www. homepage across publications.", + "RONL shape RuleUniquenessShape: sh:uniqueLang on dct:title and dct:description for cpsv:Rule. Detects subject-URI collisions where several distinct rules were authored under one URI and merge into N titles × M descriptions of duplicate rows in SPARQL results, while tolerating legitimate bilingual (nl/en) labels.", + "Cardinality and uniqueLang violations list the offending values inline (whitespace-normalised and capped at 60 characters) so collisions are immediately identifiable." + ] + } + ] + }, + { + "version": "1.8.2", + "status": "Released", + "statusColor": "green", + "borderColor": "green", "date": "May 20, 2026", "sections": [ { From 968b4a593bae38f059ca5b7c04a19c9ee55e4ac4 Mon Sep 17 00:00:00 2001 From: Steven Gort Date: Thu, 4 Jun 2026 15:17:25 +0200 Subject: [PATCH 08/41] chore: fix backend deploy action --- .github/workflows/azure-backend-acc.yml | 7 +++++++ .../src/services/shacl-validation.service.ts | 18 ++++++------------ 2 files changed, 13 insertions(+), 12 deletions(-) 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/packages/backend/src/services/shacl-validation.service.ts b/packages/backend/src/services/shacl-validation.service.ts index cc2f172..1b7bdcc 100644 --- a/packages/backend/src/services/shacl-validation.service.ts +++ b/packages/backend/src/services/shacl-validation.service.ts @@ -1,3 +1,5 @@ +// 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 @@ -266,26 +268,18 @@ class ShaclValidationService { for (const result of report.results) { const severity = severityFromTerm(result.severity); const baseMessage = - result.message.map((m) => m.value).join('; ') || - codeFromComponent(result.sourceConstraintComponent); + 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; + 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, - }); + layers[layer.key].issues.push({ severity, code: codeFromComponent(result.sourceConstraintComponent), message, location }); if (severity === 'error') summary.errors++; else if (severity === 'warning') summary.warnings++; @@ -403,4 +397,4 @@ WHERE { } export const shaclValidationService = new ShaclValidationService(); -export default shaclValidationService; +export default shaclValidationService; \ No newline at end of file From 2100423a7eb07771d4077a605fab14b12bf95ebf Mon Sep 17 00:00:00 2001 From: Steven Gort Date: Thu, 4 Jun 2026 16:06:53 +0200 Subject: [PATCH 09/41] fix(backend): track SHACL ambient declaration shim (was caught by *.d.ts ignore) --- .gitignore | 3 ++ packages/backend/src/types/shacl-rdf.d.ts | 54 +++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 packages/backend/src/types/shacl-rdf.d.ts diff --git a/.gitignore b/.gitignore index 044db93..df74401 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 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; +} From e91efe7b9173863aa8af304e496bdd227e49d252 Mon Sep 17 00:00:00 2001 From: Steven Gort Date: Fri, 5 Jun 2026 13:24:10 +0200 Subject: [PATCH 10/41] chore: release v1.9.1 --- packages/backend/scripts/shacl-smoke-merge.ts | 68 + packages/backend/scripts/shacl-smoke-rules.ts | 64 +- packages/backend/scripts/shacl-smoke.ts | 41 +- .../shapes/cpsv-ap/3.2.0/cpsv-ap-SHACL.ttl | 1264 +++++++++++++++++ .../src/services/shacl-validation.service.ts | 53 +- .../fixtures/shacl/cpsv-ap-conformant.ttl | 14 + packages/frontend/src/changelog.json | 21 + .../src/components/ShaclValidator.tsx | 25 +- 8 files changed, 1479 insertions(+), 71 deletions(-) create mode 100644 packages/backend/scripts/shacl-smoke-merge.ts create mode 100644 packages/backend/shapes/cpsv-ap/3.2.0/cpsv-ap-SHACL.ttl create mode 100644 packages/backend/tests/fixtures/shacl/cpsv-ap-conformant.ttl 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 index bc61290..e4109a5 100644 --- a/packages/backend/scripts/shacl-smoke-rules.ts +++ b/packages/backend/scripts/shacl-smoke-rules.ts @@ -1,11 +1,12 @@ // packages/backend/scripts/shacl-smoke-rules.ts // -// Smoke test for the rule URI-collision shape (ronl:RuleUniquenessShape). Loads two -// fixtures from tests/fixtures/shacl/ and asserts the expected outcomes: +// 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 errors (one each on dct:title and dct:description), -// each naming the three collided values. -// - rule-collision-pass.ttl : the same three rules under unique URIs -> valid. +// 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 @@ -16,27 +17,38 @@ 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'); - - 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) ###'); - const rp = await shaclValidationService.validateFile(pass); - console.log('valid:', rp.valid, '| summary:', JSON.stringify(rp.summary)); - - const ok = !rf.valid && rf.summary.errors === 2 && rp.valid && rp.summary.errors === 0; - console.log(`\n${ok ? 'PASS' : 'FAIL'}: expected fail=2 errors, pass=0 errors.`); - process.exit(ok ? 0 : 1); + 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); -}); \ No newline at end of file + console.error(err); + process.exit(1); +}); diff --git a/packages/backend/scripts/shacl-smoke.ts b/packages/backend/scripts/shacl-smoke.ts index 7195664..1d40c42 100644 --- a/packages/backend/scripts/shacl-smoke.ts +++ b/packages/backend/scripts/shacl-smoke.ts @@ -5,9 +5,9 @@ // carrying two divergent foaf:homepage values — the result of unioning File A and // File B) through validateFile and prints the report. // -// Expected: valid=false, one error on focus node Provincie_Flevoland, path -// foaf:homepage, in the ronl-custom layer (the CPSV-AP layers stay empty until the -// SEMIC shapes are vendored). +// 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 @@ -28,26 +28,27 @@ const MERGED_FIXTURE = ` `; async function main(): Promise { - const result = await shaclValidationService.validateFile(MERGED_FIXTURE); + 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}`); - } + 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 ok = !result.valid && result.summary.errors === 1; - console.log(`\n${ok ? 'PASS' : 'FAIL'}: expected exactly one error.`); - process.exit(ok ? 0 : 1); + 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); -}); \ No newline at end of file + console.error(err); + process.exit(1); +}); 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/src/services/shacl-validation.service.ts b/packages/backend/src/services/shacl-validation.service.ts index 1b7bdcc..6f100dc 100644 --- a/packages/backend/src/services/shacl-validation.service.ts +++ b/packages/backend/src/services/shacl-validation.service.ts @@ -38,7 +38,7 @@ import logger from '../utils/logger'; // ── Response types (shape matches DmnValidator's so the UI components are shared) ── -export type ShaclLayerKey = 'cpsv-ap-core' | 'cpsv-ap-vocab' | 'ronl-custom'; +export type ShaclLayerKey = 'cpsv-ap' | 'ronl-custom'; export interface ShaclIssue { severity: 'error' | 'warning' | 'info'; @@ -49,6 +49,8 @@ export interface ShaclIssue { 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[]; } @@ -79,14 +81,9 @@ interface LayerSpec { const LAYER_SPECS: LayerSpec[] = [ { - key: 'cpsv-ap-core', - label: 'CPSV-AP Core', - files: ['cpsv-ap/3.2.0/CPSV-AP_shacl_shapes.ttl'], - }, - { - key: 'cpsv-ap-vocab', - label: 'CPSV-AP Vocabularies', - files: ['cpsv-ap/3.2.0/CPSV-AP_shacl_cv_shapes.ttl'], + key: 'cpsv-ap', + label: 'CPSV-AP 3.2.0', + files: ['cpsv-ap/3.2.0/cpsv-ap-SHACL.ttl'], }, { key: 'ronl-custom', @@ -145,11 +142,21 @@ function compact(uri: string | undefined): string { // ── Service ───────────────────────────────────────────────────────────────────── -class ShaclValidationService { +/** + * 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); } @@ -220,9 +227,8 @@ class ShaclValidationService { private emptyLayers(): Record { return { - 'cpsv-ap-core': { label: 'CPSV-AP Core', issues: [] }, - 'cpsv-ap-vocab': { label: 'CPSV-AP Vocabularies', issues: [] }, - 'ronl-custom': { label: 'RONL Custom', issues: [] }, + 'cpsv-ap': { label: 'CPSV-AP 3.2.0', loaded: false, issues: [] }, + 'ronl-custom': { label: 'RONL Custom', loaded: false, issues: [] }, }; } @@ -262,24 +268,33 @@ class ShaclValidationService { 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); + 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; + 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 }); + layers[layer.key].issues.push({ + severity, + code: codeFromComponent(result.sourceConstraintComponent), + message, + location, + }); if (severity === 'error') summary.errors++; else if (severity === 'warning') summary.warnings++; @@ -314,7 +329,7 @@ class ShaclValidationService { }; } - logger.info('[SHACL] validateFile', { contentLength: content.length, subjects: data.size }); + logger.info('[SHACL] validateFile', { contentLength: content.length, triples: data.size }); return this.runLayers(data, null); } @@ -382,7 +397,7 @@ WHERE { subjects: subjects.size, }); - const remoteTtl = await constructGraph(targetEndpoint, query); + const remoteTtl = await this.fetchGraph(targetEndpoint, query); const remoteQuads = this.parse(remoteTtl); const merged = rdfDataset.dataset([...localQuads, ...remoteQuads]); @@ -397,4 +412,4 @@ WHERE { } export const shaclValidationService = new ShaclValidationService(); -export default shaclValidationService; \ No newline at end of file +export default shaclValidationService; 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/frontend/src/changelog.json b/packages/frontend/src/changelog.json index e4605b0..491c741 100644 --- a/packages/frontend/src/changelog.json +++ b/packages/frontend/src/changelog.json @@ -1,5 +1,26 @@ { "versions": [ + { + "version": "1.9.1", + "status": "Latest", + "statusColor": "emerald", + "borderColor": "emerald", + "date": "June 5, 2026", + "sections": [ + { + "icon": "🛡️", + "iconColor": "emerald", + "title": "SHACL Validator", + "items": [ + "Vendored the canonical CPSV-AP 3.2.0 SHACL shapes (SEMIC) — the CPSV-AP layer is now active, validating the full model (PublicService, Rule, PublicOrganisation, ContactPoint, Channel, Address, …) across 32 shapes. Previously this layer reported no shapes.", + "Collapsed the earlier CPSV-AP Core and CPSV-AP Vocabularies layers into a single CPSV-AP 3.2.0 layer, matching SEMIC's combined shape file for the 3.2.0 release.", + "Layer results now distinguish 'Not loaded' (no shapes present for that layer) from 'OK' (evaluated and clean), so an unvendored layer no longer shows a misleading green check.", + "Offending values listed in cardinality and uniqueLang messages are now whitespace-normalised and capped at 60 characters, keeping long published descriptions readable.", + "Added a fully CPSV-AP-conformant example plus deterministic merge-simulated test coverage (a cross-publication homepage collision), alongside the existing file-local fixtures." + ] + } + ] + }, { "version": "1.9.0", "status": "Latest", diff --git a/packages/frontend/src/components/ShaclValidator.tsx b/packages/frontend/src/components/ShaclValidator.tsx index 60b6005..cd13fa1 100644 --- a/packages/frontend/src/components/ShaclValidator.tsx +++ b/packages/frontend/src/components/ShaclValidator.tsx @@ -19,6 +19,7 @@ import { CheckCircle, ChevronDown, ChevronUp, + CircleDashed, Database, FileText, Info, @@ -42,6 +43,7 @@ interface ValidationIssue { interface LayerResult { label: string; + loaded: boolean; issues: ValidationIssue[]; } @@ -49,8 +51,7 @@ interface ValidationResult { valid: boolean; parseError: string | null; layers: { - 'cpsv-ap-core': LayerResult; - 'cpsv-ap-vocab': LayerResult; + 'cpsv-ap': LayerResult; 'ronl-custom': LayerResult; }; summary: { errors: number; warnings: number; infos: number }; @@ -112,7 +113,8 @@ function LayerSection({ layer }: { layer: LayerResult }) { 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 allClear = layer.issues.length === 0; + const notLoaded = !layer.loaded; + const allClear = layer.loaded && layer.issues.length === 0; return (
    @@ -122,16 +124,23 @@ function LayerSection({ layer }: { layer: LayerResult }) { className="w-full flex items-center justify-between px-3 py-2 bg-slate-50 hover:bg-slate-100 transition-colors text-left" >
    - {allClear ? ( + {notLoaded ? ( + + ) : allClear ? ( ) : ( 0 ? 'error' : warningCount > 0 ? 'warning' : 'info'} /> )} - {layer.label} + + {layer.label} +
    + {notLoaded && Not loaded} {allClear && OK} {errorCount > 0 && ( @@ -158,7 +167,11 @@ function LayerSection({ layer }: { layer: LayerResult }) { {open && (
    - {layer.issues.length === 0 ? ( + {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) => ( From af558be00901bcec2cb615e7a14d571e3fbb9a48 Mon Sep 17 00:00:00 2001 From: Steven Gort Date: Tue, 9 Jun 2026 12:41:19 +0200 Subject: [PATCH 11/41] chore: synced ttl thuisbatterij examples [no ci] --- .../RechtEnHoogteSubsidieThuisbatterij.dmn | 717 +++ ...pipeline_generator_repaired_embedded_v4.py | 3641 +++++++++++++ ...atterijBijRechtEnBudget-mcdc-analysis.json | 368 ++ ...terijBijRechtEnBudget-mcdc-test-cases.json | 3813 +++++++++++++ ...terijBijRechtEnBudget-mcdc-test-cases.xlsm | Bin 0 -> 100194 bytes ...dget.postman_collection.mcdc_examples.json | 4724 +++++++++++++++++ .../hoogte-subsidie-thuisbatterij.form | 106 + ...oogte-subsidie-thuisbatterij-Flevoland.ttl | 491 ++ .../recht-op-subsidie-thuisbatterij.form | 148 + .../thuisbatterij-mcdc-test-cases.json | 3813 +++++++++++++ ...thuisbatterij-subsidie-flevoland-main.bpmn | 341 ++ ...atterij-subsidie-flevoland-subprocess.bpmn | 314 ++ 12 files changed, 18476 insertions(+) create mode 100644 examples/organizations/flevoland/thuisbatterij/RechtEnHoogteSubsidieThuisbatterij.dmn create mode 100644 examples/organizations/flevoland/thuisbatterij/dmn_mcdc_pipeline_generator_repaired_embedded_v4.py create mode 100644 examples/organizations/flevoland/thuisbatterij/generatedRepaired/hoogteVergoedingThuisbatterijBijRechtEnBudget-mcdc-analysis.json create mode 100644 examples/organizations/flevoland/thuisbatterij/generatedRepaired/hoogteVergoedingThuisbatterijBijRechtEnBudget-mcdc-test-cases.json create mode 100644 examples/organizations/flevoland/thuisbatterij/generatedRepaired/hoogteVergoedingThuisbatterijBijRechtEnBudget-mcdc-test-cases.xlsm create mode 100644 examples/organizations/flevoland/thuisbatterij/generatedRepaired/hoogteVergoedingThuisbatterijBijRechtEnBudget.postman_collection.mcdc_examples.json create mode 100644 examples/organizations/flevoland/thuisbatterij/hoogte-subsidie-thuisbatterij.form create mode 100644 examples/organizations/flevoland/thuisbatterij/recht-en-hoogte-subsidie-thuisbatterij-Flevoland.ttl create mode 100644 examples/organizations/flevoland/thuisbatterij/recht-op-subsidie-thuisbatterij.form create mode 100644 examples/organizations/flevoland/thuisbatterij/thuisbatterij-mcdc-test-cases.json create mode 100644 examples/organizations/flevoland/thuisbatterij/thuisbatterij-subsidie-flevoland-main.bpmn create mode 100644 examples/organizations/flevoland/thuisbatterij/thuisbatterij-subsidie-flevoland-subprocess.bpmn 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 0000000000000000000000000000000000000000..1fb801d6c1fdaebfbf12ca995d363ebef7148832 GIT binary patch literal 100194 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�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 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} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From bcde806a320bfb69236dc3e13e59ed79efcffc41 Mon Sep 17 00:00:00 2001 From: Steven Gort Date: Tue, 9 Jun 2026 12:55:14 +0200 Subject: [PATCH 12/41] chore: release v1.9.2 Auto-detect shell/subprocess relationships on BPMN import and startup; remove unused DsoWerkzaamheid and zoekActiviteiten imports. --- packages/frontend/src/changelog.json | 20 +++++- .../components/BpmnModeler/BpmnModeler.tsx | 61 ++++++++++++++++++- .../components/DsoExplorer/DsoExplorer.tsx | 2 - 3 files changed, 79 insertions(+), 4 deletions(-) diff --git a/packages/frontend/src/changelog.json b/packages/frontend/src/changelog.json index 491c741..02239ed 100644 --- a/packages/frontend/src/changelog.json +++ b/packages/frontend/src/changelog.json @@ -1,10 +1,28 @@ { "versions": [ { - "version": "1.9.1", + "version": "1.9.2", "status": "Latest", "statusColor": "emerald", "borderColor": "emerald", + "date": "June 9, 2026", + "sections": [ + { + "icon": "🔀", + "iconColor": "blue", + "title": "BPMN Modeler", + "items": [ + "Uploaded BPMN processes that form a shell/subprocess pair are now automatically linked: a process with call-activity elements is classified as a shell, and any process whose BPMN process ID is targeted by a shell's call-activity is classified as its subprocess. The relationship is detected both on fresh imports and retroactively on startup, so previously uploaded standalone processes are reclassified without requiring a re-upload.", + "Removed two unused TypeScript imports (DsoWerkzaamheid, zoekActiviteiten) in DsoExplorer." + ] + } + ] + }, + { + "version": "1.9.1", + "status": "Released", + "statusColor": "slate", + "borderColor": "slate", "date": "June 5, 2026", "sections": [ { diff --git a/packages/frontend/src/components/BpmnModeler/BpmnModeler.tsx b/packages/frontend/src/components/BpmnModeler/BpmnModeler.tsx index fd420fe..42bf832 100644 --- a/packages/frontend/src/components/BpmnModeler/BpmnModeler.tsx +++ b/packages/frontend/src/components/BpmnModeler/BpmnModeler.tsx @@ -17,6 +17,58 @@ const extractBpmnProcessId = (xml: string): string => { return match?.[1] ?? 'unknown'; }; +/** Returns every calledElement value found in callActivity elements. */ +const extractCallActivityTargets = (xml: string): string[] => { + const targets: string[] = []; + const re = /calledElement="([^"]+)"/g; + let m; + while ((m = re.exec(xml)) !== null) targets.push(m[1]); + return targets; +}; + +/** + * Inspects all standalone processes and reclassifies them as shell or subprocess + * based on their BPMN XML. A process with callActivity elements becomes a shell; + * a process whose bpmnProcessId is referenced by a shell's callActivity becomes + * a subprocess. Returns true when at least one record was updated. + */ +const reclassifyProcessRoles = (all: BpmnProcess[]): boolean => { + const standalones = all.filter((p) => p.processRole === 'standalone'); + if (standalones.length === 0) return false; + + let changed = false; + const promotedShells: BpmnProcess[] = []; + + for (const p of standalones) { + if (extractCallActivityTargets(p.xml).length > 0) { + const updated: BpmnProcess = { ...p, processRole: 'shell' }; + BpmnService.saveProcess(updated); + promotedShells.push(updated); + changed = true; + } + } + + const allShells = [...all.filter((p) => p.processRole === 'shell'), ...promotedShells]; + const promotedIds = new Set(promotedShells.map((s) => s.id)); + + for (const p of standalones) { + if (promotedIds.has(p.id)) continue; + const parent = allShells.find( + (s) => p.bpmnProcessId && extractCallActivityTargets(s.xml).includes(p.bpmnProcessId) + ); + if (parent?.bpmnProcessId) { + BpmnService.saveProcess({ + ...p, + processRole: 'subprocess', + calledElement: parent.bpmnProcessId, + }); + changed = true; + } + } + + return changed; +}; + type FooterDraft = { language?: BpmnProcess['language']; organization?: string; @@ -315,7 +367,13 @@ const BpmnModeler: React.FC = ({ endpoint }) => { }, []); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { - BpmnService.hydrateFromServer().then(setProcesses); + BpmnService.hydrateFromServer().then((all) => { + if (reclassifyProcessRoles(all)) { + setProcesses(BpmnService.getProcesses()); + } else { + setProcesses(all); + } + }); }, []); const handleCreateProcess = () => { @@ -357,6 +415,7 @@ const BpmnModeler: React.FC = ({ endpoint }) => { organization, }; BpmnService.saveProcess(newProcess); + reclassifyProcessRoles(BpmnService.getProcesses()); setProcesses(BpmnService.getProcesses()); setActiveProcessId(newProcess.id); setCurrentXml(xml); diff --git a/packages/frontend/src/components/DsoExplorer/DsoExplorer.tsx b/packages/frontend/src/components/DsoExplorer/DsoExplorer.tsx index 94d2de7..7633071 100644 --- a/packages/frontend/src/components/DsoExplorer/DsoExplorer.tsx +++ b/packages/frontend/src/components/DsoExplorer/DsoExplorer.tsx @@ -11,7 +11,6 @@ import { DsoBegrip, DsoEnv, DsoRegelbeheerobject, - DsoWerkzaamheid, DsoWerkzaamheidVersie, getActiviteitDetail, getActiviteiten, @@ -21,7 +20,6 @@ import { suggereerWerkzaamheden, urnFromHref, WerkzaamhedenZoekResult, - zoekActiviteiten, zoekWerkzaamheden, } from '../../services/dsoService'; From 8ba4c9428f04be61ddde8a1027acfbb0188a1a3a Mon Sep 17 00:00:00 2001 From: Steven Gort Date: Tue, 9 Jun 2026 17:24:16 +0200 Subject: [PATCH 13/41] =?UTF-8?q?feat(dso):=20Phase=202a=20+=204=20?= =?UTF-8?q?=E2=80=94=20applicable=20rules=20panel=20with=20STTR/DMN/form?= =?UTF-8?q?=20scaffold=20downloads?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added Applicable Rules section to Activity Detail panel showing toepasbare regels from the DSO Uitvoeren Gegevens API, grouped by Conclusie / Indieningsvereisten - Backend: new /toepasbare-regels routes (metadata, STTR, DMN extraction, form scaffold) - Backend: uitvoerenGegevensBaseUrl added to both pre and prod DSO config - Form scaffold parses uitv:uitvoeringsregels from dmn:extensionElements (boolean→checkbox, list→select with options, number→number, string→textfield/textarea, bijlage→textfield) - DMN extraction returns the full dmn:definitions element as a standalone file - STTR route now sends Content-Disposition: attachment to trigger browser download - Linked TreeFellingPermitSubProcess to nl.imow-gm0995.activiteit.HoutopstandVellen via ronl:dsoActiviteitUrn; bumped example version to re-seed - Added verified STTR example outputs for Gemeente Lelystad HoutopstandVellen - Release v1.9.3 --- .../flevoland/STTR/Indieningsvereisten.xml | 1102 +++++++++++++++++ .../flevoland/STTR/conclusie.xml | 787 ++++++++++++ .../flevoland/STTR/decision-105946.dmn | 787 ++++++++++++ .../flevoland/STTR/form-scaffold-105947.json | 107 ++ .../flevoland/STTR/sttr-105946.xml | 787 ++++++++++++ .../flevoland/STTR/sttr-105947.xml | 1102 +++++++++++++++++ packages/backend/src/routes/dso.routes.ts | 89 +- packages/backend/src/services/dso.service.ts | 161 +++ packages/backend/src/utils/config.ts | 6 + .../TreeFellingPermitSubProcess.bpmn | 3 +- packages/frontend/src/changelog.json | 23 +- .../components/DsoExplorer/DsoExplorer.tsx | 167 ++- packages/frontend/src/services/dsoService.ts | 72 +- .../frontend/src/utils/exampleVersions.ts | 2 +- 14 files changed, 5188 insertions(+), 7 deletions(-) create mode 100644 examples/organizations/flevoland/STTR/Indieningsvereisten.xml create mode 100644 examples/organizations/flevoland/STTR/conclusie.xml create mode 100644 examples/organizations/flevoland/STTR/decision-105946.dmn create mode 100644 examples/organizations/flevoland/STTR/form-scaffold-105947.json create mode 100644 examples/organizations/flevoland/STTR/sttr-105946.xml create mode 100644 examples/organizations/flevoland/STTR/sttr-105947.xml 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..2df3f35 --- /dev/null +++ b/examples/organizations/flevoland/STTR/decision-105946.dmn @@ -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" + + + + + \ No newline at end of file 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/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/services/dso.service.ts b/packages/backend/src/services/dso.service.ts index ba3c628..3502395 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'; @@ -336,3 +337,163 @@ 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 +// --------------------------------------------------------------------------- + +/** + * Extracts the embedded DMN element from a conclusie STTR envelope + * and returns it as a standalone 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${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/utils/config.ts b/packages/backend/src/utils/config.ts index 70b529c..c6ce0a3 100644 --- a/packages/backend/src/utils/config.ts +++ b/packages/backend/src/utils/config.ts @@ -85,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), }, @@ -102,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/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/changelog.json b/packages/frontend/src/changelog.json index 02239ed..0339320 100644 --- a/packages/frontend/src/changelog.json +++ b/packages/frontend/src/changelog.json @@ -1,11 +1,32 @@ { "versions": [ { - "version": "1.9.2", + "version": "1.9.3", "status": "Latest", "statusColor": "emerald", "borderColor": "emerald", "date": "June 9, 2026", + "sections": [ + { + "icon": "🏛️", + "iconColor": "purple", + "title": "DSO Integration — Phase 2a + 4", + "items": [ + "Activity Detail panel now shows an 'Applicable Rules' section listing toepasbare regels fetched live from the DSO Uitvoeren Gegevens API, grouped by rule type (Conclusie / Indieningsvereisten) with validity date and STTR version.", + "↓ STTR button downloads the raw STTR XML for any rule type.", + "↓ Extract DMN button (Conclusie only) extracts the embedded DMN decision table as a standalone .dmn file ready for import into LDE or deployment to Operaton.", + "↓ Form scaffold button (Indieningsvereisten only) generates a form-js JSON scaffold from the STTR questionnaire, mapping boolean questions to checkboxes, list questions to select fields with options, number questions to number fields, and attachment requirements to labelled textfields.", + "Added ronl:dsoActiviteitUrn extension on TreeFellingPermitSubProcess linking it to nl.imow-gm0995.activiteit.HoutopstandVellen (Gemeente Lelystad)." + ] + } + ] + }, + { + "version": "1.9.2", + "status": "Released", + "statusColor": "slate", + "borderColor": "slate", + "date": "June 9, 2026", "sections": [ { "icon": "🔀", diff --git a/packages/frontend/src/components/DsoExplorer/DsoExplorer.tsx b/packages/frontend/src/components/DsoExplorer/DsoExplorer.tsx index 7633071..f87a58b 100644 --- a/packages/frontend/src/components/DsoExplorer/DsoExplorer.tsx +++ b/packages/frontend/src/components/DsoExplorer/DsoExplorer.tsx @@ -6,18 +6,24 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { ActiviteitenResult, BegrippenResult, + dmnDownloadUrl, DsoActiviteit, DsoActiviteitDetail, DsoBegrip, DsoEnv, DsoRegelbeheerobject, + DsoToepasbareRegel, DsoWerkzaamheidVersie, + fetchFormScaffold, + fetchToepasbareRegels, getActiviteitDetail, getActiviteiten, getActiviteitenByOin, getWerkzaamheidDetail, searchBegrippen, + sttrDownloadUrl, suggereerWerkzaamheden, + ToepasbareRegelsResult, urnFromHref, WerkzaamhedenZoekResult, zoekWerkzaamheden, @@ -483,16 +489,25 @@ const WerkzaamhedenTab: React.FC<{ env: DsoEnv }> = ({ env }) => { // ── Activities tab ─────────────────────────────────────────────────────────── -const TYPERING_META: Record = { +const TYPERING_META: Record = { indieningsvereisten: { label: 'Submission requirements', color: 'bg-blue-100 text-blue-700 border-blue-200', }, + Indieningsvereisten: { + label: 'Submission requirements', + color: 'bg-blue-100 text-blue-700 border-blue-200', + }, conclusie: { label: 'Decision criteria', color: 'bg-purple-100 text-purple-700 border-purple-200', }, + Conclusie: { + label: 'Decision criteria', + color: 'bg-purple-100 text-purple-700 border-purple-200', + }, maatregelen: { label: 'Measures', color: 'bg-amber-100 text-amber-700 border-amber-200' }, + Maatregelen: { label: 'Measures', color: 'bg-amber-100 text-amber-700 border-amber-200' }, }; const Section: React.FC<{ title: string; children: React.ReactNode }> = ({ title, children }) => ( @@ -504,6 +519,151 @@ const Section: React.FC<{ title: string; children: React.ReactNode }> = ({ title
    ); +// ── Applicable Rules (STTR) ────────────────────────────────────────────────── + +const ApplicableRuleRow: React.FC<{ + regel: DsoRegelbeheerobject; + env: DsoEnv; +}> = ({ regel, env }) => { + 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); + + 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); + } + }; + + 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 + + )} + {regel.typering.toLowerCase() === 'indieningsvereisten' && ( + + )} +
    + {scaffoldError &&

    {scaffoldError}

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

    No toepasbare regels found.

    + )} +
    + ); +}; + +const ApplicableRulesSection: React.FC<{ + regelBeheerObjecten: DsoRegelbeheerobject[]; + env: DsoEnv; +}> = ({ regelBeheerObjecten, env }) => { + 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; @@ -676,6 +836,11 @@ const ActivityDetailPanel: React.FC<{ )} + {/* Applicable rules (STTR) */} + {detail.regelBeheerObjecten && detail.regelBeheerObjecten.length > 0 && ( + + )} + {/* Child activities */} {detail._links?.onderliggendeActiviteiten && detail._links.onderliggendeActiviteiten.length > 0 && ( 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/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 From 6300254de0b0c99921f0278e846626d24f8a643b Mon Sep 17 00:00:00 2001 From: Steven Gort Date: Tue, 9 Jun 2026 17:27:08 +0200 Subject: [PATCH 14/41] docs: rewrite dso-integration-phase-plan to reflect v1.9.3 state --- dso-integration-phase-plan.md | 247 ++++++++++++++++++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 dso-integration-phase-plan.md diff --git a/dso-integration-phase-plan.md b/dso-integration-phase-plan.md new file mode 100644 index 0000000..e7630a4 --- /dev/null +++ b/dso-integration-phase-plan.md @@ -0,0 +1,247 @@ +# DSO Integration — Phase Plan + +## 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** a deployable DMN. "Extract DMN" returns the full `` element as a standalone `.dmn` file, which can be imported into LDE or deployed to Operaton directly. + +### 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 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) +- Next: "Import into LDE" button to register the DMN as a new asset linked to the subprocess + +**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 +- Next: "Import into LDE" button to register the form as a new asset + +**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 | +| ↓ 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 DMN into LDE from STTR | ⏳ Pending | +| Import form scaffold into LDE from STTR | ⏳ Pending | +| 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 ✅ From 8fcf0f9c064308db1ef21415e37dd4499cff03d2 Mon Sep 17 00:00:00 2001 From: Steven Gort Date: Tue, 9 Jun 2026 17:33:52 +0200 Subject: [PATCH 15/41] chore: dso integration touch [no ci] --- dso-integration-phase-plan.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dso-integration-phase-plan.md b/dso-integration-phase-plan.md index e7630a4..7c8470c 100644 --- a/dso-integration-phase-plan.md +++ b/dso-integration-phase-plan.md @@ -1,4 +1,4 @@ -# DSO Integration — Phase Plan +# DSO Integration — Phase Plan updated ## End goal From b7ebae7b4883b6e20ec9e6056abce5d640061a27 Mon Sep 17 00:00:00 2001 From: Steven Gort Date: Wed, 10 Jun 2026 08:58:05 +0200 Subject: [PATCH 16/41] @ feat(dso): location-scoped name search in Activities tab (Phase 2d) Fixing a location preset now loads the authority full activity set in one call (pageSize 200) and reveals a name search box that live-filters by omschrijving client-side. Makes activities like HoutopstandVellen findable without walking the hierarchy. @ --- dso-integration-phase-plan.md | 9 ++ packages/backend/src/services/dso.service.ts | 4 +- .../components/DsoExplorer/DsoExplorer.tsx | 95 ++++++++++++++----- 3 files changed, 85 insertions(+), 23 deletions(-) diff --git a/dso-integration-phase-plan.md b/dso-integration-phase-plan.md index 7c8470c..0b86e7b 100644 --- a/dso-integration-phase-plan.md +++ b/dso-integration-phase-plan.md @@ -129,6 +129,14 @@ The questionnaire is extracted from `uitv:uitvoeringsregels`, not from the decis - 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 @@ -192,6 +200,7 @@ The questionnaire is extracted from `uitv:uitvoeringsregels`, not from the decis | `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 | diff --git a/packages/backend/src/services/dso.service.ts b/packages/backend/src/services/dso.service.ts index 3502395..c707efb 100644 --- a/packages/backend/src/services/dso.service.ts +++ b/packages/backend/src/services/dso.service.ts @@ -100,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, diff --git a/packages/frontend/src/components/DsoExplorer/DsoExplorer.tsx b/packages/frontend/src/components/DsoExplorer/DsoExplorer.tsx index f87a58b..a4b7727 100644 --- a/packages/frontend/src/components/DsoExplorer/DsoExplorer.tsx +++ b/packages/frontend/src/components/DsoExplorer/DsoExplorer.tsx @@ -936,6 +936,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; @@ -948,6 +951,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); @@ -1003,6 +1007,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(''); @@ -1023,6 +1028,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 */} @@ -1072,6 +1085,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" @@ -1081,6 +1095,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 && ( +
    + + +
    + )}
    )}
    From f400afec49da6cccba571058e2216f7be5414ad8 Mon Sep 17 00:00:00 2001 From: Steven Gort Date: Wed, 10 Jun 2026 09:14:06 +0200 Subject: [PATCH 17/41] @ feat(dso): import indieningsvereisten form scaffold into LDE (Phase 4.2) Adds an "Import into LDE" button to indieningsvereisten entries in the Applicable Rules panel. The DSO form scaffold is already a form-js schema, so it is saved straight into the form store via FormService.saveForm (localStorage + assets API), stamped with the execution-platform metadata the editor and Operaton deploy expect. It lands in the Form Editor as a wip draft named after the activity and tagged with the authority as organization. DMN import (Phase 4.1) stays download-only: DMNs have no local asset store (the picker reads from TriplyDB via SPARQL), so importing needs an architectural decision (deploy-to-Operaton / validate / local store), deferred and documented in the phase plan. @ --- dso-integration-phase-plan.md | 12 +- .../components/DsoExplorer/DsoExplorer.tsx | 118 ++++++++++++++++-- 2 files changed, 114 insertions(+), 16 deletions(-) diff --git a/dso-integration-phase-plan.md b/dso-integration-phase-plan.md index 0b86e7b..ec2e86a 100644 --- a/dso-integration-phase-plan.md +++ b/dso-integration-phase-plan.md @@ -161,7 +161,11 @@ The questionnaire is extracted from `uitv:uitvoeringsregels`, not from the decis - 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) -- Next: "Import into LDE" button to register the DMN as a new asset linked to the subprocess +- **"Import into LDE" is blocked on an architectural decision** ⏳ — 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`. So an extracted DMN has nowhere to "import" to that would surface it in the picker. Options, increasing cost: + - **A — Deploy to Operaton:** executable + referenceable via `camunda:decisionRef`, but won't appear in the SPARQL-sourced dropdown + - **B — Validate first:** wire `POST /v1/dmns/validate` to show RONL-layer issues before deploy; keep download + - **C — Local DMN asset store:** new Postgres table + routes, merged into the DMN list alongside SPARQL results (makes DSO DMNs first-class LDE assets) + - **Decision: deferred.** DMN remains download-only until the direction is chosen. **Step 4.2 — Form scaffold from indieningsvereisten STTR ✅ Done (v1.9.3)** @@ -169,7 +173,7 @@ The questionnaire is extracted from `uitv:uitvoeringsregels`, not from the decis - 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 -- Next: "Import into LDE" button to register the form as a new asset +- ✅ **"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** @@ -207,8 +211,8 @@ The questionnaire is extracted from `uitv:uitvoeringsregels`, not from the decis | Werkzaamheid keywords + logical relations | ⏳ Pending (`_expandScope` enum) | | Works tab → Applicable Rules shortcut (Phase 2b) | ⏳ Pending | | Rule type completeness check (Phase 2c) | ⏳ Pending | -| Import DMN into LDE from STTR | ⏳ Pending | -| Import form scaffold into LDE from STTR | ⏳ Pending | +| Import DMN into LDE from STTR | ⏳ Blocked (no local DMN store — A/B/C decision deferred) | +| Import form scaffold into LDE from STTR | ✅ Live | | Maatregelen → document template scaffold (Phase 4.3) | ⏳ Pending | | BPMN subprocess scaffold (Phase 4.4) | ⏳ Pending | | Deploy bundle to Operaton (Phase 5) | ⏳ Pending | diff --git a/packages/frontend/src/components/DsoExplorer/DsoExplorer.tsx b/packages/frontend/src/components/DsoExplorer/DsoExplorer.tsx index a4b7727..0b16c86 100644 --- a/packages/frontend/src/components/DsoExplorer/DsoExplorer.tsx +++ b/packages/frontend/src/components/DsoExplorer/DsoExplorer.tsx @@ -1,6 +1,15 @@ // packages/frontend/src/components/DsoExplorer/DsoExplorer.tsx -import { BookOpen, ChevronLeft, ChevronRight, Loader2, Search, TreePine } from 'lucide-react'; +import { + BookOpen, + Check, + ChevronLeft, + ChevronRight, + Download, + Loader2, + Search, + TreePine, +} from 'lucide-react'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { @@ -28,6 +37,8 @@ import { WerkzaamhedenZoekResult, zoekWerkzaamheden, } from '../../services/dsoService'; +import { FormService } from '../../services/formService'; +import { FormSchema } from '../../types'; type Tab = 'begrippen' | 'werkzaamheden' | 'activiteiten'; @@ -524,12 +535,17 @@ const Section: React.FC<{ title: string; children: React.ReactNode }> = ({ title const ApplicableRuleRow: React.FC<{ regel: DsoRegelbeheerobject; env: DsoEnv; -}> = ({ regel, env }) => { + activityName?: string; + organization?: string; +}> = ({ regel, env, activityName, organization }) => { 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; @@ -565,6 +581,43 @@ const ApplicableRuleRow: React.FC<{ } }; + // 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: 'wip', + language: 'nl', + organization, + }; + FormService.saveForm(newForm); + setImported(true); + } catch (e) { + setImportError(e instanceof Error ? e.message : 'Import failed'); + } finally { + setImporting(false); + } + }; + return (
    )} {regel.typering.toLowerCase() === 'indieningsvereisten' && ( - + <> + + + )}
    {scaffoldError &&

    {scaffoldError}

    } + {importError &&

    {importError}

    } + {imported && ( +

    Saved to Form Editor as a draft.

    + )}
    ))} @@ -644,7 +721,9 @@ const ApplicableRuleRow: React.FC<{ const ApplicableRulesSection: React.FC<{ regelBeheerObjecten: DsoRegelbeheerobject[]; env: DsoEnv; -}> = ({ regelBeheerObjecten, env }) => { + activityName?: string; + organization?: string; +}> = ({ regelBeheerObjecten, env, activityName, organization }) => { const candidates = regelBeheerObjecten.filter( (r) => r.functioneleStructuurRef && @@ -657,7 +736,13 @@ const ApplicableRulesSection: React.FC<{
    {candidates.map((r) => ( - + ))}
    @@ -838,7 +923,16 @@ const ActivityDetailPanel: React.FC<{ {/* Applicable rules (STTR) */} {detail.regelBeheerObjecten && detail.regelBeheerObjecten.length > 0 && ( - + )} {/* Child activities */} From fdb45fd6dc19618ef99e2f50b1cb2a732f2c2336 Mon Sep 17 00:00:00 2001 From: Steven Gort Date: Wed, 10 Jun 2026 09:34:01 +0200 Subject: [PATCH 18/41] @ fix(dso): resolve readable authority name for imported form organization The RTR API only returns the authority code (e.g. GM0995), so imported forms were tagged with the code instead of a name. Add a shared LOCATION_PRESETS table + authorityLabel() resolver: known authority OINs (Lelystad, Flevoland) map to their names, others fall back to the code. The Activities tab presets now reuse the same table. @ --- .../components/DsoExplorer/DsoExplorer.tsx | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/packages/frontend/src/components/DsoExplorer/DsoExplorer.tsx b/packages/frontend/src/components/DsoExplorer/DsoExplorer.tsx index 0b16c86..5b36365 100644 --- a/packages/frontend/src/components/DsoExplorer/DsoExplorer.tsx +++ b/packages/frontend/src/components/DsoExplorer/DsoExplorer.tsx @@ -500,6 +500,29 @@ const WerkzaamhedenTab: React.FC<{ env: DsoEnv }> = ({ env }) => { // ── Activities tab ─────────────────────────────────────────────────────────── +// Known authorities with human-readable names, keyed by OIN. Drives both the +// location presets below and the organization label used when importing forms, +// since the RTR API only returns the authority code (e.g. "GM0995"), not a name. +const LOCATION_PRESETS = [ + { label: 'Lelystad', oin: '00000001005024249000' }, + { label: 'Flevoland', oin: '00000001006203243000' }, +] as const; + +/** + * Resolve a readable authority name for an activity's bestuursorgaan. + * Falls back to the bare code (organisatieType + organisatieCode) when the + * authority is not one of the known presets. + */ +function authorityLabel(bestuursorgaan?: { + oin: string; + organisatieType: string; + organisatieCode: string; +}): string | undefined { + if (!bestuursorgaan) return undefined; + const preset = LOCATION_PRESETS.find((p) => p.oin === bestuursorgaan.oin); + return preset?.label ?? `${bestuursorgaan.organisatieType}${bestuursorgaan.organisatieCode}`; +} + const TYPERING_META: Record = { indieningsvereisten: { label: 'Submission requirements', @@ -927,11 +950,7 @@ const ActivityDetailPanel: React.FC<{ regelBeheerObjecten={detail.regelBeheerObjecten} env={env} activityName={detail.omschrijving ?? undefined} - organization={ - detail.bestuursorgaan - ? `${detail.bestuursorgaan.organisatieType}${detail.bestuursorgaan.organisatieCode}` - : undefined - } + organization={authorityLabel(detail.bestuursorgaan)} /> )} @@ -1015,10 +1034,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); From f4bfd25bd71b2781f88079c38c6c9730145fb946 Mon Sep 17 00:00:00 2001 From: Steven Gort Date: Wed, 10 Jun 2026 09:45:57 +0200 Subject: [PATCH 19/41] @ chore: release v1.9.4 Changelog entry for Phase 2d (location-scoped activity name search), Phase 4.2 (import form scaffold into LDE), and the readable authority name fix for imported forms. @ --- packages/frontend/src/changelog.json | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/frontend/src/changelog.json b/packages/frontend/src/changelog.json index 0339320..227379e 100644 --- a/packages/frontend/src/changelog.json +++ b/packages/frontend/src/changelog.json @@ -1,10 +1,29 @@ { "versions": [ { - "version": "1.9.3", + "version": "1.9.4", "status": "Latest", "statusColor": "emerald", "borderColor": "emerald", + "date": "June 10, 2026", + "sections": [ + { + "icon": "🏛️", + "iconColor": "purple", + "title": "DSO Integration — Phase 2d + 4.2", + "items": [ + "Activities tab now supports name search: fixing a location (Lelystad / Flevoland) loads that authority's full activity set in one call and reveals a search box that live-filters by name, so activities like 'Boom kappen of houtopstand vellen' are findable without walking the hierarchy.", + "↓ Import into LDE button (Indieningsvereisten only) saves the generated form-js scaffold straight into the Form Editor as a draft, named after the activity and tagged with the authority — no manual download/import step needed.", + "Imported forms are now tagged with the readable authority name (e.g. 'Lelystad') instead of the bare RTR code (GM0995), falling back to the code for authorities outside the known presets." + ] + } + ] + }, + { + "version": "1.9.3", + "status": "Released", + "statusColor": "slate", + "borderColor": "slate", "date": "June 9, 2026", "sections": [ { From 9eae68dfe7aef280d0d9d946740fc04cd9054274 Mon Sep 17 00:00:00 2001 From: Steven Gort Date: Wed, 10 Jun 2026 10:14:15 +0200 Subject: [PATCH 20/41] @ chore: gitignore .claude/ local config and memory Claude Code local settings and per-machine memory live under .claude/ and encode a machine-specific absolute path, so they are not portable and should not be tracked. @ --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index df74401..80cf536 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,9 @@ dist-ssr *.sw? .claudesync +# Claude Code local config & per-machine memory (machine-specific, not portable) +.claude/ + # flatten script & dir flatten_directory.sh flattened From 008b84a0eef4cf96ac3fa0f7aac38d444043250c Mon Sep 17 00:00:00 2001 From: Steven Gort Date: Wed, 10 Jun 2026 10:16:10 +0200 Subject: [PATCH 21/41] chore: formatted --- packages/backend/src/services/dso.service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/backend/src/services/dso.service.ts b/packages/backend/src/services/dso.service.ts index c707efb..6cf4179 100644 --- a/packages/backend/src/services/dso.service.ts +++ b/packages/backend/src/services/dso.service.ts @@ -436,8 +436,7 @@ export function extractFormScaffoldFromSttr(sttrXml: string, formId: string): Fo const defs = parsed?.['dmn:definitions'] ?? parsed?.['definitions'] ?? {}; const ext = defs?.['dmn:extensionElements'] ?? {}; - const regels: unknown[] = - ext?.['uitv:uitvoeringsregels']?.['uitv:uitvoeringsregel'] ?? []; + const regels: unknown[] = ext?.['uitv:uitvoeringsregels']?.['uitv:uitvoeringsregel'] ?? []; const components: FormScaffoldField[] = []; From 45c09857e385cffde300a3b8c2b140a1812ce836 Mon Sep 17 00:00:00 2001 From: Bas Date: Wed, 10 Jun 2026 11:42:45 +0200 Subject: [PATCH 22/41] implementation of CPRMV layer in SHACL Validator - with the testfile from the CPRMV project --- .../shapes/cprmv/0.4.1/cprmv.shacl.ttl | 209 ++++++++++++++++++ .../src/services/shacl-validation.service.ts | 10 +- .../tests/fixtures/shacl/cprmv-test.ttl | 77 +++++++ 3 files changed, 294 insertions(+), 2 deletions(-) create mode 100644 packages/backend/shapes/cprmv/0.4.1/cprmv.shacl.ttl create mode 100644 packages/backend/tests/fixtures/shacl/cprmv-test.ttl 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..431d73a --- /dev/null +++ b/packages/backend/shapes/cprmv/0.4.1/cprmv.shacl.ttl @@ -0,0 +1,209 @@ +@prefix sh: . +@prefix rdf: . +@prefix rdfs: . +@prefix xsd: . +@prefix cpsv: . +@prefix cprmv: . + +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 . + diff --git a/packages/backend/src/services/shacl-validation.service.ts b/packages/backend/src/services/shacl-validation.service.ts index 6f100dc..2579b6f 100644 --- a/packages/backend/src/services/shacl-validation.service.ts +++ b/packages/backend/src/services/shacl-validation.service.ts @@ -38,7 +38,7 @@ import logger from '../utils/logger'; // ── Response types (shape matches DmnValidator's so the UI components are shared) ── -export type ShaclLayerKey = 'cpsv-ap' | 'ronl-custom'; +export type ShaclLayerKey = 'cpsv-ap' | 'ronl-custom' | 'cprmv'; export interface ShaclIssue { severity: 'error' | 'warning' | 'info'; @@ -90,6 +90,11 @@ const LAYER_SPECS: LayerSpec[] = [ label: 'RONL Custom', dir: 'ronl', }, + { + key: 'cprmv', + label: 'CPRMV 0.4.1', + files: ['cprmv/0.4.1/cprmv.shacl.ttl'], + }, ]; interface LoadedLayer { @@ -227,8 +232,9 @@ export class ShaclValidationService { 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: [] }, + 'ronl-custom': { label: 'RONL Custom', loaded: false, issues: [] } }; } diff --git a/packages/backend/tests/fixtures/shacl/cprmv-test.ttl b/packages/backend/tests/fixtures/shacl/cprmv-test.ttl new file mode 100644 index 0000000..46f763e --- /dev/null +++ b/packages/backend/tests/fixtures/shacl/cprmv-test.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" . From 6628b70a00c65938f6b010ab58076e7b89f4bf1f Mon Sep 17 00:00:00 2001 From: Steven Gort Date: Wed, 10 Jun 2026 15:26:14 +0200 Subject: [PATCH 23/41] @ feat(dso): publish-via-CPSV-Editor handoff deep-link Add a "Publish via CPSV Editor" action on Conclusie entries in the Applicable Rules panel. It opens the CPSV Editor with a deep-link (dsoImport=dmn + identifiers + DSO metadata); the CPSV Editor fetches the DMN XML from the shared backend, so the payload never rides in the URL. New VITE_CPSV_EDITOR_URL env var per environment. Also: gitignore local .env (committed envs remain .env.development / .acceptance / .production) and the v1.9.4 changelog entry. @ --- .gitignore | 3 + packages/frontend/.env | 5 -- packages/frontend/.env.acceptance | 3 +- packages/frontend/.env.development | 5 +- packages/frontend/.env.production | 3 +- packages/frontend/src/changelog.json | 10 +++ .../components/DsoExplorer/DsoExplorer.tsx | 83 ++++++++++++++++--- packages/frontend/src/vite-env.d.ts | 2 + 8 files changed, 95 insertions(+), 19 deletions(-) delete mode 100644 packages/frontend/.env diff --git a/.gitignore b/.gitignore index 80cf536..e35e761 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,9 @@ dist-ssr # 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/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/src/changelog.json b/packages/frontend/src/changelog.json index 227379e..4a98d1d 100644 --- a/packages/frontend/src/changelog.json +++ b/packages/frontend/src/changelog.json @@ -16,6 +16,16 @@ "↓ Import into LDE button (Indieningsvereisten only) saves the generated form-js scaffold straight into the Form Editor as a draft, named after the activity and tagged with the authority — no manual download/import step needed.", "Imported forms are now tagged with the readable authority name (e.g. 'Lelystad') instead of the bare RTR code (GM0995), falling back to the code for authorities outside the known presets." ] + }, + { + "icon": "⚖️", + "iconColor": "purple", + "title": "DSO Integration — DMN publish handoff (Phase 4.1)", + "items": [ + "Publish via CPSV Editor button (Conclusie only) opens the CPSV Editor with a deep-link to publish the extracted DMN to TriplyDB, where the LDE DMN picker can consume it — no local DMN store needed.", + "Extracted DMNs are now normalized to deploy and evaluate on Operaton: DMN 1.2 is upgraded to 1.3, missing input ids are added, variable names are made FEEL-safe (hyphens/spaces broke evaluation), and output columns get an explicit typeRef.", + "Verified end-to-end against Operaton: the normalized HoutopstandVellen decision deploys (all 7 decisions) and the root decision evaluates without the previous FEEL error." + ] } ] }, diff --git a/packages/frontend/src/components/DsoExplorer/DsoExplorer.tsx b/packages/frontend/src/components/DsoExplorer/DsoExplorer.tsx index 5b36365..cacd3d6 100644 --- a/packages/frontend/src/components/DsoExplorer/DsoExplorer.tsx +++ b/packages/frontend/src/components/DsoExplorer/DsoExplorer.tsx @@ -6,6 +6,7 @@ import { ChevronLeft, ChevronRight, Download, + ExternalLink, Loader2, Search, TreePine, @@ -523,6 +524,44 @@ function authorityLabel(bestuursorgaan?: { return preset?.label ?? `${bestuursorgaan.organisatieType}${bestuursorgaan.organisatieCode}`; } +// ── DSO → DMN publish handoff (CPSV Editor) ────────────────────────────────── +// LDE extracts a DMN from the conclusie STTR but has no local DMN store: the DMN +// picker reads from TriplyDB via SPARQL. The CPSV Editor already deploys DMNs to +// Operaton and publishes their RDF to that same TriplyDB graph, so we hand the +// extracted DMN off to it rather than duplicating that pipeline here. +const CPSV_EDITOR_URL = import.meta.env.VITE_CPSV_EDITOR_URL || 'http://localhost:3002'; + +/** + * Build the CPSV Editor deep-link for publishing a DSO-extracted DMN. + * + * Contract (consumed by the CPSV Editor, separate codebase): + * /?dsoImport=dmn&dmnId=&env=&...metadata + * + * Only identifiers + DSO metadata travel in the URL. The CPSV Editor fetches the + * DMN XML itself from the shared LDE backend + * (GET /v1/dso/toepasbare-regels/{dmnId}/dmn?env=), so the large DMN payload + * never goes through the query string. + */ +function buildCpsvEditorImportUrl(params: { + dmnId: number; + env: DsoEnv; + activityName?: string; + authority?: string; + activityUrn?: string; + functioneleStructuurRef?: string; +}): string { + const q = new URLSearchParams({ + dsoImport: 'dmn', + dmnId: String(params.dmnId), + env: params.env, + }); + if (params.activityName) q.set('activityName', params.activityName); + if (params.authority) q.set('authority', params.authority); + if (params.activityUrn) q.set('activityUrn', params.activityUrn); + if (params.functioneleStructuurRef) q.set('fsRef', params.functioneleStructuurRef); + return `${CPSV_EDITOR_URL.replace(/\/$/, '')}/?${q.toString()}`; +} + const TYPERING_META: Record = { indieningsvereisten: { label: 'Submission requirements', @@ -560,7 +599,8 @@ const ApplicableRuleRow: React.FC<{ env: DsoEnv; activityName?: string; organization?: string; -}> = ({ regel, env, activityName, organization }) => { + activityUrn?: string; +}> = ({ regel, env, activityName, organization, activityUrn }) => { const [result, setResult] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -686,15 +726,33 @@ const ApplicableRuleRow: React.FC<{ ↓ STTR {regel.typering.toLowerCase() === 'conclusie' && ( - - ↓ Extract DMN - + <> + + ↓ Extract DMN + + + Publish via CPSV Editor + + )} {regel.typering.toLowerCase() === 'indieningsvereisten' && ( <> @@ -746,7 +804,8 @@ const ApplicableRulesSection: React.FC<{ env: DsoEnv; activityName?: string; organization?: string; -}> = ({ regelBeheerObjecten, env, activityName, organization }) => { + activityUrn?: string; +}> = ({ regelBeheerObjecten, env, activityName, organization, activityUrn }) => { const candidates = regelBeheerObjecten.filter( (r) => r.functioneleStructuurRef && @@ -765,6 +824,7 @@ const ApplicableRulesSection: React.FC<{ env={env} activityName={activityName} organization={organization} + activityUrn={activityUrn} /> ))}
    @@ -951,6 +1011,7 @@ const ActivityDetailPanel: React.FC<{ env={env} activityName={detail.omschrijving ?? undefined} organization={authorityLabel(detail.bestuursorgaan)} + activityUrn={detail.urn} /> )} 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 } From 0b10a5264d1e6724ca0d7fad210649ac5092554a Mon Sep 17 00:00:00 2001 From: Steven Gort Date: Wed, 10 Jun 2026 15:31:25 +0200 Subject: [PATCH 24/41] @ fix(dso): normalize STTR-extracted DMN for Operaton deploy + eval The Sogelink STTR DMN neither deploys nor evaluates on Operaton as-is. normalizeDmnForOperaton now: upgrades DMN 1.2 -> 1.3 (engine only transforms 1.3); injects ids on inputs/inputExpressions (engine rejects inputs without one); renames variable names to FEEL-safe identifiers and rewrites their inputExpression references (hyphens/spaces broke FEEL evaluation); and types output columns (BIZ-004). Rule logic and output value literals are left untouched. Verified against operaton.open-regels.nl: deploys all 7 decisions and the root decision evaluates (HTTP 200) where it previously threw a FEEL exception. historyTimeToLive remains the deployer (CPSV Editor) concern. Updates the phase plan and the committed HoutopstandVellen example DMN. @ --- dso-integration-phase-plan.md | 41 +- .../flevoland/STTR/decision-105946.dmn | 1560 ++++++++--------- packages/backend/src/services/dso.service.ts | 143 +- 3 files changed, 955 insertions(+), 789 deletions(-) diff --git a/dso-integration-phase-plan.md b/dso-integration-phase-plan.md index ec2e86a..4c1f8a4 100644 --- a/dso-integration-phase-plan.md +++ b/dso-integration-phase-plan.md @@ -70,7 +70,7 @@ Content: - `dmn:inputData` elements — one per `uitvoeringsregel`, typed (boolean, string, number) - `dmn:decision` elements — full decision tables implementing the check logic -The file **is** a deployable DMN. "Extract DMN" returns the full `` element as a standalone `.dmn` file, which can be imported into LDE or deployed to Operaton directly. +The file is **structurally** a DMN but is **not deployable as-extracted** — the Sogelink STTR Builder emits DMN 1.2 with `` elements lacking ids, which Operaton refuses. "Extract DMN" returns the full `` element normalized for Operaton (DMN 1.3 + input ids — see Phase 4.1); the deployer still adds `camunda:historyTimeToLive`. ### indieningsvereisten STTR (identifier 105947) @@ -161,11 +161,37 @@ The questionnaire is extracted from `uitv:uitvoeringsregels`, not from the decis - 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) -- **"Import into LDE" is blocked on an architectural decision** ⏳ — 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`. So an extracted DMN has nowhere to "import" to that would surface it in the picker. Options, increasing cost: - - **A — Deploy to Operaton:** executable + referenceable via `camunda:decisionRef`, but won't appear in the SPARQL-sourced dropdown - - **B — Validate first:** wire `POST /v1/dmns/validate` to show RONL-layer issues before deploy; keep download - - **C — Local DMN asset store:** new Postgres table + routes, merged into the DMN list alongside SPARQL results (makes DSO DMNs first-class LDE assets) - - **Decision: deferred.** DMN remains download-only until the direction is chosen. + +**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` (CPSV Editor):** this Operaton enforces HTTL at deploy. It is a deployment policy (TTL value is the org's choice), added by the deployer — the CPSV Editor — not by extraction. +- ✅ **#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+#2+#4+BIZ-004 (and #3 added at deploy) deploys 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)** @@ -211,8 +237,9 @@ The questionnaire is extracted from `uitv:uitvoeringsregels`, not from the decis | Werkzaamheid keywords + logical relations | ⏳ Pending (`_expandScope` enum) | | Works tab → Applicable Rules shortcut (Phase 2b) | ⏳ Pending | | Rule type completeness check (Phase 2c) | ⏳ Pending | -| Import DMN into LDE from STTR | ⏳ Blocked (no local DMN store — A/B/C decision deferred) | | 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 | diff --git a/examples/organizations/flevoland/STTR/decision-105946.dmn b/examples/organizations/flevoland/STTR/decision-105946.dmn index 2df3f35..7967c7c 100644 --- a/examples/organizations/flevoland/STTR/decision-105946.dmn +++ b/examples/organizations/flevoland/STTR/decision-105946.dmn @@ -1,787 +1,787 @@ - - - - - Boom kappen of houtopstand vellen_Gemeente Lelystad - 1 - - - - - - 10 - - boolean - - - - - + + + + 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 - - - - + 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 - - - - - - false - - - - - 40 - - boolean - - - - false - - - - - 50 - - boolean - - - - false - - - - - 1.0.3 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - deels - nee - - - - - - - - - - - - - - - - - - - - deels - nee - - - - - - - - - - - - - - - - - - - - - - +[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. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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" - - - - - \ No newline at end of file + + + + + + + + + + + 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/packages/backend/src/services/dso.service.ts b/packages/backend/src/services/dso.service.ts index 6cf4179..d524207 100644 --- a/packages/backend/src/services/dso.service.ts +++ b/packages/backend/src/services/dso.service.ts @@ -392,9 +392,148 @@ export async function getSttrBestand(id: string, env: DsoEnv = 'pre'): Promise`) 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; +} + +/** + * 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). + * + * NOT handled here: `camunda:historyTimeToLive` (a deployment policy, added by + * the deployer — the CPSV Editor — not by extraction). See the phase plan. + */ +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); + + return out; +} + /** * Extracts the embedded DMN element from a conclusie STTR envelope - * and returns it as a standalone DMN XML string. + * 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. @@ -402,7 +541,7 @@ export function extractDmnFromSttr(sttrXml: string): string { // 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${match[0]}`; + return `\n${normalizeDmnForOperaton(match[0])}`; } export interface FormScaffoldField { From 9e0920891458d4ce2f9a268335fd037858823cdc Mon Sep 17 00:00:00 2001 From: Steven Gort Date: Thu, 11 Jun 2026 08:13:49 +0200 Subject: [PATCH 25/41] @ feat(dso): add historyTimeToLive so extracted DMN is deploy-ready MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This Operaton instance enforces camunda:historyTimeToLive at deploy. normalizeDmnForOperaton now declares the camunda namespace and stamps historyTimeToLive="180" (matching the LDE BPMN-template convention) on every , so the extracted DMN deploys as handed off — the CPSV Editor deploys it as-is, no patching. Decisions that already declare HTTL are left untouched. Verified: the normalized HoutopstandVellen DMN now deploys directly (all 7 decisions) and the root decision evaluates, with no external HTTL. @ --- dso-integration-phase-plan.md | 6 ++-- packages/backend/src/services/dso.service.ts | 35 ++++++++++++++++++-- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/dso-integration-phase-plan.md b/dso-integration-phase-plan.md index 4c1f8a4..4822341 100644 --- a/dso-integration-phase-plan.md +++ b/dso-integration-phase-plan.md @@ -70,7 +70,7 @@ Content: - `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, which Operaton refuses. "Extract DMN" returns the full `` element normalized for Operaton (DMN 1.3 + input ids — see Phase 4.1); the deployer still adds `camunda:historyTimeToLive`. +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) @@ -166,11 +166,11 @@ The questionnaire is extracted from `uitv:uitvoeringsregels`, not from the decis - ✅ **#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` (CPSV Editor):** this Operaton enforces HTTL at deploy. It is a deployment policy (TTL value is the org's choice), added by the deployer — the CPSV Editor — not by extraction. +- ✅ **#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+#2+#4+BIZ-004 (and #3 added at deploy) deploys 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. +- 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. diff --git a/packages/backend/src/services/dso.service.ts b/packages/backend/src/services/dso.service.ts index d524207..ceaa8ad 100644 --- a/packages/backend/src/services/dso.service.ts +++ b/packages/backend/src/services/dso.service.ts @@ -455,6 +455,31 @@ function sanitizeFeelNames(xml: string): string { 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 @@ -491,9 +516,10 @@ function ensureOutputTypeRefs(xml: string): string { * 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). - * - * NOT handled here: `camunda:historyTimeToLive` (a deployment policy, added by - * the deployer — the CPSV Editor — not by extraction). See the phase plan. + * 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). @@ -528,6 +554,9 @@ export function normalizeDmnForOperaton(dmn: string): string { // 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; } From 00dd0abcea39076d585417a8bded8c9887f49e68 Mon Sep 17 00:00:00 2001 From: Steven Gort Date: Thu, 11 Jun 2026 08:14:02 +0200 Subject: [PATCH 26/41] @ feat(form): green DSO badge for forms imported from DSO Forms imported via the DSO Applicable Rules panel now get status "dso" and render a green DSO badge in the Form Editor list, instead of the generic yellow WIP badge. form_schemas.status is unconstrained TEXT, so the new value persists fine; DSO forms stay editable like wip forms. Includes the v1.9.5 changelog entry (HTTL deploy-ready DMN + this badge). @ --- packages/frontend/src/changelog.json | 20 ++++++++++++++++++- .../components/DsoExplorer/DsoExplorer.tsx | 2 +- .../src/components/FormEditor/FormList.tsx | 5 +++++ packages/frontend/src/types/index.ts | 2 +- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/packages/frontend/src/changelog.json b/packages/frontend/src/changelog.json index 4a98d1d..c684eb5 100644 --- a/packages/frontend/src/changelog.json +++ b/packages/frontend/src/changelog.json @@ -1,10 +1,28 @@ { "versions": [ { - "version": "1.9.4", + "version": "1.9.5", "status": "Latest", "statusColor": "emerald", "borderColor": "emerald", + "date": "June 11, 2026", + "sections": [ + { + "icon": "⚖️", + "iconColor": "purple", + "title": "DSO Integration — deploy-ready DMN + form badge", + "items": [ + "Extracted DSO DMNs now carry camunda:historyTimeToLive, so they deploy to Operaton exactly as handed off — the consumer no longer has to patch the DMN before deploying. LDE now produces a fully deploy-ready and evaluatable DMN (DMN 1.3, input ids, FEEL-safe variable names, output typeRefs, history TTL).", + "Forms imported from DSO now show a green DSO badge in the Form Editor list, instead of the generic yellow WIP badge." + ] + } + ] + }, + { + "version": "1.9.4", + "status": "Released", + "statusColor": "slate", + "borderColor": "slate", "date": "June 10, 2026", "sections": [ { diff --git a/packages/frontend/src/components/DsoExplorer/DsoExplorer.tsx b/packages/frontend/src/components/DsoExplorer/DsoExplorer.tsx index cacd3d6..3588b41 100644 --- a/packages/frontend/src/components/DsoExplorer/DsoExplorer.tsx +++ b/packages/frontend/src/components/DsoExplorer/DsoExplorer.tsx @@ -668,7 +668,7 @@ const ApplicableRuleRow: React.FC<{ }, createdAt: now, updatedAt: now, - status: 'wip', + status: 'dso', language: 'nl', organization, }; 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/types/index.ts b/packages/frontend/src/types/index.ts index 3f9c51a..709b795 100644 --- a/packages/frontend/src/types/index.ts +++ b/packages/frontend/src/types/index.ts @@ -266,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; } From e735654481ff6f39e46b920d24c3ff0d852cd2d0 Mon Sep 17 00:00:00 2001 From: Steven Gort Date: Thu, 11 Jun 2026 08:20:26 +0200 Subject: [PATCH 27/41] feat(dso): add historyTimeToLive so extracted DMN is deploy-ready MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This Operaton instance enforces camunda:historyTimeToLive at deploy. normalizeDmnForOperaton now declares the camunda namespace and stamps historyTimeToLive="180" (matching the LDE BPMN-template convention) on every , so the extracted DMN deploys as handed off — the CPSV Editor deploys it as-is, no patching. Decisions that already declare HTTL are left untouched. Verified: the normalized HoutopstandVellen DMN now deploys directly (all 7 decisions) and the root decision evaluates, with no external HTTL. --- dso-integration-phase-plan.md | 6 ++-- packages/backend/src/services/dso.service.ts | 35 ++++++++++++++++++-- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/dso-integration-phase-plan.md b/dso-integration-phase-plan.md index 4c1f8a4..4822341 100644 --- a/dso-integration-phase-plan.md +++ b/dso-integration-phase-plan.md @@ -70,7 +70,7 @@ Content: - `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, which Operaton refuses. "Extract DMN" returns the full `` element normalized for Operaton (DMN 1.3 + input ids — see Phase 4.1); the deployer still adds `camunda:historyTimeToLive`. +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) @@ -166,11 +166,11 @@ The questionnaire is extracted from `uitv:uitvoeringsregels`, not from the decis - ✅ **#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` (CPSV Editor):** this Operaton enforces HTTL at deploy. It is a deployment policy (TTL value is the org's choice), added by the deployer — the CPSV Editor — not by extraction. +- ✅ **#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+#2+#4+BIZ-004 (and #3 added at deploy) deploys 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. +- 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. diff --git a/packages/backend/src/services/dso.service.ts b/packages/backend/src/services/dso.service.ts index d524207..ceaa8ad 100644 --- a/packages/backend/src/services/dso.service.ts +++ b/packages/backend/src/services/dso.service.ts @@ -455,6 +455,31 @@ function sanitizeFeelNames(xml: string): string { 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 @@ -491,9 +516,10 @@ function ensureOutputTypeRefs(xml: string): string { * 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). - * - * NOT handled here: `camunda:historyTimeToLive` (a deployment policy, added by - * the deployer — the CPSV Editor — not by extraction). See the phase plan. + * 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). @@ -528,6 +554,9 @@ export function normalizeDmnForOperaton(dmn: string): string { // 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; } From 7133691988ee870ba7c0ce728ee9c1681262a6b0 Mon Sep 17 00:00:00 2001 From: Steven Gort Date: Thu, 11 Jun 2026 08:20:37 +0200 Subject: [PATCH 28/41] feat(form): green DSO badge for forms imported from DSO Forms imported via the DSO Applicable Rules panel now get status "dso" and render a green DSO badge in the Form Editor list, instead of the generic yellow WIP badge. form_schemas.status is unconstrained TEXT, so the new value persists fine; DSO forms stay editable like wip forms. Includes the v1.9.5 changelog entry (HTTL deploy-ready DMN + this badge). --- packages/frontend/src/changelog.json | 20 ++++++++++++++++++- .../components/DsoExplorer/DsoExplorer.tsx | 2 +- .../src/components/FormEditor/FormList.tsx | 5 +++++ packages/frontend/src/types/index.ts | 2 +- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/packages/frontend/src/changelog.json b/packages/frontend/src/changelog.json index 4a98d1d..c684eb5 100644 --- a/packages/frontend/src/changelog.json +++ b/packages/frontend/src/changelog.json @@ -1,10 +1,28 @@ { "versions": [ { - "version": "1.9.4", + "version": "1.9.5", "status": "Latest", "statusColor": "emerald", "borderColor": "emerald", + "date": "June 11, 2026", + "sections": [ + { + "icon": "⚖️", + "iconColor": "purple", + "title": "DSO Integration — deploy-ready DMN + form badge", + "items": [ + "Extracted DSO DMNs now carry camunda:historyTimeToLive, so they deploy to Operaton exactly as handed off — the consumer no longer has to patch the DMN before deploying. LDE now produces a fully deploy-ready and evaluatable DMN (DMN 1.3, input ids, FEEL-safe variable names, output typeRefs, history TTL).", + "Forms imported from DSO now show a green DSO badge in the Form Editor list, instead of the generic yellow WIP badge." + ] + } + ] + }, + { + "version": "1.9.4", + "status": "Released", + "statusColor": "slate", + "borderColor": "slate", "date": "June 10, 2026", "sections": [ { diff --git a/packages/frontend/src/components/DsoExplorer/DsoExplorer.tsx b/packages/frontend/src/components/DsoExplorer/DsoExplorer.tsx index cacd3d6..3588b41 100644 --- a/packages/frontend/src/components/DsoExplorer/DsoExplorer.tsx +++ b/packages/frontend/src/components/DsoExplorer/DsoExplorer.tsx @@ -668,7 +668,7 @@ const ApplicableRuleRow: React.FC<{ }, createdAt: now, updatedAt: now, - status: 'wip', + status: 'dso', language: 'nl', organization, }; 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/types/index.ts b/packages/frontend/src/types/index.ts index 3f9c51a..709b795 100644 --- a/packages/frontend/src/types/index.ts +++ b/packages/frontend/src/types/index.ts @@ -266,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; } From ab993ed5fdaa0f3e53252fc25dd2601c8da1d81a Mon Sep 17 00:00:00 2001 From: Steven Gort Date: Thu, 11 Jun 2026 12:12:26 +0200 Subject: [PATCH 29/41] @ test(shacl): add CPRMV/CPSV-AP/RONL validation fixtures + changelog Add valid/invalid SHACL fixtures across all three layers and a malformed Turtle case for the parser path: - cprmv-nonconformant.ttl (pairs with the renamed cprmv-conformant.ttl) - cpsv-ap-rule-nonconformant.ttl - org-collision-{pass,fail}.ttl - malformed.ttl Rename cprmv-test.ttl -> cprmv-conformant.ttl for a clear conformant/ nonconformant pair. Declare the cprmv key on the frontend ValidationResult layers type. Note the SHACL/CPRMV work under v1.9.5 in the changelog. Co-Authored-By: Claude Opus 4.8 @ --- .../{cprmv-test.ttl => cprmv-conformant.ttl} | 0 .../fixtures/shacl/cprmv-nonconformant.ttl | 27 +++++++++++++++++++ .../shacl/cpsv-ap-rule-nonconformant.ttl | 13 +++++++++ .../tests/fixtures/shacl/malformed.ttl | 11 ++++++++ .../fixtures/shacl/org-collision-fail.ttl | 20 ++++++++++++++ .../fixtures/shacl/org-collision-pass.ttl | 19 +++++++++++++ packages/frontend/src/changelog.json | 9 +++++++ .../src/components/ShaclValidator.tsx | 1 + 8 files changed, 100 insertions(+) rename packages/backend/tests/fixtures/shacl/{cprmv-test.ttl => cprmv-conformant.ttl} (100%) create mode 100644 packages/backend/tests/fixtures/shacl/cprmv-nonconformant.ttl create mode 100644 packages/backend/tests/fixtures/shacl/cpsv-ap-rule-nonconformant.ttl create mode 100644 packages/backend/tests/fixtures/shacl/malformed.ttl create mode 100644 packages/backend/tests/fixtures/shacl/org-collision-fail.ttl create mode 100644 packages/backend/tests/fixtures/shacl/org-collision-pass.ttl diff --git a/packages/backend/tests/fixtures/shacl/cprmv-test.ttl b/packages/backend/tests/fixtures/shacl/cprmv-conformant.ttl similarity index 100% rename from packages/backend/tests/fixtures/shacl/cprmv-test.ttl rename to packages/backend/tests/fixtures/shacl/cprmv-conformant.ttl 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-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/frontend/src/changelog.json b/packages/frontend/src/changelog.json index c684eb5..6cecc2a 100644 --- a/packages/frontend/src/changelog.json +++ b/packages/frontend/src/changelog.json @@ -15,6 +15,15 @@ "Extracted DSO DMNs now carry camunda:historyTimeToLive, so they deploy to Operaton exactly as handed off — the consumer no longer has to patch the DMN before deploying. LDE now produces a fully deploy-ready and evaluatable DMN (DMN 1.3, input ids, FEEL-safe variable names, output typeRefs, history TTL).", "Forms imported from DSO now show a green DSO badge in the Form Editor list, instead of the generic yellow WIP badge." ] + }, + { + "icon": "🧪", + "iconColor": "blue", + "title": "SHACL Validator — CPRMV layer + validation test fixtures", + "items": [ + "The SHACL validator now checks uploaded Turtle against a CPRMV 0.4.1 shape layer alongside CPSV-AP 3.2.0 and the RONL Custom rules; the results panel renders the new layer automatically.", + "Added valid/invalid test fixtures for every layer — CPRMV (cprmv-conformant.ttl / cprmv-nonconformant.ttl), CPSV-AP (cpsv-ap-conformant.ttl / cpsv-ap-rule-nonconformant.ttl), and RONL (rule- and org-collision pass/fail) — plus a malformed-Turtle case covering the parser path." + ] } ] }, diff --git a/packages/frontend/src/components/ShaclValidator.tsx b/packages/frontend/src/components/ShaclValidator.tsx index cd13fa1..62ecd97 100644 --- a/packages/frontend/src/components/ShaclValidator.tsx +++ b/packages/frontend/src/components/ShaclValidator.tsx @@ -51,6 +51,7 @@ interface ValidationResult { valid: boolean; parseError: string | null; layers: { + cprmv: LayerResult; 'cpsv-ap': LayerResult; 'ronl-custom': LayerResult; }; From eb9974bbad77d83d44dfcf962ff4af73c39b4a95 Mon Sep 17 00:00:00 2001 From: Steven Gort Date: Thu, 11 Jun 2026 12:15:14 +0200 Subject: [PATCH 30/41] chore: release v1.9.5 - SHACL Validator CPRMV layer --- packages/backend/src/services/shacl-validation.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/services/shacl-validation.service.ts b/packages/backend/src/services/shacl-validation.service.ts index 2579b6f..5282677 100644 --- a/packages/backend/src/services/shacl-validation.service.ts +++ b/packages/backend/src/services/shacl-validation.service.ts @@ -232,9 +232,9 @@ export class ShaclValidationService { private emptyLayers(): Record { return { - 'cprmv' : { label: 'CPRMV 0.4.1', loaded: false, issues: [] }, + 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: [] } + 'ronl-custom': { label: 'RONL Custom', loaded: false, issues: [] }, }; } From d47efe77ddb38f512320f8685b25b762a280425f Mon Sep 17 00:00:00 2001 From: Steven Gort Date: Sat, 13 Jun 2026 11:38:47 +0200 Subject: [PATCH 31/41] fix: CPRMV 0.4.1 namespace tolerance + test-data fill for falsy values (v1.9.6) - getAllDmns and chain-link queries only matched cprmv:DecisionModel under the old 0.3.0 namespace, so DMNs published under the new CPRMV 0.4.1 namespace (e.g. vast_bedrag_op_vestiging) were missing from /v1/dmns and the ChainBuilder DMN picker. Both namespaces are now supported side by side until existing 0.3.0 data is migrated. - ChainBuilder "Fill with test data" rendered Integer/Double inputs as empty when the RDF-sourced test value was 0 (0 || '' = ''), making it look like nothing was filled in. Now uses ?? so 0 displays correctly. --- .../backend/src/services/sparql.service.ts | 87 +++++++++++-------- packages/frontend/src/changelog.json | 20 ++++- .../src/components/ChainBuilder/InputForm.tsx | 4 +- packages/frontend/src/utils/constants.ts | 70 ++++++++------- 4 files changed, 110 insertions(+), 71 deletions(-) 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/frontend/src/changelog.json b/packages/frontend/src/changelog.json index 6cecc2a..dcb8dcd 100644 --- a/packages/frontend/src/changelog.json +++ b/packages/frontend/src/changelog.json @@ -1,10 +1,28 @@ { "versions": [ { - "version": "1.9.5", + "version": "1.9.6", "status": "Latest", "statusColor": "emerald", "borderColor": "emerald", + "date": "June 13, 2026", + "sections": [ + { + "icon": "🐛", + "iconColor": "red", + "title": "Bug Fixes", + "items": [ + "DMNs published under the new CPRMV 0.4.1 namespace (e.g. vast_bedrag_op_vestiging) were missing from /v1/dmns and the ChainBuilder DMN picker — getAllDmns and the chain-link queries only matched cprmv:DecisionModel under the old 0.3.0 namespace. Both namespaces are now supported side by side until existing 0.3.0 data is migrated.", + "Fixed 'Fill with test data' in the ChainBuilder input form not visibly filling Integer/Double fields when the RDF-sourced test value is 0 — the input rendered as empty (0 || '' = '') instead of showing 0." + ] + } + ] + }, + { + "version": "1.9.5", + "status": "Released", + "statusColor": "slate", + "borderColor": "slate", "date": "June 11, 2026", "sections": [ { diff --git a/packages/frontend/src/components/ChainBuilder/InputForm.tsx b/packages/frontend/src/components/ChainBuilder/InputForm.tsx index 3cf0762..a87b5ca 100644 --- a/packages/frontend/src/components/ChainBuilder/InputForm.tsx +++ b/packages/frontend/src/components/ChainBuilder/InputForm.tsx @@ -53,7 +53,7 @@ const InputForm: React.FC = ({ chain, inputs, onInputChange, val onInputChange(input.identifier, parseInt(e.target.value, 10) || 0)} placeholder={`Enter ${input.title.toLowerCase()}`} className="w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" @@ -65,7 +65,7 @@ const InputForm: React.FC = ({ chain, inputs, onInputChange, val onInputChange(input.identifier, parseFloat(e.target.value) || 0)} placeholder={`Enter ${input.title.toLowerCase()}`} className="w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" 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) = "") From ec5ba475f9d967ceccb08da341e356d0a88f9ed7 Mon Sep 17 00:00:00 2001 From: Steven Gort Date: Mon, 15 Jun 2026 09:22:36 +0200 Subject: [PATCH 32/41] Show full SHACL validator messages (v1.9.7) SHACL issue messages and focus-node locations were cut off with an ellipsis and could not be read in full. - frontend (ShaclValidator IssueRow): the location used a single-line CSS truncate. Location now wraps (break-all), message wraps (break-words), and both expose the complete text on hover via a title tooltip. - backend (shacl-validation.service offendingValues): removed the 60-char cap that truncated offending values reported for cardinality (maxCount / uniqueLang) violations; the full value is now included in the message (whitespace still normalized). - changelog: add v1.9.7 (bug fix), demote 1.9.6 to Released. --- .../src/services/shacl-validation.service.ts | 7 +++---- packages/frontend/src/changelog.json | 20 ++++++++++++++++++- .../src/components/ShaclValidator.tsx | 8 ++++++-- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/packages/backend/src/services/shacl-validation.service.ts b/packages/backend/src/services/shacl-validation.service.ts index 5282677..91c65bf 100644 --- a/packages/backend/src/services/shacl-validation.service.ts +++ b/packages/backend/src/services/shacl-validation.service.ts @@ -251,10 +251,9 @@ export class ShaclValidationService { if (!focusNode || !path || path.termType !== 'NamedNode') return []; const values: string[] = []; for (const quad of data.match(focusNode, path, null)) { - // Normalise whitespace and cap length — published literals (e.g. long - // multi-line rule descriptions) would otherwise bloat the issue message. - const normalised = quad.object.value.replace(/\s+/g, ' ').trim(); - values.push(normalised.length > 60 ? `${normalised.slice(0, 60)}…` : normalised); + // 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; } diff --git a/packages/frontend/src/changelog.json b/packages/frontend/src/changelog.json index dcb8dcd..17c5095 100644 --- a/packages/frontend/src/changelog.json +++ b/packages/frontend/src/changelog.json @@ -1,10 +1,28 @@ { "versions": [ { - "version": "1.9.6", + "version": "1.9.7", "status": "Latest", "statusColor": "emerald", "borderColor": "emerald", + "date": "June 15, 2026", + "sections": [ + { + "icon": "🐛", + "iconColor": "red", + "title": "Bug Fixes", + "items": [ + "SHACL Validator: long issue messages and focus-node locations were cut off with an ellipsis and could not be read — the location used a single-line CSS truncate. Messages and locations now wrap in full (break-words / break-all) and expose the complete text on hover (title tooltip).", + "SHACL backend: removed the 60-character cap on offending values reported for cardinality (maxCount / uniqueLang) violations, so the full value appears in the issue message instead of being truncated with an ellipsis." + ] + } + ] + }, + { + "version": "1.9.6", + "status": "Released", + "statusColor": "slate", + "borderColor": "slate", "date": "June 13, 2026", "sections": [ { diff --git a/packages/frontend/src/components/ShaclValidator.tsx b/packages/frontend/src/components/ShaclValidator.tsx index 62ecd97..e005723 100644 --- a/packages/frontend/src/components/ShaclValidator.tsx +++ b/packages/frontend/src/components/ShaclValidator.tsx @@ -93,9 +93,13 @@ function IssueRow({ issue }: { issue: ValidationIssue }) {

    {issue.code} - {issue.message} + + {issue.message} + {issue.location && ( - {issue.location} + + {issue.location} + )} {issue.line && ( From ffc2de01ec7cb0b84753ef499e69555f53200008 Mon Sep 17 00:00:00 2001 From: Steven Gort Date: Wed, 17 Jun 2026 10:42:15 +0200 Subject: [PATCH 33/41] feat(shacl): add ParameterWaardeShape and TemporalRuleShape to cprmv shapes Closes two coverage gaps identified in the CPSV Editor field-mapping audit: - cprmv:ParameterWaarde had no SHACL shape; Parameters tab was entirely unvalidated. New shape requires skos:notation [1,1] and skos:prefLabel [1,n]; all other properties (schema:value, unitCode, dct:description, cprmv:validFrom/validUntil) are optional [0,1]. - cprmv:TemporalRule had no SHACL shape; validFrom/validUntil/confidenceLevel/ isBasedOn extensions on Rules were unvalidated. New shape captures all four as optional [0,1] with correct datatypes. Also adds skos:, schema:, dct: prefix declarations required by the new shapes. Co-Authored-By: Claude Sonnet 4.6 --- .../shapes/cprmv/0.4.1/cprmv.shacl.ttl | 87 +++++++++++++++++-- 1 file changed, 81 insertions(+), 6 deletions(-) diff --git a/packages/backend/shapes/cprmv/0.4.1/cprmv.shacl.ttl b/packages/backend/shapes/cprmv/0.4.1/cprmv.shacl.ttl index 431d73a..108d924 100644 --- a/packages/backend/shapes/cprmv/0.4.1/cprmv.shacl.ttl +++ b/packages/backend/shapes/cprmv/0.4.1/cprmv.shacl.ttl @@ -1,9 +1,12 @@ -@prefix sh: . -@prefix rdf: . -@prefix rdfs: . -@prefix xsd: . -@prefix cpsv: . -@prefix cprmv: . +@prefix sh: . +@prefix rdf: . +@prefix rdfs: . +@prefix xsd: . +@prefix cpsv: . +@prefix cprmv: . +@prefix skos: . +@prefix schema: . +@prefix dct: . cprmv:RuleSetShape a sh:NodeShape ; @@ -207,3 +210,75 @@ 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 ; + ] . + From 57ef65a1f89e4da8e62f2765b95d588bdfb32481 Mon Sep 17 00:00:00 2001 From: Steven Gort Date: Wed, 17 Jun 2026 11:23:46 +0200 Subject: [PATCH 34/41] chore(release): bump LDE to v1.9.8 Co-Authored-By: Claude Sonnet 4.6 --- packages/frontend/src/changelog.json | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/frontend/src/changelog.json b/packages/frontend/src/changelog.json index 17c5095..66adf14 100644 --- a/packages/frontend/src/changelog.json +++ b/packages/frontend/src/changelog.json @@ -1,10 +1,29 @@ { "versions": [ { - "version": "1.9.7", + "version": "1.9.8", + "date": "June 17, 2026", "status": "Latest", "statusColor": "emerald", "borderColor": "emerald", + "sections": [ + { + "icon": "🛡️", + "iconColor": "emerald", + "title": "CPRMV SHACL — ParameterWaardeShape and TemporalRuleShape", + "items": [ + "Added cprmv:ParameterWaardeShape targeting cprmv:ParameterWaarde: skos:notation [1,1] xsd:string and skos:prefLabel [1,n] rdf:langString are mandatory; schema:value [0,1] xsd:decimal, schema:unitCode [0,1] xsd:string, dct:description [0,1] rdf:langString, and cprmv:validFrom/validUntil [0,1] xsd:date are optional. Previously cprmv:ParameterWaarde had no SHACL shape at all, so the entire Parameters section of an editor-generated TTL was unvalidated.", + "Added cprmv:TemporalRuleShape targeting cprmv:TemporalRule: cprmv:validFrom [0,1] xsd:date, cprmv:validUntil [0,1] xsd:date, cprmv:confidenceLevel [0,1] xsd:string, and cprmv:isBasedOn [0,n] sh:class cpsv:Rule — all optional. Previously the temporal extensions on cpsv:Rule (validity window, confidence level, extends) were unvalidated by any SHACL shape.", + "Also adds skos:, schema:, and dct: @prefix declarations required by the new shapes; no changes to existing shapes in the file." + ] + } + ] + }, + { + "version": "1.9.7", + "status": "Released", + "statusColor": "slate", + "borderColor": "slate", "date": "June 15, 2026", "sections": [ { From 38ee31c0586ba1ebca5b1e2662c97eabb6c5fdf1 Mon Sep 17 00:00:00 2001 From: Steven Gort Date: Mon, 22 Jun 2026 11:18:26 +0200 Subject: [PATCH 35/41] feat: tag deployed processes with owning board (boardOwner) deployProcess now stamps a process-level onto the main BPMN at deploy time. The value is taken from an explicit boardOwner request field, or derived from the process's candidate groups (infra/rip groups -> infra-board, caseworker/hr groups -> caseworker). Injection is idempotent and conservative: on any unexpected BPMN shape the original XML is deployed untouched, so existing bundles never break. Consumed downstream by ronl-business-api to split open tasks and the Projecten archive by board without a hardcoded process allowlist. Co-Authored-By: Claude Opus 4.8 --- packages/backend/src/routes/dmn.routes.ts | 6 +- .../backend/src/services/operaton.service.ts | 106 +++++++++++++++++- 2 files changed, 109 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/routes/dmn.routes.ts b/packages/backend/src/routes/dmn.routes.ts index e4cab4f..8b85c2a 100644 --- a/packages/backend/src/routes/dmn.routes.ts +++ b/packages/backend/src/routes/dmn.routes.ts @@ -236,6 +236,7 @@ router.post('/process/deploy', async (req: Request, res: Response) => { operatonUrl, operatonUsername, operatonPassword, + boardOwner, } = req.body as { bpmnXml: string; deploymentName: string; @@ -245,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()) { @@ -271,7 +274,8 @@ router.post('/process/deploy', async (req: Request, res: Response) => { documents, operatonUrl, operatonUsername, - operatonPassword + operatonPassword, + boardOwner ); res.json({ diff --git a/packages/backend/src/services/operaton.service.ts b/packages/backend/src/services/operaton.service.ts index 939ce9c..95b66fa 100644 --- a/packages/backend/src/services/operaton.service.ts +++ b/packages/backend/src/services/operaton.service.ts @@ -643,13 +643,21 @@ 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). + // Explicit boardOwner wins; otherwise it's derived from the candidate + // groups in the BPMN. Untaggable bundles are deployed verbatim. + const owner = boardOwner ?? this.deriveBoardOwner(bpmnXml); + 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 +677,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 +726,100 @@ 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(); From a2be64f1bda7133c87b290b13c477b6a3ab6e78e Mon Sep 17 00:00:00 2001 From: Steven Gort Date: Mon, 22 Jun 2026 12:23:58 +0200 Subject: [PATCH 36/41] feat: board-ownership picker in BPMN deploy modal Adds an isolated "Board ownership" rectangle above "Resources to deploy" with segmented buttons: Auto (shows the auto-detected board), Infra-board, Caseworker, None (leave untagged). Auto omits boardOwner so the backend derives it; an explicit board overrides; None sends '' to deploy untagged. Backend boardOwner semantics made explicit: undefined=derive, ''=untagged, value=override. Co-Authored-By: Claude Opus 4.8 --- .../backend/src/services/operaton.service.ts | 8 +- .../src/components/BpmnModeler/BpmnCanvas.tsx | 98 +++++++++++++++++++ 2 files changed, 103 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/services/operaton.service.ts b/packages/backend/src/services/operaton.service.ts index 95b66fa..ee3a14f 100644 --- a/packages/backend/src/services/operaton.service.ts +++ b/packages/backend/src/services/operaton.service.ts @@ -648,9 +648,11 @@ export class OperatonService { ): Promise<{ deploymentId: string; resourceCount: number }> { try { // Stamp the owning board onto the process definition (deploy-time tag). - // Explicit boardOwner wins; otherwise it's derived from the candidate - // groups in the BPMN. Untaggable bundles are deployed verbatim. - const owner = boardOwner ?? this.deriveBoardOwner(bpmnXml); + // 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', { diff --git a/packages/frontend/src/components/BpmnModeler/BpmnCanvas.tsx b/packages/frontend/src/components/BpmnModeler/BpmnCanvas.tsx index 33fad63..714e252 100644 --- a/packages/frontend/src/components/BpmnModeler/BpmnCanvas.tsx +++ b/packages/frontend/src/components/BpmnModeler/BpmnCanvas.tsx @@ -38,6 +38,38 @@ interface BpmnCanvasProps { onClose: () => void; } +/** User's board-ownership choice in the deploy modal. */ +type BoardChoice = 'auto' | 'infra-board' | 'caseworker' | 'none'; + +/** + * Candidate-group → board mapping, mirrored from the backend's deploy-time + * derivation. Used here only to show the auto-detected suggestion in the modal; + * the backend remains authoritative (on 'auto' the UI sends no boardOwner and + * the server re-derives). Keep in sync with operaton.service.ts BOARD_BY_GROUP. + */ +const 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 candidate groups in a BPMN, or null if unknown. */ +const deriveBoardOwnerFromXml = (xml: string): string | null => { + const groups = new Set(); + for (const m of xml.matchAll(/candidateGroups\s*=\s*["']([^"']+)["']/g)) { + for (const g of m[1].split(',')) groups.add(g.trim()); + } + let found: string | null = null; + for (const g of groups) { + for (const { match, board } of BOARD_BY_GROUP) { + if (match.test(g)) { + if (board === 'infra-board') return 'infra-board'; + found = board; + } + } + } + return found; +}; + const BpmnCanvas: React.FC = ({ xml, endpoint, @@ -73,6 +105,11 @@ const BpmnCanvas: React.FC = ({ const [operatonUsername, setOperatonUsername] = useState(''); const [operatonPassword, setOperatonPassword] = useState(''); + // Board-ownership picker (deploy modal). `boardChoice` is the user's selection; + // `boardAuto` is the auto-detected suggestion shown alongside the Auto option. + const [boardChoice, setBoardChoice] = useState('auto'); + const [boardAuto, setBoardAuto] = useState(null); + const [selectedElement, setSelectedElement] = useState(null); // Keep latest onElementSelect in a ref so handleElementSelect can stay stable @@ -458,6 +495,9 @@ const BpmnCanvas: React.FC = ({ languageMismatch?: boolean; languageList?: string[]; }); + // Pre-fill the board-ownership picker with the auto-detected board. + setBoardAuto(deriveBoardOwnerFromXml(xml)); + setBoardChoice('auto'); setDeployResult(null); setShowDeployModal(true); }; @@ -530,6 +570,11 @@ const BpmnCanvas: React.FC = ({ documents, subProcesses: subProcessXmls, operatonUrl: operatonUrl.trim() || undefined, + // Board-ownership tag: omit on 'auto' (backend derives), send '' to + // force untagged on 'none', otherwise send the chosen board. + ...(boardChoice !== 'auto' && { + boardOwner: boardChoice === 'none' ? '' : boardChoice, + }), }), }); @@ -687,6 +732,59 @@ const BpmnCanvas: 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' }, + { id: 'none', label: 'None (leave untagged)' }, + ] as { id: BoardChoice; label: string }[] + ).map((opt) => { + const active = boardChoice === opt.id; + return ( + + ); + })} +
    +
    + {boardChoice === 'auto' ? ( + 'The server tags the process from its candidate groups.' + ) : boardChoice === 'none' ? ( + 'The process is deployed without a boardOwner tag.' + ) : ( + <> + Tagging as {boardChoice} — overrides + auto-detection. + + )} +
    +
    + {/* Resources preview */}
    🚀 Resources to deploy
    From a88d81f3ac6ff67da732ed86ed35bc59dee895fc Mon Sep 17 00:00:00 2001 From: Steven Gort Date: Mon, 22 Jun 2026 12:37:15 +0200 Subject: [PATCH 37/41] feat: make boardOwner mandatory and persist it on the LDE record Deploy modal now requires a board: the "None (leave untagged)" option is gone, Deploy is disabled until a board resolves (auto-detected or picked), and the resolved board is sent on both the Operaton deploy and the record write-back. Persisted via a new process_definitions.board_owner column (markDeployed) and surfaced through /bundles/public so downstream consumers (ronl-business-api Procesbibliotheek) can display it. Co-Authored-By: Claude Opus 4.8 --- packages/backend/src/db/migrate.ts | 3 +- packages/backend/src/routes/assets.routes.ts | 4 +- .../backend/src/services/assets.service.ts | 12 +++-- .../src/components/BpmnModeler/BpmnCanvas.tsx | 48 ++++++++++++++----- 4 files changed, 50 insertions(+), 17 deletions(-) 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/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/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/frontend/src/components/BpmnModeler/BpmnCanvas.tsx b/packages/frontend/src/components/BpmnModeler/BpmnCanvas.tsx index 714e252..b5785e8 100644 --- a/packages/frontend/src/components/BpmnModeler/BpmnCanvas.tsx +++ b/packages/frontend/src/components/BpmnModeler/BpmnCanvas.tsx @@ -38,8 +38,8 @@ interface BpmnCanvasProps { onClose: () => void; } -/** User's board-ownership choice in the deploy modal. */ -type BoardChoice = 'auto' | 'infra-board' | 'caseworker' | 'none'; +/** User's board-ownership choice in the deploy modal. boardOwner is mandatory. */ +type BoardChoice = 'auto' | 'infra-board' | 'caseworker'; /** * Candidate-group → board mapping, mirrored from the backend's deploy-time @@ -504,6 +504,18 @@ const BpmnCanvas: React.FC = ({ const handleDeploy = async () => { if (!modelerRef.current) return; + + // boardOwner is mandatory. On 'auto' we send the auto-detected board so the + // Operaton tag and the persisted LDE record carry the same concrete value. + const boardOwner = boardChoice === 'auto' ? boardAuto : boardChoice; + if (!boardOwner) { + setDeployResult({ + success: false, + message: 'Select a board — boardOwner is required before deploying.', + }); + return; + } + setIsDeploying(true); try { @@ -570,11 +582,7 @@ const BpmnCanvas: React.FC = ({ documents, subProcesses: subProcessXmls, operatonUrl: operatonUrl.trim() || undefined, - // Board-ownership tag: omit on 'auto' (backend derives), send '' to - // force untagged on 'none', otherwise send the chosen board. - ...(boardChoice !== 'auto' && { - boardOwner: boardChoice === 'none' ? '' : boardChoice, - }), + boardOwner, }), }); @@ -594,6 +602,7 @@ const BpmnCanvas: React.FC = ({ operatonUrl: operatonUrl.trim() || undefined, formIds: forms.map((f) => f.id), documentIds: documents.map((d) => d.id), + boardOwner, }), }).catch((err) => console.warn('[BpmnCanvas] Deploy record update failed:', err)); } @@ -628,6 +637,10 @@ const BpmnCanvas: React.FC = ({ canvas.zoom('fit-viewport'); }; + // Resolved board the deploy will use (mandatory). null when 'auto' detects none + // and the user hasn't picked one — deploy is blocked until then. + const resolvedBoard = boardChoice === 'auto' ? boardAuto : boardChoice; + return (
    {/* Toolbar */} @@ -750,7 +763,6 @@ const BpmnCanvas: React.FC = ({ { id: 'auto', label: boardAuto ? `Auto (${boardAuto})` : 'Auto (none)' }, { id: 'infra-board', label: 'Infra-board' }, { id: 'caseworker', label: 'Caseworker' }, - { id: 'none', label: 'None (leave untagged)' }, ] as { id: BoardChoice; label: string }[] ).map((opt) => { const active = boardChoice === opt.id; @@ -773,9 +785,14 @@ const BpmnCanvas: React.FC = ({
    {boardChoice === 'auto' ? ( - 'The server tags the process from its candidate groups.' - ) : boardChoice === 'none' ? ( - 'The process is deployed without a boardOwner tag.' + boardAuto ? ( + <> + Auto-detected {boardAuto} from the + candidate groups. + + ) : ( + 'No board could be auto-detected — pick one to continue.' + ) ) : ( <> Tagging as {boardChoice} — overrides @@ -783,6 +800,13 @@ const BpmnCanvas: React.FC = ({ )}
    + {!resolvedBoard && ( +
    + ⚠️ A board owner is required. Select{' '} + Infra-board or{' '} + Caseworker before deploying. +
    + )}
    {/* Resources preview */} @@ -897,7 +921,7 @@ const BpmnCanvas: React.FC = ({