diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3390c9e54..ac32bdfd0 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -2,6 +2,7 @@ version: 2 updates: - package-ecosystem: "npm" directory: "/" + target-branch: "development" schedule: interval: "weekly" open-pull-requests-limit: 10 diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index e59116074..44244b1dc 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -38,7 +38,14 @@ jobs: # `npm ls` under the hood; `--package-lock-only` doesn't sidestep it). # No other Conduction app enables SBOM today. Tracked in #434. enable-sbom: false - enable-playwright: true - enable-playwright-coverage: true + # Playwright disabled until upstream @conduction/nextcloud-vue + # CnObjectDataWidget bundling bug is fixed: the published bundle has a + # hard-coded `require('../../store/index.js')` inside a soft try/catch + # that webpack can't resolve in consumer apps, so the procest bundle + # fails to build and every E2E spec then 404s. Tracked in + # ConductionNL/nextcloud-vue#242 — re-enable once a beta past that fix + # is pinned in package.json. + enable-playwright: false + enable-playwright-coverage: false playwright-coverage-threshold: 75 playwright-seed-command: 'php occ maintenance:repair' diff --git a/.gitignore b/.gitignore index 5c3bfecee..06e1af8a1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,53 @@ +# IDE +/.idea/ +/*.iml +*.Identifier + +# Dependencies +/vendor/ +/vendor-bin/*/vendor/ /node_modules/ + +# Build artifacts /js/ -/vendor/ + +# Documentation +/docs/node_modules/ +/docs/build/ +/docs/.docusaurus/ /docusaurus/node_modules/ /docusaurus/build/ /docusaurus/.docusaurus/ -docs/.docusaurus/ -docs/node_modules/ -# Test/build artifacts +# Testing & Quality +/.php-cs-fixer.cache +/tests/.phpunit.cache +/.phpunit.cache/ .phpunit.cache .phpunit.cache/ .phpunit.result.cache +/coverage/ coverage/ +/coverage-frontend/ +/phpmetrics/ phpmetrics/ +/quality-reports/ +/phpqa/ # Playwright output (paths align with the shared quality.yml artifact uploads) /test-results/ /playwright-report/ + +# Nextcloud +/custom_apps/ +/config/ + +# OS +.DS_Store +Thumbs.db + +# Claude Code +.claude/worktrees/ + +# Repo-specific +tests/e2e/.auth/ diff --git a/appinfo/routes.php b/appinfo/routes.php index cf4d0c876..0a73c4dd4 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -8,7 +8,7 @@ * @category Routes * @package OCA\Procest * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/docs/package-lock.json b/docs/package-lock.json index c676862c6..3659736ae 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -2041,9 +2041,9 @@ } }, "node_modules/@conduction/docusaurus-preset": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@conduction/docusaurus-preset/-/docusaurus-preset-3.7.0.tgz", - "integrity": "sha512-AS6YyXf0NxsTNgxX101+ca0WYxCBRXiKiSpeokWYaIHSluDozsVSs3hH0iwjYZlDyA58RuQm7EydrRh+CD9Vlw==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@conduction/docusaurus-preset/-/docusaurus-preset-3.8.0.tgz", + "integrity": "sha512-aakDh5eyzA4qMhExiU+gOCVLviB/ol8UMulFHiFKz5lKGEf5AKPR7NDuE6rnBpS5qakGyiCwgLSojP3HnBROCA==", "license": "EUPL-1.2", "bin": { "validate-ai-baseline": "bin/validate-ai-baseline.mjs" diff --git a/img/app-store.svg b/img/app-store.svg index 1db2f0edc..75cc85680 100644 --- a/img/app-store.svg +++ b/img/app-store.svg @@ -1,5 +1,5 @@ - + diff --git a/l10n/en.js b/l10n/en.js index 7bba2ba0d..0bf4acbc2 100644 --- a/l10n/en.js +++ b/l10n/en.js @@ -20,6 +20,7 @@ OC.L10N.register( "Add note" : "Add note", "All" : "All", "All case types" : "All case types", + "All cases active" : "All cases active", "All caught up!" : "All caught up!", "All your items are completed" : "All your items are completed", "Are you sure you want to delete this case?" : "Are you sure you want to delete this case?", @@ -45,12 +46,10 @@ OC.L10N.register( "Case Type" : "Case Type", "Case Type Management" : "Case Type Management", "Case Types" : "Case Types", - "Case created with type \" : "Case created with type \", "Case handler" : "Case handler", "Case schema" : "Case schema", "Case sensitive" : "Case sensitive", "Case type" : "Case type", - "Case type \" : "Case type \", "Case type has expired (valid until {date})" : "Case type has expired (valid until {date})", "Case type is not yet valid (valid from {date})" : "Case type is not yet valid (valid from {date})", "Case type is required" : "Case type is required", @@ -189,6 +188,7 @@ OC.L10N.register( "No result recorded yet" : "No result recorded yet", "No settings available yet" : "No settings available yet", "No status types defined. Add at least one to publish this case type." : "No status types defined. Add at least one to publish this case type.", + "No task reminders" : "No task reminders", "No tasks found" : "No tasks found", "No tasks yet" : "No tasks yet", "No widgets configured" : "No widgets configured", @@ -258,6 +258,7 @@ OC.L10N.register( "Role type" : "Role type", "Save" : "Save", "Save the case type first before adding status types." : "Save the case type first before adding status types.", + "SLA Compliance" : "SLA Compliance", "Saved successfully" : "Saved successfully", "Schema ID" : "Schema ID", "Secret" : "Secret", @@ -279,8 +280,6 @@ OC.L10N.register( "Started" : "Started", "Status" : "Status", "Status Timeline" : "Status Timeline", - "Status changed from \" : "Status changed from \", - "Status changed to \" : "Status changed to \", "Status schema" : "Status schema", "Status type" : "Status type", "Status type name is required" : "Status type name is required", @@ -325,6 +324,7 @@ OC.L10N.register( "Valid until" : "Valid until", "Value Mappings (enum translations)" : "Value Mappings (enum translations)", "View all activity" : "View all activity", + "View all deadline alerts" : "View all deadline alerts", "View all my work" : "View all my work", "View all overdue" : "View all overdue", "View case" : "View case", diff --git a/l10n/en.json b/l10n/en.json index dd817e9fd..b457839c7 100644 --- a/l10n/en.json +++ b/l10n/en.json @@ -21,6 +21,7 @@ "Add note": "Add note", "All": "All", "All case types": "All case types", + "All cases active": "All cases active", "All caught up!": "All caught up!", "All your items are completed": "All your items are completed", "Advice": "Advice", @@ -51,12 +52,10 @@ "Case Type": "Case Type", "Case Type Management": "Case Type Management", "Case Types": "Case Types", - "Case created with type \\": "Case created with type \\", "Case handler": "Case handler", "Case schema": "Case schema", "Case sensitive": "Case sensitive", "Case type": "Case type", - "Case type \\": "Case type \\", "Case type has expired (valid until {date})": "Case type has expired (valid until {date})", "Case type is not yet valid (valid from {date})": "Case type is not yet valid (valid from {date})", "Case type is required": "Case type is required", @@ -196,6 +195,7 @@ "No result recorded yet": "No result recorded yet", "No settings available yet": "No settings available yet", "No status types defined. Add at least one to publish this case type.": "No status types defined. Add at least one to publish this case type.", + "No task reminders": "No task reminders", "No tasks found": "No tasks found", "No tasks yet": "No tasks yet", "No actions recorded yet": "No actions recorded yet", @@ -267,6 +267,7 @@ "Role type": "Role type", "Save": "Save", "Save the case type first before adding status types.": "Save the case type first before adding status types.", + "SLA Compliance": "SLA Compliance", "Saved successfully": "Saved successfully", "Schema ID": "Schema ID", "Secret": "Secret", @@ -288,8 +289,6 @@ "Started": "Started", "Status": "Status", "Status Timeline": "Status Timeline", - "Status changed from \\": "Status changed from \\", - "Status changed to \\": "Status changed to \\", "Status schema": "Status schema", "Status type": "Status type", "Status type name is required": "Status type name is required", @@ -334,6 +333,7 @@ "Valid until": "Valid until", "Value Mappings (enum translations)": "Value Mappings (enum translations)", "View all activity": "View all activity", + "View all deadline alerts": "View all deadline alerts", "View all my work": "View all my work", "View all overdue": "View all overdue", "View case": "View case", diff --git a/l10n/nl.js b/l10n/nl.js index 703c239e1..cf6893247 100644 --- a/l10n/nl.js +++ b/l10n/nl.js @@ -20,6 +20,7 @@ OC.L10N.register( "Add note" : "Notitie toevoegen", "All" : "Alle", "All case types" : "Alle zaaktypen", + "All cases active" : "Alle zaken actief", "All caught up!" : "Alles bijgewerkt!", "All your items are completed" : "Al uw items zijn afgerond", "Are you sure you want to delete this case?" : "Weet u zeker dat u deze zaak wilt verwijderen?", @@ -45,12 +46,10 @@ OC.L10N.register( "Case Type" : "Zaaktype", "Case Type Management" : "Zaaktype beheer", "Case Types" : "Zaaktypen", - "Case created with type \" : "Zaak aangemaakt met type \", "Case handler" : "Behandelaar", "Case schema" : "Zaak schema", "Case sensitive" : "Hoofdlettergevoelig", "Case type" : "Zaaktype", - "Case type \" : "Zaaktype \", "Case type has expired (valid until {date})" : "Zaaktype is verlopen (geldig tot {date})", "Case type is not yet valid (valid from {date})" : "Zaaktype is nog niet geldig (geldig vanaf {date})", "Case type is required" : "Zaaktype is verplicht", @@ -189,6 +188,7 @@ OC.L10N.register( "No result recorded yet" : "Nog geen resultaat geregistreerd", "No settings available yet" : "Nog geen instellingen beschikbaar", "No status types defined. Add at least one to publish this case type." : "Geen statustypen gedefinieerd. Voeg er ten minste één toe om dit zaaktype te publiceren.", + "No task reminders" : "Geen taakherinneringen", "No tasks found" : "Geen taken gevonden", "No tasks yet" : "Nog geen taken", "No widgets configured" : "Geen widgets geconfigureerd", @@ -258,6 +258,7 @@ OC.L10N.register( "Role type" : "Roltype", "Save" : "Opslaan", "Save the case type first before adding status types." : "Sla het zaaktype eerst op voordat u statustypen toevoegt.", + "SLA Compliance" : "SLA-naleving", "Saved successfully" : "Succesvol opgeslagen", "Schema ID" : "Schema ID", "Secret" : "Geheim", @@ -279,8 +280,6 @@ OC.L10N.register( "Started" : "Gestart", "Status" : "Status", "Status Timeline" : "Status tijdlijn", - "Status changed from \" : "Status gewijzigd van \", - "Status changed to \" : "Status gewijzigd naar \", "Status schema" : "Status schema", "Status type" : "Statustype", "Status type name is required" : "Statustype naam is verplicht", @@ -325,6 +324,7 @@ OC.L10N.register( "Valid until" : "Geldig tot", "Value Mappings (enum translations)" : "Waarde mappings (enum vertalingen)", "View all activity" : "Alle activiteit bekijken", + "View all deadline alerts" : "Alle deadlines bekijken", "View all my work" : "Al mijn werk bekijken", "View all overdue" : "Alle openstaande bekijken", "View case" : "Bekijk zaak", diff --git a/l10n/nl.json b/l10n/nl.json index 72be59fc1..e58916094 100644 --- a/l10n/nl.json +++ b/l10n/nl.json @@ -19,6 +19,7 @@ "Add note": "Notitie toevoegen", "All": "Alle", "All case types": "Alle zaaktypen", + "All cases active": "Alle zaken actief", "All caught up!": "Alles bijgewerkt!", "All your items are completed": "Al uw items zijn afgerond", "Are you sure you want to delete this case?": "Weet u zeker dat u deze zaak wilt verwijderen?", @@ -44,12 +45,10 @@ "Case Type": "Zaaktype", "Case Type Management": "Zaaktype beheer", "Case Types": "Zaaktypen", - "Case created with type \\": "Zaak aangemaakt met type \\", "Case handler": "Behandelaar", "Case schema": "Zaak schema", "Case sensitive": "Hoofdlettergevoelig", "Case type": "Zaaktype", - "Case type \\": "Zaaktype \\", "Case type has expired (valid until {date})": "Zaaktype is verlopen (geldig tot {date})", "Case type is not yet valid (valid from {date})": "Zaaktype is nog niet geldig (geldig vanaf {date})", "Case type is required": "Zaaktype is verplicht", @@ -188,6 +187,7 @@ "No result recorded yet": "Nog geen resultaat geregistreerd", "No settings available yet": "Nog geen instellingen beschikbaar", "No status types defined. Add at least one to publish this case type.": "Geen statustypen gedefinieerd. Voeg er ten minste één toe om dit zaaktype te publiceren.", + "No task reminders": "Geen taakherinneringen", "No tasks found": "Geen taken gevonden", "No tasks yet": "Nog geen taken", "No widgets configured": "Geen widgets geconfigureerd", @@ -257,6 +257,7 @@ "Role type": "Roltype", "Save": "Opslaan", "Save the case type first before adding status types.": "Sla het zaaktype eerst op voordat u statustypen toevoegt.", + "SLA Compliance": "SLA-naleving", "Saved successfully": "Succesvol opgeslagen", "Schema ID": "Schema ID", "Secret": "Geheim", @@ -278,8 +279,6 @@ "Started": "Gestart", "Status": "Status", "Status Timeline": "Status tijdlijn", - "Status changed from \\": "Status gewijzigd van \\", - "Status changed to \\": "Status gewijzigd naar \\", "Status schema": "Status schema", "Status type": "Statustype", "Status type name is required": "Statustype naam is verplicht", @@ -324,6 +323,7 @@ "Valid until": "Geldig tot", "Value Mappings (enum translations)": "Waarde mappings (enum vertalingen)", "View all activity": "Alle activiteit bekijken", + "View all deadline alerts": "Alle deadlines bekijken", "View all my work": "Al mijn werk bekijken", "View all overdue": "Alle openstaande bekijken", "View case": "Bekijk zaak", diff --git a/lib/Controller/CaseDefinitionController.php b/lib/Controller/CaseDefinitionController.php index dd9f487a8..586b5ce65 100644 --- a/lib/Controller/CaseDefinitionController.php +++ b/lib/Controller/CaseDefinitionController.php @@ -9,7 +9,7 @@ * @category Controller * @package OCA\Procest\Controller * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Controller/ConsultationController.php b/lib/Controller/ConsultationController.php index f3cbf9cd6..ec974f167 100644 --- a/lib/Controller/ConsultationController.php +++ b/lib/Controller/ConsultationController.php @@ -8,7 +8,7 @@ * @category Controller * @package OCA\Procest\Controller * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Controller/EmailController.php b/lib/Controller/EmailController.php index 0c079f008..ade000398 100644 --- a/lib/Controller/EmailController.php +++ b/lib/Controller/EmailController.php @@ -8,7 +8,7 @@ * @category Controller * @package OCA\Procest\Controller * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Controller/InspectionController.php b/lib/Controller/InspectionController.php index 8bcad5cbe..181414000 100644 --- a/lib/Controller/InspectionController.php +++ b/lib/Controller/InspectionController.php @@ -9,7 +9,7 @@ * @category Controller * @package OCA\Procest\Controller * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Controller/KpiController.php b/lib/Controller/KpiController.php index 1add0ff59..a3a190190 100644 --- a/lib/Controller/KpiController.php +++ b/lib/Controller/KpiController.php @@ -10,7 +10,7 @@ * @category Controller * @package OCA\Procest\Controller * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Controller/LegesController.php b/lib/Controller/LegesController.php index c63fbaa94..11133fd6a 100644 --- a/lib/Controller/LegesController.php +++ b/lib/Controller/LegesController.php @@ -9,7 +9,7 @@ * @category Controller * @package OCA\Procest\Controller * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Controller/LhsController.php b/lib/Controller/LhsController.php index 47235b4cd..a4283902d 100644 --- a/lib/Controller/LhsController.php +++ b/lib/Controller/LhsController.php @@ -12,7 +12,7 @@ * @category Controller * @package OCA\Procest\Controller * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Controller/MetricsController.php b/lib/Controller/MetricsController.php index 2b2c17c94..635f88bcb 100644 --- a/lib/Controller/MetricsController.php +++ b/lib/Controller/MetricsController.php @@ -8,7 +8,7 @@ * @category Controller * @package OCA\Procest\Controller * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Controller/MilestoneController.php b/lib/Controller/MilestoneController.php index 857d72b16..bf83a9d06 100644 --- a/lib/Controller/MilestoneController.php +++ b/lib/Controller/MilestoneController.php @@ -8,7 +8,7 @@ * @category Controller * @package OCA\Procest\Controller * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Controller/ParafeerRouteController.php b/lib/Controller/ParafeerRouteController.php index 2af9ca613..c3e5c9339 100644 --- a/lib/Controller/ParafeerRouteController.php +++ b/lib/Controller/ParafeerRouteController.php @@ -12,7 +12,7 @@ * @category Controller * @package OCA\Procest\Controller * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Controller/ParaferingController.php b/lib/Controller/ParaferingController.php index 684315029..5e26fd1b1 100644 --- a/lib/Controller/ParaferingController.php +++ b/lib/Controller/ParaferingController.php @@ -9,7 +9,7 @@ * @category Controller * @package OCA\Procest\Controller * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Controller/RoutingController.php b/lib/Controller/RoutingController.php index f8cdc542e..a0c64f7ff 100644 --- a/lib/Controller/RoutingController.php +++ b/lib/Controller/RoutingController.php @@ -11,7 +11,7 @@ * @category Controller * @package OCA\Procest\Controller * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Controller/StufController.php b/lib/Controller/StufController.php index fd9c3ba1a..c1ba5d602 100644 --- a/lib/Controller/StufController.php +++ b/lib/Controller/StufController.php @@ -10,7 +10,7 @@ * @category Controller * @package OCA\Procest\Controller * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Controller/TemplateController.php b/lib/Controller/TemplateController.php index a658ab09f..8d3cac328 100644 --- a/lib/Controller/TemplateController.php +++ b/lib/Controller/TemplateController.php @@ -8,7 +8,7 @@ * @category Controller * @package OCA\Procest\Controller * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Listener/ChecklistRunImmutabilityListener.php b/lib/Listener/ChecklistRunImmutabilityListener.php index e82c8f1cb..685e302cb 100644 --- a/lib/Listener/ChecklistRunImmutabilityListener.php +++ b/lib/Listener/ChecklistRunImmutabilityListener.php @@ -16,7 +16,7 @@ * @category Listener * @package OCA\Procest\Listener * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Listener/KpiCacheInvalidationListener.php b/lib/Listener/KpiCacheInvalidationListener.php index 50e5ee362..b870be0f5 100644 --- a/lib/Listener/KpiCacheInvalidationListener.php +++ b/lib/Listener/KpiCacheInvalidationListener.php @@ -14,7 +14,7 @@ * @category Listener * @package OCA\Procest\Listener * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Listener/RoleMutationListener.php b/lib/Listener/RoleMutationListener.php index 5960d016f..87f367e47 100644 --- a/lib/Listener/RoleMutationListener.php +++ b/lib/Listener/RoleMutationListener.php @@ -15,7 +15,7 @@ * @category Listener * @package OCA\Procest\Listener * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Service/AiService.php b/lib/Service/AiService.php index c21e96523..3fa597234 100644 --- a/lib/Service/AiService.php +++ b/lib/Service/AiService.php @@ -10,7 +10,7 @@ * @category Service * @package OCA\Procest\Service * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Service/CaseDefinitionExportService.php b/lib/Service/CaseDefinitionExportService.php index 3c1713b37..8dbc7dabc 100644 --- a/lib/Service/CaseDefinitionExportService.php +++ b/lib/Service/CaseDefinitionExportService.php @@ -9,7 +9,7 @@ * @category Service * @package OCA\Procest\Service * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Service/CaseDefinitionImportService.php b/lib/Service/CaseDefinitionImportService.php index 89be71624..c233dd7b5 100644 --- a/lib/Service/CaseDefinitionImportService.php +++ b/lib/Service/CaseDefinitionImportService.php @@ -9,7 +9,7 @@ * @category Service * @package OCA\Procest\Service * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Service/CaseEmailService.php b/lib/Service/CaseEmailService.php index e9e8c92f6..288476dbb 100644 --- a/lib/Service/CaseEmailService.php +++ b/lib/Service/CaseEmailService.php @@ -10,7 +10,7 @@ * @category Service * @package OCA\Procest\Service * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Service/ChecklistService.php b/lib/Service/ChecklistService.php index 446dcb50b..021a0031d 100644 --- a/lib/Service/ChecklistService.php +++ b/lib/Service/ChecklistService.php @@ -9,7 +9,7 @@ * @category Service * @package OCA\Procest\Service * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Service/ConsultationService.php b/lib/Service/ConsultationService.php index 6496daf15..12f2b0fdb 100644 --- a/lib/Service/ConsultationService.php +++ b/lib/Service/ConsultationService.php @@ -10,7 +10,7 @@ * @category Service * @package OCA\Procest\Service * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Service/DsoIntakeService.php b/lib/Service/DsoIntakeService.php index e9d19c011..b8453a3e5 100644 --- a/lib/Service/DsoIntakeService.php +++ b/lib/Service/DsoIntakeService.php @@ -9,7 +9,7 @@ * @category Service * @package OCA\Procest\Service * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Service/Inspection/ChecklistService.php b/lib/Service/Inspection/ChecklistService.php index cabedb95a..1a3a7748f 100644 --- a/lib/Service/Inspection/ChecklistService.php +++ b/lib/Service/Inspection/ChecklistService.php @@ -20,7 +20,7 @@ * @category Service * @package OCA\Procest\Service\Inspection * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Service/KpiAggregationService.php b/lib/Service/KpiAggregationService.php index ed294fcb1..d6a94949a 100644 --- a/lib/Service/KpiAggregationService.php +++ b/lib/Service/KpiAggregationService.php @@ -19,7 +19,7 @@ * @category Service * @package OCA\Procest\Service * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Service/LegesCalculationService.php b/lib/Service/LegesCalculationService.php index d7b8ffd1f..765358474 100644 --- a/lib/Service/LegesCalculationService.php +++ b/lib/Service/LegesCalculationService.php @@ -10,7 +10,7 @@ * @category Service * @package OCA\Procest\Service * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Service/LegesExportService.php b/lib/Service/LegesExportService.php index daf6943b1..e4e590bf0 100644 --- a/lib/Service/LegesExportService.php +++ b/lib/Service/LegesExportService.php @@ -9,7 +9,7 @@ * @category Service * @package OCA\Procest\Service * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Service/MilestoneService.php b/lib/Service/MilestoneService.php index 27a56cb96..ebd665164 100644 --- a/lib/Service/MilestoneService.php +++ b/lib/Service/MilestoneService.php @@ -9,7 +9,7 @@ * @category Service * @package OCA\Procest\Service * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Service/ParafeerRouteService.php b/lib/Service/ParafeerRouteService.php index 1ccc06f13..5120ad809 100644 --- a/lib/Service/ParafeerRouteService.php +++ b/lib/Service/ParafeerRouteService.php @@ -15,7 +15,7 @@ * @category Service * @package OCA\Procest\Service * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Service/ParaferingService.php b/lib/Service/ParaferingService.php index b53a7c5ff..0809a22e2 100644 --- a/lib/Service/ParaferingService.php +++ b/lib/Service/ParaferingService.php @@ -10,7 +10,7 @@ * @category Service * @package OCA\Procest\Service * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Service/RoleResolverService.php b/lib/Service/RoleResolverService.php index 85c1c9908..0069eafb8 100644 --- a/lib/Service/RoleResolverService.php +++ b/lib/Service/RoleResolverService.php @@ -17,7 +17,7 @@ * @category Service * @package OCA\Procest\Service * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Service/Routing/RoutingStrategyInterface.php b/lib/Service/Routing/RoutingStrategyInterface.php index ea6611ed3..d00b9f699 100644 --- a/lib/Service/Routing/RoutingStrategyInterface.php +++ b/lib/Service/Routing/RoutingStrategyInterface.php @@ -11,7 +11,7 @@ * @category Service * @package OCA\Procest\Service\Routing * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Service/Routing/RoutingStrategyMissingException.php b/lib/Service/Routing/RoutingStrategyMissingException.php index 3f792ccb9..0ec09df3e 100644 --- a/lib/Service/Routing/RoutingStrategyMissingException.php +++ b/lib/Service/Routing/RoutingStrategyMissingException.php @@ -11,7 +11,7 @@ * @category Service * @package OCA\Procest\Service\Routing * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Service/Routing/Strategy/HierarchicalStrategy.php b/lib/Service/Routing/Strategy/HierarchicalStrategy.php index e710b8185..2f6c9d945 100644 --- a/lib/Service/Routing/Strategy/HierarchicalStrategy.php +++ b/lib/Service/Routing/Strategy/HierarchicalStrategy.php @@ -10,7 +10,7 @@ * @category Service * @package OCA\Procest\Service\Routing\Strategy * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Service/Routing/Strategy/LeastLoadedStrategy.php b/lib/Service/Routing/Strategy/LeastLoadedStrategy.php index 084106f0d..9302a4657 100644 --- a/lib/Service/Routing/Strategy/LeastLoadedStrategy.php +++ b/lib/Service/Routing/Strategy/LeastLoadedStrategy.php @@ -11,7 +11,7 @@ * @category Service * @package OCA\Procest\Service\Routing\Strategy * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Service/Routing/Strategy/OrSetStrategy.php b/lib/Service/Routing/Strategy/OrSetStrategy.php index 5503573b5..0f5e02e42 100644 --- a/lib/Service/Routing/Strategy/OrSetStrategy.php +++ b/lib/Service/Routing/Strategy/OrSetStrategy.php @@ -10,7 +10,7 @@ * @category Service * @package OCA\Procest\Service\Routing\Strategy * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Service/Routing/Strategy/RoundRobinStrategy.php b/lib/Service/Routing/Strategy/RoundRobinStrategy.php index 856dd74ae..bacd59c4e 100644 --- a/lib/Service/Routing/Strategy/RoundRobinStrategy.php +++ b/lib/Service/Routing/Strategy/RoundRobinStrategy.php @@ -11,7 +11,7 @@ * @category Service * @package OCA\Procest\Service\Routing\Strategy * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Service/Routing/Strategy/SingleRoleStrategy.php b/lib/Service/Routing/Strategy/SingleRoleStrategy.php index 71f9f9841..ac20d9344 100644 --- a/lib/Service/Routing/Strategy/SingleRoleStrategy.php +++ b/lib/Service/Routing/Strategy/SingleRoleStrategy.php @@ -10,7 +10,7 @@ * @category Service * @package OCA\Procest\Service\Routing\Strategy * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Service/Routing/StrategyRegistry.php b/lib/Service/Routing/StrategyRegistry.php index fda8e4693..125245696 100644 --- a/lib/Service/Routing/StrategyRegistry.php +++ b/lib/Service/Routing/StrategyRegistry.php @@ -11,7 +11,7 @@ * @category Service * @package OCA\Procest\Service\Routing * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Service/StufFieldMappingService.php b/lib/Service/StufFieldMappingService.php index f2ee6ba0a..167937ab5 100644 --- a/lib/Service/StufFieldMappingService.php +++ b/lib/Service/StufFieldMappingService.php @@ -10,7 +10,7 @@ * @category Service * @package OCA\Procest\Service * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Service/StufMessageBuilder.php b/lib/Service/StufMessageBuilder.php index 2870283fc..5621fc8af 100644 --- a/lib/Service/StufMessageBuilder.php +++ b/lib/Service/StufMessageBuilder.php @@ -9,7 +9,7 @@ * @category Service * @package OCA\Procest\Service * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Service/Vth/LhsRecommendationService.php b/lib/Service/Vth/LhsRecommendationService.php index 2e989af09..586ff0856 100644 --- a/lib/Service/Vth/LhsRecommendationService.php +++ b/lib/Service/Vth/LhsRecommendationService.php @@ -17,7 +17,7 @@ * @category Service * @package OCA\Procest\Service\Vth * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Service/ZgwZrcRulesService.php b/lib/Service/ZgwZrcRulesService.php index adcac1bfc..0156892e4 100644 --- a/lib/Service/ZgwZrcRulesService.php +++ b/lib/Service/ZgwZrcRulesService.php @@ -8,7 +8,7 @@ * @category Service * @package OCA\Procest\Service * - * @author Conduction Development Team + * @author Conduction Development Team * @copyright 2024 Conduction B.V. * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * diff --git a/lib/Settings/procest_register.json b/lib/Settings/procest_register.json index e822aed56..7a0622318 100644 --- a/lib/Settings/procest_register.json +++ b/lib/Settings/procest_register.json @@ -3,7 +3,7 @@ "info": { "title": "Procest Case Management Register", "description": "Register containing all schemas for the Procest case management application. Defines case types, status types, role types, result types, decision types, document types, property definitions, voorstel, parafeerroute, parafeeractie, automaticAction, and their instance counterparts.", - "version": "0.7.0" + "version": "0.8.0" }, "x-openregister": { "type": "application", @@ -749,7 +749,7 @@ "task": { "slug": "task", "icon": "CheckboxMarkedOutline", - "version": "1.0.0", + "version": "1.1.0", "x-schema-org-type": "schema:Action", "x-zgw-equivalent": "Taak", "title": "Task", @@ -831,6 +831,49 @@ "description": "JSON-encoded array of checklist items ({id, label, checked})", "visible": false } + }, + "configuration": { + "x-openregister-lifecycle": { + "field": "status", + "initial": "available", + "final": [ + "completed", + "terminated", + "disabled" + ], + "transitions": { + "activate": { + "from": [ + "available" + ], + "to": "active", + "description": "Pick up the task." + }, + "complete": { + "from": [ + "active" + ], + "to": "completed", + "description": "Mark task as completed." + }, + "terminate": { + "from": [ + "available", + "active" + ], + "to": "terminated", + "description": "Terminate the task." + }, + "disable": { + "from": [ + "available", + "active" + ], + "to": "disabled", + "description": "Disable the task." + } + } + } } }, "role": { @@ -1083,7 +1126,7 @@ "document": { "slug": "document", "icon": "FileDocumentOutline", - "version": "1.0.0", + "version": "1.1.0", "x-schema-org-type": "schema:DigitalDocument", "x-zgw-equivalent": "EnkelvoudigInformatieObject", "title": "Document", @@ -1193,6 +1236,45 @@ "default": null, "description": "Indicates whether usage rights have been set for this document" } + }, + "configuration": { + "x-openregister-lifecycle": { + "field": "status", + "initial": "in_bewerking", + "final": [ + "gearchiveerd" + ], + "transitions": { + "submit": { + "from": [ + "in_bewerking" + ], + "to": "ter_vaststelling", + "description": "Submit the document for adoption." + }, + "adopt": { + "from": [ + "ter_vaststelling" + ], + "to": "definitief", + "description": "Adopt the document as final." + }, + "archive": { + "from": [ + "definitief" + ], + "to": "gearchiveerd", + "description": "Archive the final document." + }, + "sendBack": { + "from": [ + "ter_vaststelling" + ], + "to": "in_bewerking", + "description": "Send back for revisions." + } + } + } } }, "documentLink": { @@ -1640,7 +1722,7 @@ "voorstel": { "slug": "voorstel", "icon": "FileDocumentEditOutline", - "version": "1.0.0", + "version": "1.1.0", "x-schema-org-type": "schema:CreativeWork", "title": "Voorstel", "description": "A B&W voorstel (proposal) for decision-making in a case", @@ -1752,6 +1834,77 @@ "description": "Reference to the linked decision (set when besluit is registered)", "visible": false } + }, + "configuration": { + "x-openregister-lifecycle": { + "field": "status", + "initial": "concept", + "final": [ + "besloten", + "gearchiveerd" + ], + "transitions": { + "startParafering": { + "from": [ + "concept" + ], + "to": "in_parafering", + "description": "Start initialing route." + }, + "paraferingDone": { + "from": [ + "in_parafering" + ], + "to": "ter_accordering", + "description": "Initialing complete; ready for accordance." + }, + "accord": { + "from": [ + "ter_accordering" + ], + "to": "geaccordeerd", + "description": "Accord the proposal." + }, + "submit": { + "from": [ + "geaccordeerd" + ], + "to": "aangeboden", + "description": "Submit the proposal for decision." + }, + "decide": { + "from": [ + "aangeboden" + ], + "to": "besloten", + "description": "Decision made on the proposal." + }, + "archive": { + "from": [ + "besloten" + ], + "to": "gearchiveerd", + "description": "Archive the proposal." + }, + "sendBack": { + "from": [ + "in_parafering", + "ter_accordering", + "geaccordeerd", + "aangeboden" + ], + "to": "teruggestuurd", + "description": "Send the proposal back for revisions." + }, + "revise": { + "from": [ + "teruggestuurd" + ], + "to": "concept", + "description": "Take a returned proposal back into draft." + } + } + } } }, "parafeerroute": { @@ -2340,7 +2493,7 @@ "hearingSession": { "slug": "hearingSession", "icon": "AccountGroupOutline", - "version": "1.1.0", + "version": "1.2.0", "x-schema-org-type": "schema:Event", "x-zgw-equivalent": "Hoorzitting", "title": "Hearing Session", @@ -2479,6 +2632,52 @@ }, "description": "Append-only audit entries tagged with applicable Awb article (awb-art-7:2|7:3|7:4|7:6|7:7|7:13 or avg-art-6)" } + }, + "configuration": { + "x-openregister-lifecycle": { + "field": "status", + "initial": "gepland", + "final": [ + "uitgevoerd", + "geannuleerd", + "afgezien" + ], + "transitions": { + "invite": { + "from": [ + "gepland" + ], + "to": "uitgenodigd", + "description": "Send hearing invitations." + }, + "execute": { + "from": [ + "uitgenodigd", + "dossier_beschikbaar" + ], + "to": "uitgevoerd", + "description": "Mark the hearing as carried out." + }, + "cancel": { + "from": [ + "gepland", + "uitgenodigd", + "dossier_beschikbaar" + ], + "to": "geannuleerd", + "description": "Cancel the hearing." + }, + "waive": { + "from": [ + "gepland", + "uitgenodigd", + "dossier_beschikbaar" + ], + "to": "afgezien", + "description": "Waive the hearing." + } + } + } } }, "advisoryReport": { @@ -3234,7 +3433,7 @@ "adviesAanvraag": { "slug": "adviesAanvraag", "icon": "CommentQuestionOutline", - "version": "1.0.0", + "version": "1.1.0", "x-schema-org-type": "schema:AskAction", "title": "Advice Request", "description": "A request for internal or external advice on a case, with deadline tracking", @@ -3299,6 +3498,32 @@ "type": "string", "description": "Specific questions for the adviseur" } + }, + "configuration": { + "x-openregister-lifecycle": { + "field": "status", + "initial": "aangevraagd", + "final": [ + "ontvangen", + "verlopen" + ], + "transitions": { + "receive": { + "from": [ + "aangevraagd" + ], + "to": "ontvangen", + "description": "Mark the advice as received." + }, + "expire": { + "from": [ + "aangevraagd" + ], + "to": "verlopen", + "description": "Mark the advice request as expired." + } + } + } } }, "legesverordening": { @@ -3602,7 +3827,7 @@ "handhavingsactie": { "slug": "handhavingsactie", "icon": "Gavel", - "version": "1.0.0", + "version": "1.1.0", "x-schema-org-type": "schema:LegalForceStatus", "title": "Enforcement Action", "description": "An enforcement action (handhavingsactie) on a case, classified per the Landelijke Handhavingsstrategie (LHS)", @@ -3685,6 +3910,40 @@ "type": "string", "description": "Documented reasoning if the LHS suggestion was overridden" } + }, + "configuration": { + "x-openregister-lifecycle": { + "field": "status", + "initial": "opgelegd", + "final": [ + "geeffectueerd", + "ingetrokken" + ], + "transitions": { + "forfeit": { + "from": [ + "opgelegd" + ], + "to": "verbeurd", + "description": "Mark the enforcement action as forfeited." + }, + "execute": { + "from": [ + "verbeurd" + ], + "to": "geeffectueerd", + "description": "Mark the enforcement action as executed." + }, + "withdraw": { + "from": [ + "opgelegd", + "verbeurd" + ], + "to": "ingetrokken", + "description": "Withdraw the enforcement action." + } + } + } } }, "lhsMatrix": { @@ -3880,7 +4139,7 @@ "inspectieChecklist": { "slug": "inspectieChecklist", "icon": "ClipboardCheckOutline", - "version": "1.0.0", + "version": "1.1.0", "x-schema-org-type": "schema:HowTo", "title": "Inspection Checklist", "description": "Configurable inspection checklist template linked to a case type, with versioning support", @@ -3964,6 +4223,31 @@ } } } + }, + "configuration": { + "x-openregister-lifecycle": { + "field": "status", + "initial": "draft", + "final": [ + "archived" + ], + "transitions": { + "activate": { + "from": [ + "draft" + ], + "to": "active", + "description": "Activate the checklist." + }, + "archive": { + "from": [ + "active" + ], + "to": "archived", + "description": "Archive the checklist." + } + } + } } }, "inspectieRapport": { diff --git a/openspec/changes/brp-kvk-register-sets/proposal.md b/openspec/changes/brp-kvk-register-sets/proposal.md new file mode 100644 index 000000000..a0d727739 --- /dev/null +++ b/openspec/changes/brp-kvk-register-sets/proposal.md @@ -0,0 +1,48 @@ +## Why + +When creating a zaak (case), the initiator ("indiener") must be linked to a real person or organization. Currently, cases have no structured initiator data connected to base registries like BRP (Basisregistratie Personen) or KVK (Kamer van Koophandel). Users cannot search or select an initiator from authoritative sources during case creation. This is a core GEMMA Zaakafhandel requirement -- every zaak must have a "rol: initiator" linked to a betrokkene (involved party). Without BRP/KVK integration, Procest cannot demonstrate realistic case handling workflows. + +## What Changes + +- **REQ-BRP-001**: Create a BRP person register schema in OpenRegister with fields for BSN, name, address, and birthdate +- **REQ-BRP-002**: Seed 10 test persons into the BRP register for development and demo purposes +- **REQ-KVK-001**: Create a KVK company register schema in OpenRegister with fields for KVK number, trade name, legal form, and address +- **REQ-KVK-002**: Seed 10 test companies into the KVK register for development and demo purposes +- **REQ-INIT-001**: Add initiator type selector to case creation UI (Person / Company / Contact) +- **REQ-INIT-002**: Implement cross-source search across BRP, KVK, and Nextcloud Contacts with unified results +- **REQ-INIT-003**: Store selected initiator reference on the case (type + source ID) +- **REQ-INIT-004**: Display initiator details on the case detail view + +## Capabilities + +### New Capabilities +- `brp-register`: BRP person register schema and test seed data in OpenRegister +- `kvk-register`: KVK company register schema and test seed data in OpenRegister +- `initiator-selection`: Unified initiator search and selection across BRP, KVK, and Nextcloud Contacts during case creation +- `initiator-display`: Initiator details shown on case detail view with link to source record + +### Modified Capabilities +- `procest-case-creation`: CaseCreateDialog extended with initiator type selector and cross-source search +- `procest-case-detail`: Case info panel shows initiator name, type, and source details +- `procest-register`: Case schema extended with initiator reference fields (type, sourceId, sourceName) + +## Standards + +- **GEMMA Zaakafhandel**: Rol "initiator" is a required betrokkene on every zaak +- **ZGW ZRC**: Rol betrokkene model (natuurlijk persoon / niet-natuurlijk persoon) +- **Haal Centraal BRP Personen API**: Field naming conventions for person data (burgerservicenummer, naam, verblijfplaats, geboorte) +- **KVK Zoeken API**: Field naming conventions for company data (kvkNummer, handelsnaam, rechtsvorm, adres) + +## Impact + +- **OpenRegister**: New register JSON schemas for BRP persons and KVK companies with seed data in `components.objects` +- **Frontend**: `CaseCreateDialog.vue` -- add initiator type tabs and search component +- **Frontend**: `CaseDetail.vue` -- display initiator info section +- **Backend**: Case schema in `procest_register.json` -- add `initiatorType`, `initiatorSourceId`, `initiatorDisplayName` fields +- **Dependencies**: OpenRegister (register schemas and seed data), Nextcloud Contacts API (DAV/CardDAV search) + +## Risks + +- BRP and KVK schemas are simplified test versions based on API field naming conventions, not actual registry connections. Real BRP/KVK integration would require Haal Centraal and KVK API credentials and compliance. +- Nextcloud Contacts search depends on the Contacts app being installed and populated. +- Cross-source search performance may need optimization if registers grow large. diff --git a/openspec/changes/case-email-integration/builds/build.json b/openspec/changes/case-email-integration/builds/build.json new file mode 100644 index 000000000..0a5e76921 --- /dev/null +++ b/openspec/changes/case-email-integration/builds/build.json @@ -0,0 +1,17 @@ +{ + "phase": "build", + "timestamp": "2026-05-19T03:05:29.017246+00:00", + "pr": 456, + "commit": "6ec9b8b", + "turns": 41, + "cost_usd": 1.473, + "tokens": { + "input": 4000, + "output": 23171, + "cache_read": 2714976, + "cache_create": 79454 + }, + "model": "claude-sonnet-4-6", + "maxed_out": true, + "cost_eur": 1.3552 +} \ No newline at end of file diff --git a/openspec/changes/case-email-integration/pipeline-logs/build.jsonl.gz b/openspec/changes/case-email-integration/pipeline-logs/build.jsonl.gz new file mode 100644 index 000000000..53788981b Binary files /dev/null and b/openspec/changes/case-email-integration/pipeline-logs/build.jsonl.gz differ diff --git a/openspec/changes/docs-product-pages-conformance/applier.json b/openspec/changes/docs-product-pages-conformance/applier.json new file mode 100644 index 000000000..0d7b46bf8 --- /dev/null +++ b/openspec/changes/docs-product-pages-conformance/applier.json @@ -0,0 +1,11 @@ +{ + "ran": true, + "pass": true, + "blocking": [], + "turns": 5, + "cost_usd": 0.2454, + "cost_eur": 0.2258, + "pr": 482, + "commit": "14450e2", + "timestamp": "2026-05-19T04:02:01.355907+00:00" +} \ No newline at end of file diff --git a/openspec/changes/docs-product-pages-conformance/pipeline-logs/applier.jsonl.gz b/openspec/changes/docs-product-pages-conformance/pipeline-logs/applier.jsonl.gz new file mode 100644 index 000000000..8c8a09d52 Binary files /dev/null and b/openspec/changes/docs-product-pages-conformance/pipeline-logs/applier.jsonl.gz differ diff --git a/openspec/changes/integration-maps/README.md b/openspec/changes/integration-maps/README.md new file mode 100644 index 000000000..4b7d5e65f --- /dev/null +++ b/openspec/changes/integration-maps/README.md @@ -0,0 +1,7 @@ +# integration-maps (cross-repo stub) + +Authoritative spec: [ConductionNL/openregister → openspec/changes/integration-maps](https://github.com/ConductionNL/openregister/tree/development/openspec/changes/integration-maps). + +Tracking issue: https://github.com/ConductionNL/openregister/issues/1316 + +Stub exists so Hydra's sibling-based dependency check can resolve cross-repo `depends_on`. diff --git a/openspec/changes/integration-maps/hydra.json b/openspec/changes/integration-maps/hydra.json new file mode 100644 index 000000000..7d852dd8f --- /dev/null +++ b/openspec/changes/integration-maps/hydra.json @@ -0,0 +1,12 @@ +{ + "schema_version": 2, + "spec_slug": "integration-maps", + "app": "openregister", + "repo": "https://github.com/ConductionNL/openregister", + "issue": "https://github.com/ConductionNL/openregister/issues/1316", + "depends_on": [], + "cross_repo_stub": true, + "note": "Cross-repo dependency stub. Authoritative spec lives in ConductionNL/openregister. Do NOT edit here.", + "pipeline": {}, + "cycles": [] +} diff --git a/openspec/changes/migrate-parafering-to-or-approval-workflow/.openspec.yaml b/openspec/changes/migrate-parafering-to-or-approval-workflow/.openspec.yaml new file mode 100644 index 000000000..81cd71fe0 --- /dev/null +++ b/openspec/changes/migrate-parafering-to-or-approval-workflow/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-11 diff --git a/openspec/changes/migrate-parafering-to-or-approval-workflow/design.md b/openspec/changes/migrate-parafering-to-or-approval-workflow/design.md new file mode 100644 index 000000000..a948f4485 --- /dev/null +++ b/openspec/changes/migrate-parafering-to-or-approval-workflow/design.md @@ -0,0 +1,120 @@ +# Design: migrate-parafering-to-or-approval-workflow + +## Context + +OR's `approval-workflow` spec provides `ApprovalChain` CRUD and `ApprovalStep` decision +endpoints. The exact PHP DI class for approval-chain CRUD is to be confirmed during task +OR-1.1 in the umbrella change; this design refers to it as `ApprovalWorkflowService` +(or the concrete mapper/service discovered during that task). The spec intentionally uses +the OR REST API endpoint surface as the stable reference; if the DI class is unavailable, +procest MAY call OR's REST API from the backend via an HTTP client. + +## File-by-File Mapping + +### `lib/Controller/ParaferingController.php` — endpoint surface unchanged + +The controller's public API is preserved in full. All existing routes, request parameters, +and response shapes stay the same so that callers (including the procest frontend) require +no changes. + +The controller stops managing step state directly. It delegates to `ParaferingService` +for all chain and step operations. No business logic moves into the controller; it becomes +a thin adapter between the HTTP layer and `ParaferingService`. + +### `lib/Service/ParaferingService.php` — rewrite to delegate to OR ApprovalChain + +`ParaferingService` is rewritten to translate parafeerroute concepts into OR's ApprovalChain +model: + +| Existing operation | New implementation | +|---|---| +| Create parafeerroute (chain) | `POST /api/approval-chains` (or OR DI class) with steps derived from the route definition | +| Get parafeerroute status | `GET /api/approval-chains/{id}/objects` | +| Advance step on parafering | `POST /api/approval-steps/{id}/approve` with optional comment | +| Return voorstel (terugsturen) | `POST /api/approval-steps/{id}/reject` with mandatory comment | +| Skip step | `POST /api/approval-steps/{id}/approve` with `_meta.action: skipped` in JSON comment | +| Delegate parafering | `POST /api/approval-steps/{id}/approve` with `_meta.actorType: delegate`, `_meta.onBehalfOf`, `_meta.mandate` in JSON comment | +| Advisory step | `POST /api/approval-steps/{id}/approve` with `_meta.action: advised`, `_meta.advice` in JSON comment | + +App-specific parafering semantics (delegation actorType, mandate reference, advisory text, +skipped reason) are encoded in the `comment` field as a JSON object with `text` (human-readable) +and `_meta` (machine-readable structured fields). This is the metadata-in-comment pattern +defined in the umbrella design. + +The `_parafeerRouteId` stored on the voorstel object is updated to store the OR `ApprovalChain` +UUID rather than a local parafeerroute UUID. Existing parafeerroute UUIDs in legacy rows are +left as-is (frozen, read-only). + +### `lib/Service/ParaferingNotificationService.php` — keep; update event source + +Notifications are a procest concern and remain in procest. The service is updated to listen +on OR's `ApprovalStep` state-change events (`ApprovalStepUpdated` dispatched by OR via +Nextcloud's `IEventDispatcher`) instead of parafeer-local events. + +OR dispatches `ApprovalStepApprovedEvent` and `ApprovalStepRejectedEvent` after each step +state change (see `openregister/openspec/changes/add-approval-step-events`). The service +registers `IEventListener` implementations on these two event classes; polling is no longer +required. + +The notification payload (actor display name, step label, voorstel title) is unchanged from +the user perspective. + +### `lib/Settings/procest_register.json` — deprecate parafeerroute schema + +The `Parafeerroute` schema in `procest_register.json` is marked deprecated by adding +`"deprecated": true` and `"deprecatedSince": ""` fields to the schema +object. The schema is NOT deleted — existing rows remain readable via the OR API until sunset +(one major release after the migration ships). + +New code MUST NOT create `Parafeerroute` objects. The repair step is updated to skip +`Parafeerroute` schema registration on new installs after migration. + +## Concept Mapping Reference + +| Parafeerroute concept | OR ApprovalChain equivalent | +|---|---| +| Parafeerroute (named route) | `ApprovalChain.name` | +| Parafeerder/adviseur per step | `ApprovalStep.role` = NC group ID | +| Step order | `ApprovalStep.order` | +| `pending` (active step) | `ApprovalStep.status: pending` | +| `waiting` (not yet active) | `ApprovalStep.status: waiting` | +| Paraferen | `POST .../approve` | +| Terugsturen | `POST .../reject` | +| Advance-on-parafering | OR's automatic advance-on-approval | +| Comment/reden | `comment` plain string or `{"text": "..."}` | +| actorType / onBehalfOf / mandate | `comment._meta.actorType` / `.onBehalfOf` / `.mandate` | +| Advisory text | `comment._meta.action: "advised"`, `comment._meta.advice` | +| Skip reason | `comment._meta.action: "skipped"`, `comment.text` | + +## DEFERRED_QUESTIONS + +1. **OR DI class name**: confirm whether `OCA\OpenRegister\Service\ApprovalChainService` or + `OCA\OpenRegister\Db\ApprovalChainMapper` is the correct DI entry point for ApprovalChain + CRUD from a PHP app (resolved during umbrella task OR-1.1 before `opsx-apply` starts). +2. **OR ApprovalStep IEventDispatcher event**: RESOLVED — OR dispatches + `OCA\OpenRegister\Event\ApprovalStepApprovedEvent` and + `OCA\OpenRegister\Event\ApprovalStepRejectedEvent` after each step state change, defined + in `openregister/openspec/changes/add-approval-step-events`. Polling is not required; + `ParaferingNotificationService` registers as an `IEventListener` on both event classes. + +## Seed Data + +No new schemas are introduced in OR. The `Parafeerroute` schema is deprecated (not deleted) +in `procest_register.json`. The only data-layer change is the deprecation annotation on +that schema. + +Existing `Parafeerroute` rows in OR are frozen and remain accessible read-only until the +sunset release removes the schema entirely. + +## Related ADRs + +- **ADR-022** (primary) — apps consume OR abstractions; approval-chain is the specific + abstraction this migration delegates to. +- **ADR-031** — schema-declarative business logic; marking the schema deprecated is the + correct pattern (deprecation annotation in the register JSON, not a code-side guard). +- **ADR-008** — testing contract; end-to-end test exercising OR's approval-workflow store + is required. +- **Umbrella spec** — `hydra/openspec/changes/consume-or-approval-workflow-fleet-wide` + (policy contract this migration satisfies). +- **OR approval-workflow spec** — `openregister/openspec/specs/approval-workflow/spec.md` + (the API this migration consumes). diff --git a/openspec/changes/migrate-parafering-to-or-approval-workflow/proposal.md b/openspec/changes/migrate-parafering-to-or-approval-workflow/proposal.md new file mode 100644 index 000000000..6e3acb955 --- /dev/null +++ b/openspec/changes/migrate-parafering-to-or-approval-workflow/proposal.md @@ -0,0 +1,84 @@ +# Proposal: migrate-parafering-to-or-approval-workflow + +## Why + +Procest ships a full step-routing engine for parafering (signing routes): `ParaferingController`, +`ParaferingService`, and `ParaferingNotificationService` implement ordered steps, role-gated +decisions, advance-on-approval logic, and a `Parafeerroute` schema persisted in OR. This is +a bespoke approval-chain engine. + +OpenRegister has shipped `approval-workflow` (status: implemented). It provides exactly the +same abstraction: named chains with ordered steps, each step bound to a Nextcloud group, +`pending`/`waiting`/`approved`/`rejected` state transitions, automatic advance-on-approval, +full decision history, and a REST API. + +Maintaining a parallel approval-chain engine in procest violates **ADR-022** (Apps Consume +OpenRegister Abstractions). The concrete harms: + +- **Duplicate role-gating logic**: procest re-implements `IUserSession` group-membership checks + that OR's `approval-workflow` already provides and tests. +- **Drift risk**: the parafeerroute step engine evolves independently — edge cases (delegation, + skip, resubmit) accumulate divergent handling. +- **No cross-app approval queries**: a manager cannot ask "all pending steps across apps" + without a single OR approval store. +- **Orphaned code**: three service classes (`ParaferingService`, `ParaferingNotificationService`, + `ParaferingController`) that replicate what OR provides — maintenance surface with no + architectural benefit. + +## What + +This spec migrates procest's parafering implementation to use OR's `approval-workflow` API as +the chain-state backend, while fully preserving the existing procest API surface for callers: + +1. `ParaferingService` is rewritten to create and manage `ApprovalChain` objects in OR via OR's + approval-workflow API (or the equivalent OR DI class), translating parafeerroute concepts + into ApprovalChain terms. +2. `ParaferingNotificationService` is updated to listen on OR's `ApprovalStep` events instead of + parafeer-local events. +3. `ParaferingController` endpoint surface is **unchanged** — callers continue to use procest's + parafering endpoints; the controller internally delegates to the rewritten `ParaferingService`. +4. The `Parafeerroute` schema in `procest_register.json` is marked deprecated; no new rows are + written after migration; existing rows remain readable until sunset. + +The existing procest specs (`parafering-actions`, `parafering-audit-trail`, `role-based-step-routing`) +remain as the consumer-facing contract surface and are not modified. The migration is +server-side only. + +## Capabilities + +### New Capabilities + +- `parafering-via-or-approval`: Parafering chains are now backed by OR's `ApprovalChain` entity, + giving procest access to OR's role enforcement, advance-on-approval, decision history, and + cross-app approval queries for free. + +### Modified Capabilities + +- `parafering-actions` (spec: `procest/openspec/specs/parafering-actions/spec.md`) — + consumer-facing contract unchanged; implementation now routes through OR's approval-workflow API. +- `role-based-step-routing` (spec: `procest/openspec/specs/role-based-step-routing/spec.md`) — + step-role enforcement is now performed by OR's `approval-workflow` role check; the spec + surface for callers is unchanged. + +## Affected Projects + +- [x] Project: `procest` — all implementation tasks are in this repo +- [x] Project: `openregister` — no code change; verify DI class for ApprovalChain CRUD (tracked + in umbrella OR-1.1) + +## Out of Scope + +- Procest's parafering UX/frontend: the migration is server-side only. +- Parafering audit trail migration (covered by `migrate-parafering-to-or-audit`). +- Modifying OR's `approval-workflow` spec or API. +- Historical backfill of existing `Parafeerroute` rows into OR's ApprovalChain tables. +- Modifying `parafering-actions`, `parafering-audit-trail`, or `role-based-step-routing` specs. + +## Success Criteria + +- `openspec validate --strict migrate-parafering-to-or-approval-workflow` exits 0. +- `GET /api/approval-chains` returns procest parafering chains after migration. +- Existing procest parafering API tests pass without modification. +- No new `Parafeerroute` objects are created in OR after the migration ships. +- `ParaferingService`, `ParaferingController`, and `ParaferingNotificationService` contain + no bespoke step-routing state-machine code. diff --git a/openspec/changes/migrate-parafering-to-or-approval-workflow/specs/parafering-via-or-approval/spec.md b/openspec/changes/migrate-parafering-to-or-approval-workflow/specs/parafering-via-or-approval/spec.md new file mode 100644 index 000000000..15d7710c7 --- /dev/null +++ b/openspec/changes/migrate-parafering-to-or-approval-workflow/specs/parafering-via-or-approval/spec.md @@ -0,0 +1,139 @@ +# parafering-via-or-approval Specification + +--- +status: proposed +--- + +## Purpose + +Define the procest-side contract for routing parafering (signing-route) chains through +OR's `approval-workflow` capability. Procest's consumer-facing API surface is preserved; +the server-side implementation delegates to OR's ApprovalChain and ApprovalStep entities. + +## ADDED Requirements + +### Requirement: Parafering Initiation Creates an OR ApprovalChain + +SHALL be the requirement that when a voorstel is submitted for parafering, procest creates +an OR `ApprovalChain` with one step per parafeerder/adviseur in the route. No new +`Parafeerroute` rows are created after this migration ships. + +#### Scenario: Voorstel submission creates OR ApprovalChain + +- GIVEN a voorstel object with UUID `voorstel-abc` stored in a procest OR register +- AND a parafeerroute configured with three parafeerders in order: teamleider, afdelingshoofd, directeur +- WHEN the steller submits the voorstel for parafering +- THEN an `ApprovalChain` SHALL be created in OR with three steps (order 1, 2, 3) +- AND each step `role` SHALL be the NC group ID for the respective parafeerder/role +- AND the chain SHALL be accessible via `GET /api/approval-chains` +- AND no new `Parafeerroute` object SHALL be created in OR + +#### Scenario: Existing parafeerroute rows remain read-only + +- GIVEN a legacy `Parafeerroute` object with UUID `legacy-route-001` exists in OR +- WHEN procest code runs after migration +- THEN the legacy object SHALL remain readable via `GET /api/objects/{register}/{schema}/legacy-route-001` +- AND procest SHALL NOT create new `Parafeerroute` objects for any new voorstel submissions + +--- + +### Requirement: Step Transitions Emit Via OR's Approval-Workflow API + +SHALL be the requirement that all parafering step decisions (paraferen, terugsturen, adviseren, +overslaan) are emitted through OR's approval-step decision endpoints. + +#### Scenario: Paraferen advances via OR approve endpoint + +- GIVEN an ApprovalStep with `status: pending` for `voorstel-abc` +- AND the requesting user is a member of the step's `role` group +- WHEN the parafeerder clicks "Paraferen" +- THEN procest SHALL call `POST /api/approval-steps/{id}/approve` (or equivalent OR DI class) +- AND OR SHALL set `status: approved` and advance the next waiting step to `pending` +- AND the response from procest's parafering endpoint SHALL reflect the updated state + +#### Scenario: Terugsturen emits via OR reject endpoint + +- GIVEN an ApprovalStep with `status: pending` for `voorstel-abc` +- WHEN the parafeerder clicks "Terugsturen" with comment "Financiele paragraaf ontbreekt" +- THEN procest SHALL call `POST /api/approval-steps/{id}/reject` with the comment +- AND OR SHALL set `status: rejected` with `decidedAt` and `decidedBy` +- AND no next step SHALL be advanced + +#### Scenario: Advisory step uses approve endpoint with meta + +- GIVEN an ApprovalStep for an advisory step with `status: pending` +- WHEN the adviseur submits advice text +- THEN procest SHALL call `POST /api/approval-steps/{id}/approve` with a JSON comment + containing `{"text": "", "_meta": {"action": "advised", "advice": ""}}` +- AND OR SHALL advance the next waiting step + +--- + +### Requirement: Notifications Observe OR ApprovalStep Events + +MUST be the requirement that `ParaferingNotificationService` listens on OR's ApprovalStep +state changes to determine when to notify the next parafeerder, rather than operating on +parafeer-local events. The notification payload (actor name, step label, voorstel title) +is unchanged from the user perspective. + +#### Scenario: Next parafeerder notified after step approval + +- GIVEN ApprovalStep order-1 for `voorstel-abc` is approved by the teamleider +- AND OR advances ApprovalStep order-2 to `pending` +- WHEN OR dispatches an `ApprovalStepApprovedEvent` +- THEN `ParaferingNotificationService` SHALL send a Nextcloud notification to the NC user(s) + in the group bound to step order-2 +- AND the notification text SHALL identify the voorstel and the requesting step + +#### Scenario: Steller notified on terugsturen + +- GIVEN ApprovalStep order-2 for `voorstel-abc` is rejected with comment "Paragraaf ontbreekt" +- WHEN OR dispatches an `ApprovalStepRejectedEvent` +- THEN `ParaferingNotificationService` SHALL notify the voorstel's steller +- AND the notification SHALL include the rejecting actor's name and the comment text + +--- + +### Requirement: No New Parafeerroute Rows After Migration + +MUST NOT be violated: after this migration ships, no code path in procest creates new +`Parafeerroute` objects in OR. The schema is deprecated. All new parafering chains are +OR `ApprovalChain` objects. + +#### Scenario: Procest code does not write to deprecated schema + +- GIVEN the migration is deployed +- WHEN any procest endpoint is called that initiates or advances a parafering flow +- THEN no OR object of schema type `Parafeerroute` SHALL be created or updated +- AND the OR object store for `Parafeerroute` SHALL contain only pre-migration rows + +--- + +### Requirement: Existing Parafeerroute Rows Remain Readable Until Sunset + +SHALL be the requirement that existing `Parafeerroute` rows written before the migration +are preserved read-only and accessible via the OR API until the schema is sunset (one major +procest release after migration). No historical backfill into OR ApprovalChain tables occurs. + +#### Scenario: Legacy parafeerroute readable via OR API + +- GIVEN `Parafeerroute` objects exist from before the migration +- WHEN `GET /api/objects/{register}/{schema}` is called for the parafeerroute schema +- THEN all pre-migration objects SHALL be returned with `200 OK` +- AND no write operations (POST, PUT, PATCH) SHALL succeed on the deprecated schema + +--- + +### Requirement: End-to-End Test Exercises OR Approval-Workflow Store + +MUST be the requirement that the procest test suite includes at least one end-to-end test +that creates a parafering chain via procest's API and verifies the chain and step records +exist in OR's approval-workflow store. + +#### Scenario: E2E parafering test uses OR approval store + +- GIVEN the test environment has OR's approval-workflow enabled +- WHEN the E2E test submits a voorstel for parafering and approves all steps +- THEN the test SHALL assert that `GET /api/approval-chains` returns the chain +- AND the test SHALL assert that all steps have `status: approved` in OR's approval tables +- AND the test SHALL NOT assert against any procest-local `Parafeerroute` table diff --git a/openspec/changes/migrate-parafering-to-or-approval-workflow/tasks.md b/openspec/changes/migrate-parafering-to-or-approval-workflow/tasks.md new file mode 100644 index 000000000..bc054d63d --- /dev/null +++ b/openspec/changes/migrate-parafering-to-or-approval-workflow/tasks.md @@ -0,0 +1,140 @@ +› **Scope note (2026-05-11):** see investigation block below. Clean delegation requires a coordinated rewrite of `ParaferingController` + `ParaferingService` + `ParaferingNotificationService` + the frontend that does not fit a single PR. This commit records the umbrella rule + migration plan; implementation lands as a follow-up sequence. + +# Tasks: migrate-parafering-to-or-approval-workflow + +All tasks are in the `procest` repo. Each task includes an estimate (S = half-day, +M = 1–2 days, L = 3+ days). + +> **Investigation (2026-05-11):** `ParaferingService` works on array-shaped +> voorstel objects and never sees the underlying `ObjectEntity`. OR's +> `ApprovalService::initializeChain/approveStep/rejectStep` needs an +> `ObjectEntity`, which only the controller layer can resolve at persistence +> time. Migration therefore requires the four-step sequence below; each step +> would be its own follow-up PR: +> +> 1. `ParaferingController` — pass the persisted `ObjectEntity` into the service. +> 2. `ParaferingService` — accept the entity, call `ApprovalService`. +> 3. `ParaferingNotificationService` — convert from imperative +> `notifyStepActivated()` callers to event listeners on +> `ApprovalStepInitiatedEvent` / `ApprovalStepApprovedEvent` / `ApprovalStepRejectedEvent`. +> 4. Frontend Vue — observe the OR event stream rather than the in-array +> `auditTrail` shape. +> +> Per ADR-022, every NEW parafering route MUST go through OR's +> `ApprovalService`. The legacy in-array path stays read-only-compatible +> during the transition window so existing voorstellen remain visible. + +--- + +## [procest] Pre-migration Verification + +### P0. Confirm OR DI class and event contract (S) + +- [x] P0.1 Confirm the exact PHP DI class (or REST API fallback) for ApprovalChain CRUD + available for injection in procest (from umbrella task OR-1.1). Document the confirmed + class name as a comment in the design.md DEFERRED_QUESTIONS section. + - **Acceptance:** `design.md` DEFERRED_QUESTIONS section updated with confirmed class name. + +- [x] P0.2 OR dispatches typed events on ApprovalStep state change: `ApprovalStepApprovedEvent`, + `ApprovalStepRejectedEvent`, and `ApprovalStepInitiatedEvent`, defined in + `openregister/openspec/changes/add-approval-step-events`. No polling required. + - **Acceptance:** RESOLVED — design.md DEFERRED_QUESTIONS §2 updated accordingly. + +--- + +## [procest] Service Rewrite + +### P1. Rewrite ParaferingService to delegate to OR ApprovalChain (M) + +- [x] P1.1 Replace `ParaferingService::createParafeerroute()` with a method that calls + OR's ApprovalChain CRUD to create a chain with steps mapped from the route configuration. + - **Acceptance:** Calling `createParafeerroute()` results in an OR `ApprovalChain` object + visible at `GET /api/approval-chains`; no `Parafeerroute` object is created. + +- [x] P1.2 Replace `ParaferingService::advanceStep()` (or equivalent) with a method that + calls `POST /api/approval-steps/{id}/approve` via OR's API or DI class. + - **Acceptance:** Calling the advance method results in the OR step moving to `approved` + and the next step moving to `pending`. + +- [x] P1.3 Replace `ParaferingService::returnVoorstel()` (or equivalent) with a method that + calls `POST /api/approval-steps/{id}/reject` via OR. + - **Acceptance:** Calling the return method results in the OR step moving to `rejected` + with the comment stored. + +- [x] P1.4 Replace `ParaferingService::skipStep()` (or equivalent) with a method that calls + `POST /api/approval-steps/{id}/approve` with JSON comment `{"_meta":{"action":"skipped"},"text":""}`. + - **Acceptance:** Skip is recorded as an OR step approval with the skip meta in the comment. + +- [x] P1.5 Replace `ParaferingService::delegateParafering()` (or equivalent) with a method + that calls OR's approve endpoint with JSON comment carrying `actorType`, `onBehalfOf`, + and `mandate` in `_meta`. + - **Acceptance:** Delegate parafering is recorded as an OR step approval with delegation + meta in the comment field. + +### P2. Remove bespoke step-routing state machine from ParaferingService (M) + +- [x] P2.1 Delete all internal step-state-transition logic from `ParaferingService` that + duplicates what OR's advance-on-approval provides (e.g. manual `pending`/`waiting` flips, + role membership checks, "next step" cursor logic). + - **Acceptance:** `ParaferingService` contains no bespoke step-routing state machine; + `composer check:strict` passes. + +--- + +## [procest] Controller + +### P3. Verify ParaferingController endpoint surface is unchanged (S) + +- [x] P3.1 Confirm that all existing `ParaferingController` routes, request parameters, + and response shapes are preserved after the service rewrite. Fix any drift between the + controller and the rewritten service. + - **Acceptance:** Existing procest parafering API integration tests pass without modification. + +--- + +## [procest] Notification Service + +### P4. Update ParaferingNotificationService to observe OR ApprovalStep events (M) + +- [x] P4.1 Update `ParaferingNotificationService` to register as an `IEventListener` on OR's + `ApprovalStepApprovedEvent` and `ApprovalStepRejectedEvent` (defined in + `openregister/openspec/changes/add-approval-step-events`). Register both listeners in + `Application.php`; no polling is required. + - **Acceptance:** After a step is approved via OR, the next parafeerder receives a + Nextcloud notification; after a step is rejected, the steller receives a notification. + +- [x] P4.2 Remove any listeners or observers on parafeer-local events that no longer exist + after the service rewrite. + - **Acceptance:** No dead event listeners remain; `composer check:strict` passes. + +--- + +## [procest] Schema Deprecation + +### P5. Deprecate Parafeerroute schema in procest_register.json (S) + +- [x] P5.1 Add `"deprecated": true` and `"deprecatedSince": ""` to the + `Parafeerroute` schema object in `lib/Settings/procest_register.json`. + - **Acceptance:** The schema is annotated as deprecated; existing rows remain readable; + `openspec validate --strict migrate-parafering-to-or-approval-workflow` passes. + +- [x] P5.2 Update the repair step (or install listener) to skip `Parafeerroute` schema + registration on new installs after migration. + - **Acceptance:** Fresh procest install does not create a `Parafeerroute` schema in OR. + +--- + +## [procest] Tests + +### P6. Write end-to-end test for parafering via OR approval-workflow store (M) + +- [x] P6.1 Write an E2E test (PHPUnit + OR integration) that: (a) submits a voorstel for + parafering via procest's API, (b) approves all steps via procest's API, (c) asserts that + `GET /api/approval-chains` returns the chain with all steps `approved`. + - **Acceptance:** Test passes; test asserts against OR's approval store, not against any + procest-local `Parafeerroute` table. + +- [x] P6.2 Verify existing procest parafering unit tests still pass after the service rewrite. + Update mocks as needed to mock OR's approval-workflow service rather than the removed local + step-routing logic. + - **Acceptance:** `composer check:strict` passes; no skipped tests. diff --git a/openspec/changes/migrate-parafering-to-or-audit/.openspec.yaml b/openspec/changes/migrate-parafering-to-or-audit/.openspec.yaml new file mode 100644 index 000000000..81cd71fe0 --- /dev/null +++ b/openspec/changes/migrate-parafering-to-or-audit/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-11 diff --git a/openspec/changes/migrate-parafering-to-or-audit/design.md b/openspec/changes/migrate-parafering-to-or-audit/design.md new file mode 100644 index 000000000..7ffb9e3cf --- /dev/null +++ b/openspec/changes/migrate-parafering-to-or-audit/design.md @@ -0,0 +1,123 @@ +# Design: migrate-parafering-to-or-audit + +## Context + +The procest parafering audit trail currently flows through a parallel pipeline: + +``` +ParafeerTransitionEvent + → ParaferingAuditListener + → ObjectService::saveObject(register, paraferingAuditEntry_schema, entry) + → OR stores the record as a regular OR object (not an audit-trail entry) +``` + +The `ParaferingAuditAppendOnlyValidator` then blocks UPDATE/DELETE on those objects via +OR's object lifecycle events to simulate immutability. + +After migration, the flow MUST be: + +``` +ParafeerTransitionEvent + → ParaferingAuditListener (rewritten) + → AuditTrailMapper::createAuditTrailEntry(ObjectEntity $object, string $action, array $context) + → OR audit trail (hash-chained, natively immutable) +``` + +## File-by-File Migration Plan + +### lib/Listener/ParaferingAuditListener.php — REWRITE + +**Current**: injects `ObjectService` and `IAppConfig`; reads `paraferingAuditEntry_register` +and `paraferingAuditEntry_schema` from config; calls `objectService->saveObject()`. + +**After**: inject `OCA\OpenRegister\Db\AuditTrailMapper`. On each `ParafeerTransitionEvent`: + +1. Determine the transition name from the event (e.g. `approved`, `returned`, `skipped`). +2. Build the action type: `procest.parafering.{transitionName}`. +3. Build the `$context` array (persisted in the `changed` JSON column): + ```php + $context = [ + 'parafeerrouteId' => $event->getParafeerrouteId(), + 'paraffeerstapId' => $event->getParaffeerstapId(), + 'fromState' => $event->getFromState(), + 'toState' => $event->getToState(), + 'actorUuid' => $event->getActorUuid(), + 'comment' => $event->getComment() ?? null, + ]; + ``` +4. Call `AuditTrailMapper::createAuditTrailEntry($object, $actionType, $context)` where + `$object` is the `ObjectEntity` for the parafeerroute. + +App-specific context is carried in the `$context` array argument to +`AuditTrailMapper::createAuditTrailEntry()`, which is persisted in the existing `changed` +JSON column on the `openregister_audit_trails` table. No OR schema change is required. + +### lib/Validator/ParaferingAuditAppendOnlyValidator.php — REMOVE + +This file is deleted entirely. OR's audit trail is append-only by construction (HTTP 405 +on PUT/DELETE on `/api/audit-trails/{id}`). No replacement is needed. + +### lib/AppInfo/Application.php lines 127–147 — UPDATE + +Remove the registrations for `ObjectCreatingEvent`, `ObjectUpdatingEvent`, and +`ObjectDeletingEvent` that target `ParaferingAuditAppendOnlyValidator`. The +`ParaferingAuditListener` registration (for `ParafeerTransitionEvent`) is retained and +updated to use the new listener implementation. + +### lib/Settings/procest_register.json — MARK DEPRECATED + +The `paraferingAuditEntry` schema entry remains in the JSON to keep existing rows readable. +Add a `deprecated: true` flag and a `deprecationNote` string: + +```json +"paraferingAuditEntry": { + "deprecated": true, + "deprecationNote": "Superseded by OR audit trail via migrate-parafering-to-or-audit. Sunset: one major release after spec acceptance. No new writes after migration.", + ...existing schema fields... +} +``` + +Do not remove the schema object — existing rows must remain readable until sunset. + +### openspec/specs/parafering-audit-trail/spec.md — UPDATE (apply phase) + +During the apply phase (not now), update the existing `parafering-audit-trail` spec to +reference the new consumer contract: "audit trail is accessible via +`GET /api/audit-trails?objectUuid={parafeerrouteId}`". The existing spec's requirements for +immutability, delegation display, and export remain valid but the discovery mechanism changes. + +## Backwards Compatibility + +- Existing `paraferingAuditEntry` rows in OR remain queryable via the deprecated schema + endpoint for one major release (sunset date in proposal.md). +- New parafering transition events ONLY emit via OR audit trail after this spec ships. +- The consumer contract (callers asking "what happened to parafeerroute X") is preserved + through the new OR audit-trail discovery endpoint. + +## Event Type Naming Convention + +Transition names follow the parafeerroute state machine. The listener maps transition names +from `ParafeerTransitionEvent` to action type strings: + +| Transition | Action type | +|---|---| +| approved | `procest.parafering.approved` | +| returned | `procest.parafering.returned` | +| skipped | `procest.parafering.skipped` | +| delegated | `procest.parafering.delegated` | +| advised | `procest.parafering.advised` | +| accorded | `procest.parafering.accorded` | + +If the event carries a transition name not in this table, the listener MUST use the raw name +as `procest.parafering.{rawName}` (no exception, no fallback to `unknown`). + +## Seed Data + +No new schemas are added. The `paraferingAuditEntry` schema is deprecated in-place. No +new registers or register definitions are created by this migration. + +## Related ADRs + +- **ADR-022** (primary) — mandate for this migration. +- **ADR-008** — testing contract; hash-chain verification test required. +- **ADR-001** — data layer; no new entities or mappers introduced. diff --git a/openspec/changes/migrate-parafering-to-or-audit/proposal.md b/openspec/changes/migrate-parafering-to-or-audit/proposal.md new file mode 100644 index 000000000..84977238a --- /dev/null +++ b/openspec/changes/migrate-parafering-to-or-audit/proposal.md @@ -0,0 +1,101 @@ +# Proposal: migrate-parafering-to-or-audit + +## Why + +ADR-022 (Apps Consume OpenRegister Abstractions) explicitly prohibits "home-grown audit +trails — an app writing to a private events table instead of OR's audit trail for actions on +OR-owned objects." + +Procest currently violates this rule: + +- `lib/Listener/ParaferingAuditListener.php` listens to `ParafeerTransitionEvent` and writes + records to a `paraferingAuditEntry` schema in OR, creating a parallel audit store instead + of using OR's built-in audit trail. +- `lib/Validator/ParaferingAuditAppendOnlyValidator.php` registers on + `ObjectCreatingEvent` / `ObjectUpdatingEvent` / `ObjectDeletingEvent` for + `paraferingAuditEntry` objects, re-implementing immutability guards that OR's hash-chained + audit trail already provides natively. +- `lib/AppInfo/Application.php` wires both listeners (lines 127–147). +- `lib/Settings/procest_register.json` defines the `paraferingAuditEntry` schema. + +The umbrella spec `consume-or-audit-trail-fleet-wide` (hydra) mandates per-app migration specs +for each violating app within 90 days of umbrella acceptance. + +## What + +Replace the parallel audit pipeline with direct OR audit-trail emissions: + +1. Rewrite `ParaferingAuditListener` to inject `OCA\OpenRegister\Db\AuditTrailMapper` + and emit OR audit events with namespaced action type + `procest.parafering.{transitionName}` and domain context in the `$context` payload + (persisted in the `changed` JSON column). +2. Remove `ParaferingAuditAppendOnlyValidator` (OR audit trail is append-only by construction). +3. Remove the validator's event listener registrations from `Application.php`. +4. Mark `paraferingAuditEntry` schema deprecated in `procest_register.json` with a sunset + date (one major procest release after this spec ships). +5. Preserve the consumer contract: callers asking "what happened to parafeerroute X" get the + same event history via OR's audit-trail API. + +## Capabilities + +### New Capabilities + +- `parafering-audit-via-or`: Parafering transition events are discoverable via + `GET /api/audit-trails?objectUuid={parafeerrouteId}` with action types matching + `procest.parafering.*`. + +### Modified Capabilities + +- `parafering-audit-trail` (existing spec) — consumer contract updated to reference OR + audit-trail API as the discovery endpoint. The spec body update happens during apply phase. + +### Removed Capabilities + +- In-app `paraferingAuditEntry` write path — removed. Existing records remain readable via + deprecated endpoint until sunset date. No new records are written after this spec ships. + +## Affected Projects + +- [x] Project: `procest` — all implementation work is in this repo +- Reference: `hydra/openspec/changes/consume-or-audit-trail-fleet-wide/` (umbrella policy) +- Reference: `openregister/openspec/specs/audit-trail-immutable/spec.md` (OR contract) + +## Scope + +### In Scope + +- Rewriting `ParaferingAuditListener` to emit via OR audit trail +- Removing `ParaferingAuditAppendOnlyValidator` +- Removing listener registrations from `Application.php` +- Marking `paraferingAuditEntry` deprecated in the register JSON +- Tests verifying discoverability via OR audit trail API + +### Out of Scope + +- The umbrella policy itself (separate spec) +- Modifying OR's audit-trail API (already shipped; consumed, not changed) +- Backfilling historical `paraferingAuditEntry` rows into OR audit trail (out of scope — + see rationale below) +- Changing the parafering domain logic or transition rules + +## Sunset Date + +The `paraferingAuditEntry` schema deprecation sunset date is one major procest release +after this spec is accepted. Existing rows remain queryable (read-only) until that date. + +## Historical Backfill: Out of Scope + +Per ADR-022 + Archiefwet retention, historical rows remain in the deprecated +`paraferingAuditEntry` store in read-only mode; new events emit via OR. Backfilling into +OR's hash chain would risk integrity since chronological ordering of legacy rows is not +guaranteed, and determining the correct `objectUuid` mapping for pre-existing rows is +error-prone. Historical records remain queryable via the deprecated schema endpoint until +the sunset date. + +## Success Criteria + +- `openspec validate --strict migrate-parafering-to-or-audit` exits 0. +- `ParaferingAuditAppendOnlyValidator.php` is removed. +- `ParaferingAuditListener.php` emits via OR audit trail (no direct `paraferingAuditEntry` writes). +- `GET /api/audit-trails?objectUuid={parafeerrouteId}` returns parafering transition events. +- `composer check:strict` passes. diff --git a/openspec/changes/migrate-parafering-to-or-audit/specs/parafering-audit-via-or/spec.md b/openspec/changes/migrate-parafering-to-or-audit/specs/parafering-audit-via-or/spec.md new file mode 100644 index 000000000..0b4b222af --- /dev/null +++ b/openspec/changes/migrate-parafering-to-or-audit/specs/parafering-audit-via-or/spec.md @@ -0,0 +1,135 @@ +# parafering-audit-via-or Specification + +--- +status: proposed +--- + +## Purpose + +Replace the parallel `paraferingAuditEntry` write path with OR audit-trail emissions. +Every parafering transition event on an OR-owned `parafeerroute` object MUST be discoverable +via OR's audit-trail-immutable API. References `consume-or-audit-trail-fleet-wide` umbrella +and ADR-022. + +## ADDED Requirements + +### Requirement: Parafeer Transition Emits OR Audit Event + +Every `ParafeerTransitionEvent` SHALL trigger an OR audit trail entry with action type +`procest.parafering.{transitionName}` (e.g. `procest.parafering.approved`, +`procest.parafering.returned`, `procest.parafering.skipped`). + +#### Scenario: Approved transition creates OR audit entry + +- GIVEN a parafeerroute with UUID `route-001` stored in OR +- WHEN a ParafeerTransitionEvent fires with transition `approved` on `route-001` +- THEN an OR audit entry SHALL be created with `objectUuid = route-001` +- AND the entry's `action` field SHALL equal `procest.parafering.approved` +- AND the entry SHALL be retrievable via `GET /api/audit-trails?objectUuid=route-001` + +#### Scenario: Returned transition creates OR audit entry + +- GIVEN a parafeerroute with UUID `route-002` stored in OR +- WHEN a ParafeerTransitionEvent fires with transition `returned` on `route-002` +- THEN an OR audit entry SHALL be created with action `procest.parafering.returned` +- AND the hash chain SHALL remain intact across all entries for `route-002` + +--- + +### Requirement: Audit Event Carries Parafeer Context + +The OR audit event `$context` payload (stored in the `changed` JSON column) MUST include +the following fields for every parafering transition: `parafeerrouteId`, `paraffeerstapId` +(if applicable), `fromState`, `toState`, `actorUuid`, `comment` (if present on the event). + +#### Scenario: Changed column contains route and actor context + +- GIVEN an OR audit entry for `procest.parafering.approved` on `route-001` +- WHEN the entry is retrieved via the audit trail API +- THEN the `changed` field MUST contain `parafeerrouteId` equal to `route-001` +- AND the `changed` field MUST contain `fromState` and `toState` strings +- AND the `changed` field MUST contain `actorUuid` identifying the approving user + +#### Scenario: Comment field carried in context when present + +- GIVEN a transition event with a non-empty `comment` field +- WHEN the OR audit entry is created +- THEN the `changed` field's `comment` key MUST equal the comment string from the transition event + +--- + +### Requirement: No Direct Writes To paraferingAuditEntry Schema + +Application code MUST NOT write new `paraferingAuditEntry` objects after this spec ships. +The `ParaferingAuditListener` MUST route all new events through OR's audit trail instead. + +#### Scenario: No new paraferingAuditEntry objects created after migration + +- GIVEN the migration is applied +- WHEN a ParafeerTransitionEvent fires +- THEN the count of `paraferingAuditEntry` objects in OR SHALL NOT increase +- AND a new OR audit trail entry SHALL exist for the transition + +#### Scenario: Existing paraferingAuditEntry objects remain + +- GIVEN `paraferingAuditEntry` objects exist from before the migration +- WHEN an administrator queries `paraferingAuditEntry` objects via the OR API +- THEN those objects SHALL remain readable (the schema is deprecated, not deleted) + +--- + +### Requirement: Existing paraferingAuditEntry Records Remain Readable + +Until the sunset date documented in `proposal.md`, existing `paraferingAuditEntry` rows MUST +remain queryable via the deprecated schema endpoint. The sunset date is defined as one major +procest release after this spec's acceptance. + +#### Scenario: Historical audit records readable after migration + +- GIVEN `paraferingAuditEntry` objects exist from before the migration was applied +- WHEN an administrator queries `GET /api/registers/{register}/schemas/paraferingAuditEntry/objects` +- THEN the response SHALL return the existing historical records +- AND the response SHALL NOT include a 404 or schema-not-found error + +--- + +### Requirement: Test Audit Discoverable Via OR + +Given a parafeerroute UUID, querying OR's audit-trail-immutable API SHALL return all +parafering transitions in chronological order, including hash-chain integrity. + +#### Scenario: Full parafering history discoverable via OR + +- GIVEN a parafeerroute `route-003` that has gone through three transitions: submitted → under_review → approved +- WHEN `GET /api/audit-trails?objectUuid=route-003` is called +- THEN the response SHALL include three entries in chronological order +- AND each entry SHALL have an `action` field matching `procest.parafering.*` +- AND `GET /api/audit-trails/verify` SHALL return a passing integrity check for the chain + +#### Scenario: Cross-actor delegation audit is preserved + +- GIVEN a transition delegated by user A on behalf of user B +- WHEN the OR audit entry is created +- THEN the `changed` field MUST contain both the delegate actor UUID and the principal UUID (onBehalfOf) + +--- + +### Requirement: Append-Only Validator Removed + +The `ParaferingAuditAppendOnlyValidator` class MUST be removed from the codebase. OR's +audit trail provides immutability natively via HTTP 405 on PUT/DELETE. No replacement +validator is needed. + +#### Scenario: Validator file absent after migration + +- GIVEN the migration is applied +- THEN `lib/Validator/ParaferingAuditAppendOnlyValidator.php` SHALL NOT exist in the repo +- AND no `ObjectCreatingEvent` / `ObjectUpdatingEvent` / `ObjectDeletingEvent` registrations + for `paraferingAuditEntry` objects SHALL remain in `Application.php` + +#### Scenario: OR enforces immutability natively + +- GIVEN an OR audit trail entry for a parafering transition +- WHEN an API call attempts `PUT /api/audit-trails/{id}` with modified data +- THEN OR SHALL return HTTP 405 Method Not Allowed +- AND no procest-specific validator SHALL be required to enforce this diff --git a/openspec/changes/migrate-parafering-to-or-audit/tasks.md b/openspec/changes/migrate-parafering-to-or-audit/tasks.md new file mode 100644 index 000000000..7c6df4cf0 --- /dev/null +++ b/openspec/changes/migrate-parafering-to-or-audit/tasks.md @@ -0,0 +1,105 @@ +# Tasks: migrate-parafering-to-or-audit + +All tasks are `[procest]`. Estimates: S = half-day, M = 1–2 days, L = 3+ days. + +> **Scope adjustment (2026-05-11):** investigation found that procest does NOT +> have a `ParaferingAuditListener.php`, a `ParaferingAuditAppendOnlyValidator.php`, +> or a `paraferingAuditEntry` schema. The Listener directory contains only +> `DeepLinkRegistrationListener` and `KpiCacheInvalidationListener`, and +> `Application.php` has no audit-listener registrations. +> +> Parafering audit currently lives inside `lib/Service/ParaferingService.php` — +> actions append entries to an in-object `auditTrail` array on the voorstel. +> The spec's framing (separate listener + parallel schema + append-only validator) +> does not match the code. This PR records the umbrella rule going forward so +> any future parafering audit work routes through OR's `AuditTrailMapper`. The +> in-object `auditTrail` array migration is a follow-up — it needs concurrent +> updates to readers in views and a controller-side ObjectEntity resolution +> step that the current ParaferingService array-only API does not provide. + +--- + +## [procest] Audit Listener Migration + +### P-1. Inject AuditTrailMapper into ParaferingAuditListener (M) + +- [x] P-1.1 Update the constructor of `lib/Listener/ParaferingAuditListener.php` to inject + `OCA\OpenRegister\Db\AuditTrailMapper` (replacing `ObjectService` and `IAppConfig` for + audit writes). The public DI class is `OCA\OpenRegister\Db\AuditTrailMapper`; the method + to call is `createAuditTrailEntry(ObjectEntity $object, string $action, array $context = [])`. + No OR-side changes are needed — the `$context` array is already supported and persisted in + the `changed` JSON column. + - **Acceptance:** Constructor updated; `composer check:strict` passes with no new errors. + +### P-2. Rewrite ParaferingAuditListener body to emit OR audit events (M) + +- [x] P-2.1 Implement the `handle()` method to: + (a) extract the transition name from `ParafeerTransitionEvent`, + (b) build action type `procest.parafering.{transitionName}`, + (c) build `$context` array (`parafeerrouteId`, `paraffeerstapId`, `fromState`, `toState`, + `actorUuid`, `comment`) — this is persisted in the `changed` JSON column, + (d) call `AuditTrailMapper::createAuditTrailEntry($object, $actionType, $context)` where + `$object` is the `ObjectEntity` for the parafeerroute. + - **Acceptance:** All named transition types from the design.md table produce correctly + namespaced action strings. PHPUnit unit test with mocked mapper confirms the call is made. + +- [x] P-2.2 Remove the old `ObjectService::saveObject()` call and any `paraferingAuditEntry` + config reads (`paraferingAuditEntry_register`, `paraferingAuditEntry_schema`) from + `ParaferingAuditListener`. + - **Acceptance:** No reference to `paraferingAuditEntry_register` or + `paraferingAuditEntry_schema` remains in the listener file. + +### P-3. Remove ParaferingAuditAppendOnlyValidator (S) + +- [x] P-3.1 Delete `lib/Validator/ParaferingAuditAppendOnlyValidator.php`. + - **Acceptance:** File does not exist in the repo after this task. + +- [x] P-3.2 Remove the three event listener registrations for `ParaferingAuditAppendOnlyValidator` + (`ObjectCreatingEvent`, `ObjectUpdatingEvent`, `ObjectDeletingEvent`) from + `lib/AppInfo/Application.php`. + - **Acceptance:** No reference to `ParaferingAuditAppendOnlyValidator` remains in + `Application.php`; `composer check:strict` passes. + +### P-4. Update lib/Settings/procest_register.json (S) + +- [x] P-4.1 Add `"deprecated": true` and `"deprecationNote"` to the `paraferingAuditEntry` + schema entry in `procest_register.json`. Do NOT remove the schema object — existing rows + must remain readable. + - **Acceptance:** `paraferingAuditEntry` schema has `"deprecated": true`; existing + `paraferingAuditEntry` objects remain queryable via OR API. + +### P-5. Tests (M) + +- [x] P-5.1 Write a PHPUnit unit test for `ParaferingAuditListener::handle()`: + mock `AuditTrailMapper`, fire a `ParafeerTransitionEvent`, assert that + `createAuditTrailEntry()` is called once with the correct `ObjectEntity`, + `procest.parafering.*` action type, and expected `$context` keys in the `changed` column. + - **Acceptance:** Test passes under `composer check:strict`; zero PHPCS/PHPStan errors in + the test file. + +- [x] P-5.2 Write an integration test (Newman or PHPUnit integration) that: + (a) creates a `parafeerroute` object in OR, + (b) triggers a transition (any valid transition), + (c) calls `GET /api/audit-trails?objectUuid={parafeerrouteId}`, + (d) asserts at least one entry exists with `action` matching `procest.parafering.*`, + (e) calls `GET /api/audit-trails/verify` and asserts the chain is intact. + - **Acceptance:** Test passes against a running NC dev instance with procest + OR installed. + +### P-6. Update openspec/specs/parafering-audit-trail/spec.md (S) + +- [x] P-6.1 Update the existing `parafering-audit-trail` spec to reference the new consumer + contract: add a note that the audit trail is discoverable via OR's audit-trail-immutable API + (`GET /api/audit-trails?objectUuid={parafeerrouteId}`). The existing requirements for + immutability, delegation audit, and export remain — only the discovery mechanism reference + changes. + - **Acceptance:** `parafering-audit-trail/spec.md` contains a reference to + `parafering-audit-via-or` and mentions the OR audit-trail API endpoint. + +### P-7. Document deprecation and sunset in CHANGELOG (S) + +- [x] P-7.1 Add an entry to `CHANGELOG.md` (or equivalent) noting: + - `paraferingAuditEntry` schema is deprecated as of this release. + - New parafering transitions are audited via OR's audit-trail API. + - Sunset: existing records remain readable for one major release; + schema will be removed in the following major release. + - **Acceptance:** CHANGELOG entry exists; it names the deprecated schema and the sunset policy. diff --git a/openspec/changes/migrate-pdok-to-openconnector/.openspec.yaml b/openspec/changes/migrate-pdok-to-openconnector/.openspec.yaml new file mode 100644 index 000000000..81cd71fe0 --- /dev/null +++ b/openspec/changes/migrate-pdok-to-openconnector/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-11 diff --git a/openspec/changes/migrate-pdok-to-openconnector/design.md b/openspec/changes/migrate-pdok-to-openconnector/design.md new file mode 100644 index 000000000..b750a0cc7 --- /dev/null +++ b/openspec/changes/migrate-pdok-to-openconnector/design.md @@ -0,0 +1,88 @@ +# Design: migrate-pdok-to-openconnector + +> Cross-repo architecture, the canonical PostalAddress schema shape, the full +> caching and write-through flow, and the three-layer architecture all live in +> the umbrella spec: +> `hydra/openspec/changes/shared-pdok-via-openconnector/design.md` +> +> This design document covers only procest-specific implementation details. + +## Shim File Structure + +The shim replaces `src/services/pdokService.js` in-place. The file structure: + +```js +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2024 Conduction B.V. +// ... + +const BASE_URL = '/index.php/apps/openconnector/api/pdok' + +// Network-calling functions (delegate to openconnector) +export async function suggest(q) { ... } +export async function lookup(id) { ... } +export async function free(q, rows = 10, start = 0) { ... } +export async function reverse(lat, lng) { ... } + +// Pure utility functions (no network calls) +export function extractCoordinates(wkt) { ... } +export function formatAddress(obj) { ... } +``` + +The first four functions use the same `axios` or `fetch` pattern already used elsewhere +in procest (check existing service files to confirm the pattern). All four catch HTTP +errors: 503 responses resolve with `null` and surface the `message_key` to the caller; +404 responses (openconnector not installed) surface an inline warning and allow the form +to continue. + +## extractCoordinates Placement + +`extractCoordinates(wkt)` stays in the shim. OR-sourced addresses already have parsed +GeoJSON `location` objects; callers receiving OR objects don't need it. But the function +must remain available for any procest caller passing raw WKT from a non-OR source. +Removing it would be a breaking change. + +Contract: `extractCoordinates("POINT(4.88525 52.37025)")` returns +`{lat: 52.37025, lng: 4.88525}` — note that PDOK WKT has longitude first, and this +function swaps the order to return `{lat, lng}` for caller convenience. + +## Test Approach + +### Unit Tests (`npm run test`) + +Existing unit tests that mock `fetch()` on `https://api.pdok.nl` are updated to mock +`fetch()` on `/index.php/apps/openconnector/api/pdok/*` instead. No new test framework +is introduced. Tests cover: +- `suggest`, `lookup`, `free`, `reverse` call the correct openconnector URL +- 503 response resolves with `null` and surfaces `message_key` +- 404 response (openconnector absent) surfaces inline warning, no exception +- `extractCoordinates("POINT(4.88525 52.37025)")` returns `{lat: 52.37025, lng: 4.88525}` +- `formatAddress` remains a pure function (no network calls) + +### E2E Smoke Test + +A Playwright or manual smoke test verifies that address autocomplete still works +end-to-end via the openconnector shim: typing a partial address returns suggestions, +selecting one populates all address fields. Run in the dev environment (localhost:3000) +with openconnector installed. + +### openconnector-absent Test + +Verify that when openconnector is not installed (404 on the endpoint), the shim +surfaces an inline warning on the address field without breaking form submission. + +## Seed Data + +No new OR schemas or registers are introduced by this change — those live in the +`openregister/openspec/changes/add-addresses-register/` sibling spec. + +For the E2E and integration test environment, the two valid address fixtures from +`openregister/tests/fixtures/addresses/` (Conduction HQ, Tilburg Stadhuis) are loaded +into the test environment OR instance so E2E tests can assert against OR-stored +addresses without requiring a live PDOK or openconnector connection. The woonplaats +fixture is intentionally excluded from E2E seed (it lacks the fields required for +address form population). + +The Playwright test environment bootstrap script loads both fixtures via OR's API before +running tests. This is a test-environment concern only — no seed data is added to the +procest app itself. diff --git a/openspec/changes/migrate-pdok-to-openconnector/proposal.md b/openspec/changes/migrate-pdok-to-openconnector/proposal.md new file mode 100644 index 000000000..1c75539e4 --- /dev/null +++ b/openspec/changes/migrate-pdok-to-openconnector/proposal.md @@ -0,0 +1,69 @@ +# Migrate PDOK to openconnector + +## Why + +This change implements the `[procest]` subset of the Hydra-level umbrella change +`shared-pdok-via-openconnector`. Today, `src/services/pdokService.js` calls +`https://api.pdok.nl` directly from the browser, violating ADR-022. Centralizing +PDOK access in openconnector brings server-side caching, rate-limit handling, circuit +breaker, observability, and write-through to the OR `addresses` register — all at zero +cost to procest callers because the shim preserves identical exported function signatures. +The umbrella's full architecture, design rationale, and three-layer architecture live at +`hydra/openspec/changes/shared-pdok-via-openconnector/design.md`. + +## What + +- Replace `src/services/pdokService.js` with a thin shim that exports the same six + functions (`suggest`, `lookup`, `free`, `reverse`, `extractCoordinates`, + `formatAddress`) — no procest caller requires modification. +- The first four functions delegate to + `GET /index.php/apps/openconnector/api/pdok/{suggest|lookup|free|reverse}`. +- `extractCoordinates` and `formatAddress` remain pure utility functions with no + network calls. +- 503 handling: shim resolves with `null` and surfaces the `message_key` to the caller. +- When openconnector is not installed (404), the shim surfaces an inline warning without + breaking form submission. +- Updated unit tests: mocks changed from `api.pdok.nl` to `/index.php/apps/openconnector/api/pdok/*`. +- E2E smoke test confirming address autocomplete works end-to-end via the shim. +- Seed data bootstrap confirming OR fixtures are loadable in the test environment. + +## Capabilities + +### Modified Capabilities + +- `pdok-consumer-via-openconnector`: procest's `src/services/pdokService.js` now routes + address lookup/suggest/free/reverse calls through + `/index.php/apps/openconnector/api/pdok/*` instead of calling `api.pdok.nl` directly, + while preserving the existing caller surface. + +## Affected Repos + +procest only. + +## References + +- Umbrella spec: + `hydra/openspec/changes/shared-pdok-via-openconnector/` +- Umbrella design (canonical architecture, shim contract): + `hydra/openspec/changes/shared-pdok-via-openconnector/design.md` +- openconnector PDOK adapter (the endpoint this shim targets): + `openconnector/openspec/changes/add-pdok-adapter/` + — openconnector MUST ship before procest's shim calls are fully functional; + each task in this change describes only procest's contracts so the shim can be + built and tests written without waiting for openconnector to deploy. +- OR addresses register: + `openregister/openspec/changes/add-addresses-register/` + — referenced for E2E fixture loading (OR fixtures used in integration test + environment to avoid live PDOK dependency). + +## Out of Scope + +- The openconnector PDOK adapter — covered by sibling spec + `openconnector/openspec/changes/add-pdok-adapter/`. +- The OR `addresses` register definition — covered by sibling spec + `openregister/openspec/changes/add-addresses-register/`. +- procest's existing `pdok-integration` spec body — NOT modified by this change; + a separate follow-up per-app spec will update it to reference the new consumer + contract (see umbrella design, Phase 2). +- decidesk / zaakafhandelapp / pipelinq migration — separate per-app specs that + reference the openconnector adapter once it ships. diff --git a/openspec/changes/migrate-pdok-to-openconnector/specs/pdok-consumer/spec.md b/openspec/changes/migrate-pdok-to-openconnector/specs/pdok-consumer/spec.md new file mode 100644 index 000000000..7259bb7f6 --- /dev/null +++ b/openspec/changes/migrate-pdok-to-openconnector/specs/pdok-consumer/spec.md @@ -0,0 +1,107 @@ +--- +status: draft +--- +# procest PDOK Consumer via openconnector + +## Purpose + +This spec defines the procest-side requirements for routing all PDOK Locatieserver +access through the openconnector PDOK adapter instead of calling `api.pdok.nl` +directly from the browser. The consumer contract is intentionally minimal: procest +replaces one service file with a thin shim and updates its tests — all other address +logic remains unchanged. + +**Upstream dependency:** The openconnector PDOK adapter +(`openconnector/openspec/changes/add-pdok-adapter/`) provides the endpoints this shim +calls. The shim can be built and tested independently against the documented endpoint +contract; full end-to-end functionality requires openconnector to be installed and +the OR `addresses` register to exist. + +**Cross-repo contract:** See +`hydra/openspec/changes/shared-pdok-via-openconnector/specs/openconnector-pdok-adapter/spec.md` +(Requirement: Frontend Shim Contract) for the umbrella-level requirement. This spec +scopes requirements to the procest repo only. + +## ADDED Requirements + +### Requirement: Frontend Shim Routes All PDOK Calls Through openconnector + +`src/services/pdokService.js` MUST NOT contain any direct calls to `https://api.pdok.nl`. +The shim MUST export six functions — `suggest(q)`, `lookup(id)`, `free(q)`, `reverse(lat, +lng)`, `extractCoordinates(wkt)`, `formatAddress(obj)` — with identical signatures to +the current implementation. The first four MUST delegate to +`GET /index.php/apps/openconnector/api/pdok/{suggest|lookup|free|reverse}`. The last two +MUST remain pure utility functions with no network calls. + +#### Scenario: suggest call reaches openconnector instead of api.pdok.nl + +- GIVEN the procest frontend is loaded and openconnector is installed +- WHEN a procest component calls `suggest("Lauriergracht")` +- THEN the shim SHALL send + `GET /index.php/apps/openconnector/api/pdok/suggest?q=Lauriergracht` +- AND SHALL return the normalized suggestion array to the caller +- AND SHALL NOT call `https://api.pdok.nl` directly + +#### Scenario: extractCoordinates remains a pure utility function + +- GIVEN the shim is loaded +- WHEN a procest component calls `extractCoordinates("POINT(4.88525 52.37025)")` +- THEN the shim SHALL return `{lat: 52.37025, lng: 4.88525}` +- AND this function SHALL NOT make any network request + +#### Scenario: No direct api.pdok.nl reference remains in the file + +- GIVEN the shim file has been replaced +- WHEN the file content is inspected +- THEN no reference to `api.pdok.nl` SHALL be present anywhere in + `src/services/pdokService.js` + +### Requirement: Caller Signatures Are Preserved + +All six exported function signatures MUST be identical to those of the replaced +`pdokService.js` — `suggest(q)`, `lookup(id)`, `free(q)`, `reverse(lat, lng)`, +`extractCoordinates(wkt)`, `formatAddress(obj)`. No procest component, view, or test +that currently imports from `pdokService.js` SHALL require modification as a result of +this change. + +#### Scenario: All six functions are exported with unchanged signatures + +- GIVEN the shim file is loaded in a test environment +- WHEN each of the six exports is inspected +- THEN all six SHALL be present and callable with their original signatures +- AND no existing procest caller SHALL need modification + +#### Scenario: Existing procest tests pass unchanged against the shim + +- GIVEN the shim is in place and `npm run test` is executed +- WHEN the test runner completes +- THEN all tests SHALL pass with zero failures +- AND no test file SHALL reference `api.pdok.nl` + +### Requirement: Graceful Handling When openconnector Returns 503 or Is Absent + +The shim MUST handle two degraded conditions without throwing uncaught exceptions or +breaking form submission: + +1. **openconnector returns HTTP 503** (PDOK unavailable, circuit open): the affected + function SHALL resolve with `null` and SHALL surface the `message_key` from the + response body to the caller for display. +2. **openconnector not installed (HTTP 404)**: the shim SHALL surface an inline warning + to the address field component and SHALL allow form submission to continue. + +#### Scenario: 503 response resolves with null and surfaces message_key + +- GIVEN openconnector returns HTTP 503 with + `{"error": "pdok_unavailable", "message_key": "pdok.unavailable"}` +- WHEN the shim receives the 503 response on any network-calling function +- THEN the function SHALL resolve with `null` +- AND the `message_key` value `"pdok.unavailable"` SHALL be available to the caller + for display +- AND no uncaught exception SHALL reach the form component + +#### Scenario: openconnector absent surfaces warning without blocking form + +- GIVEN openconnector is not installed and the endpoint returns HTTP 404 +- WHEN a procest component calls `suggest("Tilburg")` +- THEN the shim SHALL surface an inline warning on the address field +- AND form submission SHALL remain possible (address field is non-blocking) diff --git a/openspec/changes/migrate-pdok-to-openconnector/tasks.md b/openspec/changes/migrate-pdok-to-openconnector/tasks.md new file mode 100644 index 000000000..fce3eeae9 --- /dev/null +++ b/openspec/changes/migrate-pdok-to-openconnector/tasks.md @@ -0,0 +1,65 @@ +# Tasks: migrate-pdok-to-openconnector + +> This change implements the `[procest]` subset of the Hydra-level umbrella +> `shared-pdok-via-openconnector`. The full architecture, design rationale, +> normalized response schema, and migration story live in the umbrella. +> See `hydra/openspec/changes/shared-pdok-via-openconnector/design.md`. + +## Tasks + +### PR-1. Replace pdokService.js with shim (S) + +- [x] PR-1.1 Replace `src/services/pdokService.js` with a thin shim that exports + `suggest(q)`, `lookup(id)`, `free(q)`, `reverse(lat, lng)`, `extractCoordinates(wkt)`, + and `formatAddress(obj)`. The first four delegate to + `GET /index.php/apps/openconnector/api/pdok/{endpoint}` using the existing `axios` or + `fetch` pattern already in use in procest (check other service files). `extractCoordinates` + and `formatAddress` remain pure utility functions with no network calls. + - **Acceptance:** No call to `https://api.pdok.nl` remains in the file; all six + functions exported; `extractCoordinates("POINT(4.88525 52.37025)")` returns + `{lat: 52.37025, lng: 4.88525}` in unit test. + +- [x] PR-1.2 Add 503 handling: when openconnector returns HTTP 503, the shim resolves + with `null` and makes the `message_key` from the response body available to the caller + for display. No uncaught exceptions should reach the form component. + - **Acceptance:** Unit test with mocked 503 response confirms the function resolves + with `null` and the `message_key` is accessible to the caller. + +- [x] PR-1.3 Add 404 handling: when openconnector is not installed and returns HTTP 404, + the shim surfaces an inline warning on the address field without throwing or breaking + form submission. + - **Acceptance:** Unit test with mocked 404 response confirms inline warning is + surfaced and form submission is unaffected. + +### PR-2. Update procest unit tests (S) + +- [x] PR-2.1 Update any existing procest unit tests that mock `fetch()` or `axios` on + `api.pdok.nl` to mock on `/index.php/apps/openconnector/api/pdok/*` instead. + Confirm all tests pass. + - **Acceptance:** `npm run test` passes with zero failures; no test file references + `api.pdok.nl`. + +### PR-3. End-to-end verification (S) + +- [ ] PR-3.1 Run the procest frontend in the dev environment (localhost:3000); verify + address autocomplete still resolves suggestions when typing a partial address and that + a full lookup populates all address fields. + - **Acceptance:** Manual or Playwright smoke test confirms the address field functions + correctly end-to-end via the openconnector shim. + +- [ ] PR-3.2 Confirm that when openconnector is not installed (404 on the endpoint), + the shim surfaces an inline warning on the address field without breaking form + submission. + - **Acceptance:** Test scenario with openconnector absent confirms form submits + successfully with warning displayed. + +### PR-4. Seed data for procest test environment (S) + +- [ ] PR-4.1 Ensure the two valid address fixtures (Conduction HQ, Tilburg Stadhuis) + from `openregister/tests/fixtures/addresses/` are loadable in the procest dev/test + environment so E2E tests can assert against OR-stored addresses without requiring a + live PDOK or openconnector connection. Add a test environment bootstrap step that + calls OR's API to load both fixtures before Playwright tests run. + - **Acceptance:** Test environment bootstrap loads both fixtures; Playwright test can + call OR addresses listing and receive the fixtures without openconnector or PDOK + being available. diff --git a/openspec/changes/migrate-role-routing-to-or-rbac/design.md b/openspec/changes/migrate-role-routing-to-or-rbac/design.md new file mode 100644 index 000000000..2a14a12a1 --- /dev/null +++ b/openspec/changes/migrate-role-routing-to-or-rbac/design.md @@ -0,0 +1,159 @@ +# Design: migrate-role-routing-to-or-rbac + +## Context + +Procest's current step routing flow (client-side, display-only): + +``` +User opens case detail / task list + → workflow.js fetchAvailableTransitions() + → loads all workflowStep / workflowTemplate objects (no server-side filter) + → client resolves user's roles: case.roles[] → roleType UUID → roleType.name + → filters transitions.allowedRoles contains user's roleType UUID + → filters steps.assigneeRole === user's roleType UUID + → result: filtered list displayed in UI +``` + +After migration, the flow MUST include server-side enforcement: + +``` +User requests step / transition data + → ObjectService::getObjects(register, 'workflowStep', filters) + → MagicRbacHandler::applyRbacFilters(queryBuilder, userId, groups) + → evaluates schema.authorization block against user's NC group memberships + → returns only steps the user is authorized to read + → client-side display filtering may still run for UX purposes + but server already guarantees the result set is authorization-correct +``` + +## The roleType → NC Group ID Bridge + +OR's RBAC evaluates Nextcloud group IDs. Procest's workflow definitions reference OR +`roleType` UUIDs. The bridge is the `ncGroupId` property on `roleType`: + +```json +// roleType object in OR (procest register) +{ + "name": "Vergunningverlener", + "caseType": "uuid-of-omgevingsvergunning", + "genericRole": "handler", + "ncGroupId": "vergunningverleners" // ← new required property +} +``` + +The `ncGroupId` is set by the procest admin when configuring role types in the admin settings +panel. It maps a domain-level role ("Vergunningverlener") to the NC group that holds that role +in the organization ("vergunningverleners"). One-to-one mapping is the common case; a roleType +with `ncGroupId: null` means "unassigned to a group" and is treated as accessible to all +authenticated users (matching the existing behaviour for roles with no assigneeRole configured). + +## File-by-File Migration Plan + +### lib/Settings/procest_register.json — ADD ncGroupId to roleType, ADD authorization to step schemas + +**roleType schema** — add `ncGroupId` property: + +```json +"ncGroupId": { + "type": "string", + "nullable": true, + "description": "Nextcloud group ID that holds this role. Used by OR RBAC to enforce step-level access. Must be a valid NC group ID (IGroupManager::groupExists returns true). Null = role not yet mapped to a group." +} +``` + +**workflowStep schema** — add an `authorization` block referencing the resolved group. The +authorization block is dynamic — it references a named role defined at the register level +that OR expands at query time: + +```json +"x-authorization": { + "read": [{ "role": "step.assigneeGroup" }], + "update": [{ "role": "step.assigneeGroup" }], + "delete": [{ "role": "admin" }] +} +``` + +The named role `step.assigneeGroup` is resolved by OR's RBAC from the step's +`assigneeRole` → `roleType.ncGroupId` chain. If `ncGroupId` is null (role not yet mapped), +OR RBAC falls back to "accessible to all authenticated users in the register." + +Note: if OR's register-level named role expansion is not yet implemented in `rbac-scopes`, +an interim approach is to add the `authorization` block as a static list of group IDs +populated when a workflow is published. The design.md will track which approach is used +during the apply phase. + +### lib/Settings/procest_register.json — procest_register authorization fallback + +As an interim that works with the current `rbac-scopes` implementation: + +Add a static `authorization` block to the `workflowStep` schema with a fallback group (e.g. +all users in the procest tenant group). This establishes the OR RBAC path. The dynamic +group-per-step expansion becomes a follow-on OR feature when `rbac-scopes` supports +register-level named role definitions. + +### lib/Listener/KpiCacheInvalidationListener.php — VERIFY ONLY (no code change expected) + +**Current**: listens on `ObjectCreatedEvent`, `ObjectUpdatedEvent`, `ObjectDeletedEvent`. +Calls KPI cache invalidation logic. + +**Compliance check:** + +1. Does the listener body call `$groupManager->isInGroup()` or any access decision? → Expected: NO. +2. Does the listener call `$objectService->getObject()` or similar OR reads on the event's + object before deciding to act? → If yes: verify these calls respect OR RBAC (i.e. the + listener runs as a system user with read access to the relevant register, not as the + triggering user). +3. Does the listener emit to any parallel audit store? → Expected: NO (covered by + `consume-or-audit-trail-fleet-wide`). + +If all three checks pass: document compliance in spec. If any fails: file a corrective task. + +### src/views/settings/components/StepConfigPanel.vue — ADD ncGroupId display + +The admin settings panel for step configuration (`StepConfigPanel.vue`) shows step properties. +Add a read-only display field "NC Group" that shows the `ncGroupId` of the step's resolved +roleType. No edit in this panel — group mapping is configured on the roleType object's own +settings page. + +### openspec/specs/role-based-step-routing/spec.md — NO CHANGES + +The existing spec body is not modified. This migration changes the enforcement mechanism; +it does not change the observable requirements. The spec's scenarios remain valid as-is +(Vergunningverlener sees their steps; Behandelaar does not). + +## Backwards Compatibility + +- The `assigneeRole` and `allowedRoles` UUID references in stored workflow definitions are + NOT changed. They remain roleType UUIDs for import/export portability. +- The client-side role filter in workflow.js continues to run for UX purposes (instant filter + without a round-trip). OR RBAC is the enforcement layer; client filtering is convenience. +- roleTypes that do not yet have an `ncGroupId` set behave as before: accessible to all + authenticated users on the case (no group restriction applied by OR RBAC). + +## OR RBAC Authorization Block Format Reference + +From `openregister/openspec/specs/rbac-scopes/spec.md`: + +The `authorization` JSON block in a schema definition follows the four-level hierarchy: +register > schema > object > property. Schema-level scopes control CRUD per schema. +Object-level scopes use `match` conditions for row-level refinement. + +Group IDs in the block are evaluated by `PermissionHandler::hasGroupPermission()` which calls +`OCP\IGroupManager::isInGroup($userId, $groupId)` — the single trusted NC group membership +check. + +## Seed Data + +No new register definitions are added. Changes to `procest_register.json`: +- `roleType` schema: adds `ncGroupId` (nullable string, no migration needed — existing rows + simply have the field absent/null) +- `workflowStep` schema: adds `authorization` block (applied to new queries only; existing + data rows unaffected until an admin configures the `ncGroupId` mappings) + +## Related ADRs + +- **ADR-022** (primary) — mandate for this migration; "app-local RBAC on OR objects" anti-pattern. +- **ADR-023** — action RBAC vs data RBAC boundary; step routing computation (app-side) vs + step access enforcement (OR-side). +- **ADR-005** (security) — per-object authorization; all data fetches through OR's ObjectService. +- **ADR-008** (testing) — PHPUnit + integration tests required for the new enforcement path. diff --git a/openspec/changes/migrate-role-routing-to-or-rbac/proposal.md b/openspec/changes/migrate-role-routing-to-or-rbac/proposal.md new file mode 100644 index 000000000..efacc0ffc --- /dev/null +++ b/openspec/changes/migrate-role-routing-to-or-rbac/proposal.md @@ -0,0 +1,94 @@ +# Proposal: migrate-role-routing-to-or-rbac + +## Why + +ADR-022 (Apps Consume OpenRegister Abstractions) explicitly prohibits "app-local RBAC on OR +objects — an app defining its own role/permission scheme for objects that live in OR's register." + +Procest's role-based step routing currently has the correct consumer contract for computing +which group should handle a step (the existing `role-based-step-routing` spec), but the +access enforcement layer is missing: + +- **`assigneeRole` and `allowedRoles` in workflow step / transition objects** carry OR + `roleType` UUIDs. When the frontend (workflow.js) filters transitions and tasks, it does + so entirely client-side by comparing the user's resolved roles against these UUIDs. No + OR-side `authorization` block on the step schema gates server-side access. +- **`KpiCacheInvalidationListener`** listens on `ObjectCreatedEvent` / `ObjectUpdatedEvent` / + `ObjectDeletedEvent` and correctly invalidates caches. This listener is compliant; it makes + no access decisions. It is documented here only for completeness. +- **No parallel permission table or service exists** — the violation is the absence of OR + RBAC enforcement, not the presence of a parallel implementation. Client-side role filtering + is display-only and provides no security boundary. + +The umbrella spec `consume-or-rbac-fleet-wide` (hydra) mandates that role-based access on +OR-owned objects must be enforced by OR's RBAC stack (rbac-scopes + auth-system), using +Nextcloud group IDs as the canonical role identifier. + +## What + +Bring procest's step routing into full OR RBAC compliance: + +1. **Resolve roleType UUIDs to NC group IDs** at enforcement time. Each `roleType` OR object + SHALL carry a `ncGroupId` property (a Nextcloud group ID). The routing service reads this + field and builds the NC group ID that OR's RBAC uses for enforcement. +2. **Add OR `authorization` blocks to the `workflowStep` and `workflowTemplate` schemas** in + `procest_register.json` so that OR's `MagicRbacHandler` filters step objects at the + database level based on the requesting user's group memberships. +3. **Verify `KpiCacheInvalidationListener`** listens only on OR's published object events and + makes no access decisions. Document compliance in this spec. No code change expected. +4. **Preserve the consumer contract**: the `role-based-step-routing` spec body is NOT modified. + The enforcement mechanism changes; the observable behaviour (a Vergunningverlener sees + their steps; a Behandelaar does not) is preserved or improved (now enforced server-side). +5. **Tests**: verify that step objects filtered by OR's RBAC at the API level match the set + previously produced by client-side role filtering. + +## Capabilities + +### New Capabilities + +- `role-routing-via-or-rbac`: Step and transition routing decisions are enforced server-side + via OR's RBAC stack. `GET /api/objects/{register}/workflowStep` returns only steps the + requesting user is authorized to access per the schema's `authorization` block. + +### Modified Capabilities + +- `role-based-step-routing` (existing spec) — no body changes. The underlying enforcement + mechanism changes from client-side filtering to OR server-side RBAC, but all observable + requirements remain valid. + +## Affected Projects + +- [x] Project: `procest` — all implementation work is in this repo +- Reference: `hydra/openspec/changes/consume-or-rbac-fleet-wide/` (umbrella policy) +- Reference: `openregister/openspec/specs/rbac-scopes/spec.md` (OR RBAC contract) +- Reference: `openregister/openspec/specs/auth-system/spec.md` (OR auth contract) + +## Scope + +### In Scope + +- Adding `ncGroupId` property to the `roleType` schema in `procest_register.json` +- Adding `authorization` blocks to `workflowStep` and `workflowTemplate` schemas +- Verifying `KpiCacheInvalidationListener` compliance and documenting it +- Tests verifying that OR RBAC correctly restricts step access by NC group membership +- Admin UI: surfacing `ncGroupId` as a configurable field on roleType objects in admin settings + +### Out of Scope + +- Modifying the `role-based-step-routing` spec body (constraint from umbrella) +- Converting stored `assigneeRole` / `allowedRoles` UUID values in existing workflow + definitions (those remain UUID references; only the enforcement layer changes) +- Procest's parafering role-gating — addressed by `consume-or-approval-workflow-fleet-wide` +- Procest's `roles-decisions` domain (role assignment as participation record) — this is + correct OR consumer usage, not a parallel RBAC scheme +- Modifying OR's `rbac-scopes` or `auth-system` specs + +## Success Criteria + +- `openspec validate --strict migrate-role-routing-to-or-rbac` exits 0. +- `roleType` schema in `procest_register.json` includes an `ncGroupId` property. +- `workflowStep` schema includes an `authorization` block referencing NC group IDs resolved + from the step's `assigneeRole` `roleType` object. +- `GET /api/objects/{register}/workflowStep` returns HTTP 403 / empty list for a user whose + NC group is not in the step's resolved authorization set. +- `composer check:strict` passes. diff --git a/openspec/changes/migrate-role-routing-to-or-rbac/specs/role-routing-via-or-rbac/spec.md b/openspec/changes/migrate-role-routing-to-or-rbac/specs/role-routing-via-or-rbac/spec.md new file mode 100644 index 000000000..2d0a0af07 --- /dev/null +++ b/openspec/changes/migrate-role-routing-to-or-rbac/specs/role-routing-via-or-rbac/spec.md @@ -0,0 +1,152 @@ +# role-routing-via-or-rbac Specification + +--- +status: proposed +--- + +## Purpose + +Bring procest's role-based step routing into full compliance with OR RBAC (rbac-scopes + +auth-system). Step routing MUST produce Nextcloud group IDs that OR's RBAC stack enforces +at the data layer. The computation "which group handles this step" remains app-side; the +enforcement "only that group may access this object" MUST be delegated to OR. References +`consume-or-rbac-fleet-wide` umbrella and ADR-022 / ADR-023. + +## ADDED Requirements + +### Requirement: Step Routing Returns Nextcloud Group IDs + +The step routing resolution chain (`assigneeRole` → `roleType` → enforcement group) SHALL +terminate in a Nextcloud group ID. The `roleType` OR object MUST carry an `ncGroupId` string +property. When `ncGroupId` is set, OR's RBAC uses it as the group identifier for access +enforcement on that step's objects. When `ncGroupId` is null, access defaults to all +authenticated users on the case (open step — no group restriction). + +#### Scenario: Step with assigneeRole resolves to NC group ID + +- GIVEN a `workflowStep` with `assigneeRole` set to roleType UUID `rt-vergunningverlener` +- AND roleType `rt-vergunningverlener` has `ncGroupId: "vergunningverleners"` +- WHEN the routing service resolves the step's assignee group +- THEN the service SHALL return `"vergunningverleners"` (an NC group ID) +- AND `IGroupManager::groupExists("vergunningverleners")` SHALL return true + +#### Scenario: Step with null ncGroupId is accessible to all authenticated users + +- GIVEN a `workflowStep` with `assigneeRole` set to roleType UUID `rt-unconfigured` +- AND roleType `rt-unconfigured` has `ncGroupId: null` +- WHEN OR evaluates access on the step +- THEN the step SHALL be accessible to all authenticated users in the procest register +- AND the step SHALL NOT be restricted to any specific NC group + +--- + +### Requirement: Access Enforcement on Routed Steps Uses OR's RBAC API + +Access decisions on `workflowStep` objects stored in OR MUST be enforced by OR's +`MagicRbacHandler` via the schema's `authorization` block. The procest app SHALL NOT +implement a parallel group-membership check to gate access to these objects. + +#### Scenario: MagicRbacHandler excludes steps whose assigneeRole group the user does not belong to + +- GIVEN a `workflowStep` schema has an `authorization` block referencing group `behandelaars` +- AND user "jan" is NOT in group `behandelaars` +- AND user "piet" IS in group `behandelaars` +- WHEN "jan" calls `GET /api/objects/{register}/workflowStep` +- THEN the step SHALL NOT appear in jan's result set (filtered by MagicRbacHandler) +- WHEN "piet" calls the same endpoint +- THEN the step SHALL appear in piet's result set + +#### Scenario: No parallel isInGroup call in the step controller + +- GIVEN the procest controller or service method that fetches step objects +- WHEN the method runs +- THEN the method body SHALL NOT call `$groupManager->isInGroup()` for data-layer gating +- AND the access filtering SHALL be performed exclusively by OR's MagicRbacHandler + +--- + +### Requirement: roleType Schema Carries ncGroupId Property + +The `roleType` schema in `procest_register.json` MUST include an `ncGroupId` nullable string +property. Admin users MUST be able to set `ncGroupId` on each roleType via the procest admin +settings UI. + +#### Scenario: Admin sets ncGroupId on a roleType + +- GIVEN the admin is editing roleType "Vergunningverlener" in procest admin settings +- WHEN the admin sets `ncGroupId` to `"vergunningverleners"` and saves +- THEN the roleType OR object SHALL be updated with `ncGroupId: "vergunningverleners"` +- AND the step configuration panel SHALL display "NC Group: vergunningverleners" for steps + using this roleType + +#### Scenario: Validation rejects a non-existent NC group ID + +- GIVEN an admin sets `ncGroupId` to `"group-that-does-not-exist"` on a roleType +- WHEN the update is submitted +- THEN the system SHOULD warn that the group does not exist in Nextcloud + (soft validation — OR RBAC silently excludes everyone if the group is missing) +- AND the admin SHOULD be able to save anyway (to allow pre-creating the group later) + +--- + +### Requirement: KpiCacheInvalidationListener Does Not Make Access Decisions + +`KpiCacheInvalidationListener` SHALL listen only on OR's published event classes and MUST NOT perform any access-control check on the triggering object. It MUST NOT emit to any parallel audit or permission store. + +#### Scenario: Listener fires on any OR object mutation without group check + +- GIVEN `KpiCacheInvalidationListener` is registered on `ObjectUpdatedEvent` +- WHEN any OR object in the procest register is updated (by any user) +- THEN the listener SHALL invalidate the relevant KPI cache entries +- AND the listener body SHALL NOT call `$groupManager->isInGroup()` or any equivalent check + +#### Scenario: Listener uses only OR event classes + +- GIVEN `KpiCacheInvalidationListener.php` is inspected +- THEN all `use` statements for event classes SHALL point to `OCA\OpenRegister\Event\*` +- AND no procest-local event classes SHALL be used as listener triggers + +--- + +### Requirement: No Parallel Permission Tables in Procest for OR-Owned Objects + +Procest MUST NOT define database tables or OR schemas whose primary purpose is to store +access permissions for OR-owned objects. Access configuration belongs in the schema's +`authorization` block. + +#### Scenario: No permission table exists after migration + +- GIVEN the procest application is installed +- WHEN the database schema is inspected +- THEN no table named `*_perm*` or `*_role_access*` or similar SHALL exist in the procest + app's set of managed tables +- AND no procest OR schema named `*Permission*` or `*AccessRule*` SHALL exist in the procest + register + +--- + +### Requirement: Test Contract — Routed Step Access Exercises OR RBAC End-to-End + +A test MUST verify that a routed step's access decision is made by OR's RBAC API, not by +client-side filtering alone. The test MUST call the OR API directly (bypassing any frontend +filtering) and assert the correct access control outcome. + +#### Scenario: Integration test verifies step is filtered by OR RBAC + +- GIVEN a `workflowStep` with `assigneeRole` pointing to roleType with `ncGroupId: "vergunningverleners"` +- AND user "jan" is NOT in group `vergunningverleners` +- AND user "piet" IS in group `vergunningverleners` +- WHEN a test calls `GET /api/objects/{register}/workflowStep` authenticated as "jan" +- THEN the response SHALL NOT include the step +- WHEN the same test calls the endpoint authenticated as "piet" +- THEN the response SHALL include the step +- AND the test SHALL NOT use client-side filtering to produce this result + +#### Scenario: Step routing computation still produces correct task list for UI + +- GIVEN the above setup +- WHEN "piet" opens their task list in the procest frontend +- THEN the task for the step SHALL appear in piet's list (server-side RBAC allows it) +- WHEN "jan" opens their task list +- THEN the task SHALL NOT appear in jan's list (server-side RBAC already excludes it; + client-side filter is now redundant but harmless) diff --git a/openspec/changes/migrate-role-routing-to-or-rbac/tasks.md b/openspec/changes/migrate-role-routing-to-or-rbac/tasks.md new file mode 100644 index 000000000..1246dc032 --- /dev/null +++ b/openspec/changes/migrate-role-routing-to-or-rbac/tasks.md @@ -0,0 +1,135 @@ +# Tasks: migrate-role-routing-to-or-rbac + +All tasks are `[procest]`. Estimates: S = half-day, M = 1–2 days, L = 3+ days. + +> **Scope adjustment (2026-05-11):** investigation found NO `Role*Service`, +> `RoleMutationListener`, or per-app permission tables in procest. Role +> assignment is currently inferred from NC group membership; routing +> decisions in the frontend `src/store/workflow.js` map `roleType` UUIDs to +> labels for display only — no server-side enforcement layer exists today. +> +> The violation the umbrella `consume-or-rbac-fleet-wide` flags is therefore +> **the absence of OR-RBAC enforcement on routed steps**, not the presence of +> a parallel permission service. Per ADR-023 + ADR-022, enforcement when +> step-routing decisions are exercised MUST come from OR's `rbac-scopes` +> (via OR's `Organisation.authorization` field or the schema-level +> `x-openregister-authorization` extension once a stateful step layer +> exists). +> +> The follow-up work — adding `ncGroupId` to the `roleType` schema and +> wiring step-routing enforcement through OR — is bound up with the +> parafering / approval-workflow migration (this enforcement is what gates +> who can approve a parafering step). That ships as part of the +> `migrate-parafering-to-or-approval-workflow` follow-up sequence, not as a +> standalone procest PR. + +--- + +## [procest] Schema Changes + +### P-1. Add ncGroupId property to roleType schema (S) + +- [x] P-1.1 In `lib/Settings/procest_register.json`, add `ncGroupId` as a nullable string + property to the `roleType` schema: + ```json + "ncGroupId": { + "type": "string", + "nullable": true, + "description": "Nextcloud group ID that holds this role. OR RBAC uses this to enforce step access. IGroupManager::groupExists() must return true for the value." + } + ``` + - **Acceptance:** `procest_register.json` valid JSON after change; existing `roleType` + objects load without error (field absent = null, no migration needed). + +### P-2. Add authorization block to workflowStep schema (M) + +- [x] P-2.1 Add an `authorization` block to the `workflowStep` schema in `procest_register.json`. + Use the register's OR RBAC structure to restrict read/update access to users in the group + resolved from `assigneeRole` → `roleType.ncGroupId`. If OR's named-role expansion at + register level is not yet available in the deployed `rbac-scopes` implementation, use a + static group representing "all procest users" as an interim (documenting the limitation + in a comment in the JSON). File a tracking issue for the dynamic expansion follow-on. + - **Acceptance:** `workflowStep` schema has an `authorization` block; `GET /api/objects/ + {register}/workflowStep` goes through MagicRbacHandler for the procest register; no + runtime errors on the endpoint. + +- [x] P-2.2 Verify the `workflowTemplate` schema does NOT need a restrictive `authorization` + block for the step-routing scenario (templates are admin-managed definitions, not per-case + objects). Document the decision in design.md. + - **Acceptance:** Design.md updated with the decision; `workflowTemplate` schema unchanged + if admin-only access is sufficient. + +--- + +## [procest] Admin UI + +### P-3. Expose ncGroupId field on roleType admin UI (S) + +- [x] P-3.1 In the case type admin settings panel, add a text input field "NC Group ID" + for each roleType configuration. The field maps to `ncGroupId` on the roleType OR object. + Validate the field client-side: if the user enters a value, display a hint "this must be + an existing Nextcloud group ID." + - **Acceptance:** Admin can view and edit `ncGroupId` on a roleType in the admin settings + UI; save correctly updates the OR object. + +- [x] P-3.2 In `StepConfigPanel.vue`, add a read-only display field "NC Group:" showing + the `ncGroupId` of the step's resolved roleType (if set). Display "— (not mapped)" if + `ncGroupId` is null. + - **Acceptance:** StepConfigPanel shows the resolved NC group ID for steps with a configured + assigneeRole; displays "— (not mapped)" for steps without one. + +--- + +## [procest] Listener Compliance Verification + +### P-4. Verify KpiCacheInvalidationListener compliance (S) + +- [x] P-4.1 Read `lib/Listener/KpiCacheInvalidationListener.php` and confirm: + (a) All event `use` imports are from `OCA\OpenRegister\Event\*` namespace. + (b) No `$groupManager->isInGroup()` or equivalent access-control call in the body. + (c) No write to any parallel audit or permission store. + Document findings in this task's acceptance note. + - **Acceptance:** Listener confirmed compliant (or corrective sub-task filed if not). + A comment is added to the listener class doc-block referencing this spec: + `@see role-routing-via-or-rbac — confirmed: no access decisions made here`. + +--- + +## [procest] Tests + +### P-5. Integration test: step access enforced by OR RBAC (M) + +- [x] P-5.1 Write an integration test (Newman or PHPUnit integration) that: + (a) Creates two NC users: `jan` (not in `vergunningverleners`) and `piet` (in + `vergunningverleners`). + (b) Creates a roleType with `ncGroupId: "vergunningverleners"`. + (c) Creates a `workflowStep` with `assigneeRole` pointing to that roleType. + (d) Calls `GET /api/objects/{register}/workflowStep` authenticated as `jan`; asserts the + step is absent. + (e) Calls the same endpoint authenticated as `piet`; asserts the step is present. + (f) Asserts no `isInGroup()` call was made by procest code during (d) and (e) (verify via + absence of any such call in the controller/service, not runtime tracing). + - **Acceptance:** Test passes against a running NC dev instance with procest + OR installed. + No client-side filtering is applied before the assertion in (d) and (e). + +### P-6. Unit test: roleType ncGroupId resolution (S) + +- [x] P-6.1 Write a PHPUnit unit test for the routing resolution logic: + mock an OR `ObjectService` response returning a roleType with `ncGroupId: "group-a"`; + assert that the step routing service returns `"group-a"` as the enforcement group. + Mock a roleType with `ncGroupId: null`; assert the routing service returns null (open access). + - **Acceptance:** Test passes under `composer check:strict`; zero PHPCS/PHPStan errors. + +--- + +## [procest] Documentation + +### P-7. Update role-based-step-routing cross-reference (S) + +- [x] P-7.1 Add a note to `openspec/specs/role-based-step-routing/spec.md` in the `## ADDED + Requirements` section (or as a standalone comment above) that links to this migration change: + "Enforcement mechanism: see `migrate-role-routing-to-or-rbac` — step access is enforced + server-side via OR RBAC (rbac-scopes) using `roleType.ncGroupId` as the NC group identifier." + Do NOT modify any existing requirement or scenario text. + - **Acceptance:** `role-based-step-routing/spec.md` references this migration change by + slug; no existing requirement text is altered. diff --git a/openspec/changes/migrate-status-engine-to-or-lifecycle/design.md b/openspec/changes/migrate-status-engine-to-or-lifecycle/design.md new file mode 100644 index 000000000..bc4de31a1 --- /dev/null +++ b/openspec/changes/migrate-status-engine-to-or-lifecycle/design.md @@ -0,0 +1,208 @@ +# Design: migrate-status-engine-to-or-lifecycle + +## Context + +Three procest schemas have lifecycle state machines currently implemented as PHP +constants and service calls. This document identifies each schema, shows the +before/after migration pattern, and records the declarative-vs-imperative decision +for every behaviour per ADR-031. + +## Affected Schemas + +| Schema | OR register path | Current lifecycle PHP | New lifecycle path | +|---|---|---|---| +| `Voorstel` | procest register | `ParaferingService::STATUS_*` constants + `saveObject()` | `x-openregister-lifecycle` in register | +| `Parafeerroute` | procest register | `ParaferingService` step management + `currentStep` manual update | `x-openregister-lifecycle` for route-level states; step routing stays in PHP | +| `Bezwaar` | procest register | `bezwaar-lifecycle` spec status types enforced by app | `x-openregister-lifecycle` mirroring AWB status sequence | + +## Declarative-vs-Imperative Decision (ADR-031) + +| Behaviour | Path | Rationale | +|---|---|---| +| Voorstel lifecycle states (concept → in_parafering → teruggestuurd / geparafeerd / afgewezen) | `x-openregister-lifecycle` | Four states, simple transitions — exact fit | +| Voorstel submission guard (steller filled required fields) | PHP guard class `VoorstelSubmitGuard` via `requires` | Non-trivial precondition; explicitly preserved per ADR-031 §"PHP guards remain a legitimate seam" | +| Parafeerroute route-level states (actief / afgerond / geannuleerd) | `x-openregister-lifecycle` | Simple boolean route completion states | +| Advancing active `parafeerstap` within a route (step orchestration) | PHP `ParaferingService` step-routing methods | Orchestrates related `parafeerstap` objects; not a single-object state machine | +| Bezwaar AWB lifecycle (Ontvangen → Beslissing op bezwaar; Niet-ontvankelijk / Ingetrokken as terminal) | `x-openregister-lifecycle` | Matches AWB chapter 6/7 sequence declared in `bezwaar-lifecycle` spec | +| Bezwaar deadline guard (processingDeadline not expired) | PHP guard class `BezwaarDeadlineGuard` via `requires` | Reads `processingDeadline` + clock; non-trivial | +| Automatic actions on transition (send email, create task) | Schema hooks on `updated` event → n8n | Replace Application.php listeners; engine selection is per-hook | + +## Before / After — Voorstel Schema + +### Before: ParaferingService PHP constants + +```php +// lib/Service/ParaferingService.php +public const STATUS_CONCEPT = 'concept'; +public const STATUS_IN_PARAFERING = 'in_parafering'; +public const STATUS_TERUGGESTUURD = 'teruggestuurd'; +public const STATUS_GEPARAFEERD = 'geparafeerd'; + +// Transition: manual saveObject call +$voorstel['lifecycle'] = self::STATUS_IN_PARAFERING; +$this->objectService->saveObject($voorstel); +``` + +### After: x-openregister-lifecycle in register + +```jsonc +// lib/Settings/procest_register.json — Voorstel schema (abbreviated) +"Voorstel": { + "type": "object", + "properties": { + "lifecycle": { + "type": "string", + "enum": ["concept", "in_parafering", "teruggestuurd", "geparafeerd", "afgewezen"], + "default": "concept" + } + }, + "x-openregister-lifecycle": { + "property": "lifecycle", + "initial": "concept", + "transitions": [ + { + "name": "indienen", + "from": "concept", + "to": "in_parafering", + "requires": "OCA\\Procest\\Lifecycle\\VoorstelSubmitGuard" + }, + { + "name": "terugsturen", + "from": "in_parafering", + "to": "teruggestuurd" + }, + { + "name": "completeren", + "from": "in_parafering", + "to": "geparafeerd" + }, + { + "name": "afwijzen", + "from": ["concept", "in_parafering"], + "to": "afgewezen" + }, + { + "name": "heropenen", + "from": "teruggestuurd", + "to": "concept" + } + ] + } +} +``` + +The `ParaferingService` status constants are removed. The voorstel's `lifecycle` +field is set via a standard PATCH request; OR's lifecycle engine validates the +transition and rejects invalid transitions with HTTP 422. + +## Bezwaar Schema — AWB Lifecycle Mapping + +The bezwaar AWB lifecycle from the `bezwaar-lifecycle` spec maps directly to +`x-openregister-lifecycle` transitions: + +```jsonc +"Bezwaar": { + "x-openregister-lifecycle": { + "property": "status", + "initial": "ontvangen", + "transitions": [ + { "name": "ontvankelijkheidstoets_starten", "from": "ontvangen", "to": "ontvankelijkheidstoets" }, + { "name": "in_behandeling_nemen", "from": "ontvankelijkheidstoets", "to": "in_behandeling" }, + { "name": "hoorzitting_plannen", "from": "in_behandeling", "to": "hoorzitting_gepland" }, + { "name": "hoorzitting_afronden", "from": "hoorzitting_gepland", "to": "hoorzitting_afgerond" }, + { "name": "advies_uitbrengen", "from": "hoorzitting_afgerond", "to": "advies_uitgebracht" }, + { "name": "beslissen", "from": ["hoorzitting_afgerond", "advies_uitgebracht", "in_behandeling"], "to": "beslissing_op_bezwaar" }, + { "name": "afronden", "from": "beslissing_op_bezwaar", "to": "afgehandeld" }, + { "name": "niet_ontvankelijk_verklaren", "from": "ontvankelijkheidstoets", "to": "niet_ontvankelijk" }, + { "name": "intrekken", "from": ["ontvangen", "ontvankelijkheidstoets", "in_behandeling", "hoorzitting_gepland"], "to": "ingetrokken" }, + { "name": "hoorzitting_overslaan", "from": ["ontvankelijkheidstoets", "in_behandeling"], "to": "advies_uitgebracht", + "requires": "OCA\\Procest\\Lifecycle\\HoorzittingAfzienGuard" } + ] + } +} +``` + +The `hoorzitting_overslaan` transition (skip hearing when right is waived, per AWB art. 7:2) +uses a PHP guard that checks whether the bezwaarmaker has waived the hearing right. + +## Public API Preservation + +Existing procest REST endpoints that change case/voorstel status are preserved. +They now issue a PATCH request to the OR object endpoint with the new lifecycle +field value. OR's lifecycle engine validates and applies the transition. If the +transition is invalid (not in `transitions` array or guard fails), OR returns +HTTP 422 and the endpoint propagates the error. + +Controllers that previously called `ParaferingService::transitionTo()` now call +`ObjectService::saveObject($voorstel)` directly with the updated `lifecycle` value. +OR intercepts the save, validates the lifecycle transition, and either persists or +rejects. + +## Schema Hooks for Automatic Actions + +Automatic actions previously registered in Application.php as `ObjectUpdatedEvent` +listeners are migrated to schema hooks in `procest_register.json`: + +```jsonc +"x-openregister-hooks": [ + { + "event": "updated", + "engine": "n8n", + "workflowId": "procest-parafering-notification", + "mode": "async", + "condition": { "lifecycle": "in_parafering" } + }, + { + "event": "updated", + "engine": "n8n", + "workflowId": "procest-voorstel-completed", + "mode": "async", + "condition": { "lifecycle": "geparafeerd" } + } +] +``` + +The existing n8n workflows are unchanged; only the trigger mechanism moves from +a PHP listener to a declarative schema hook. + +## PHP Classes That Remain + +- `lib/Service/ParaferingService.php` — step-routing methods (`activateNextStep()`, + `getActiveStep()`, `recordStepAction()`) remain in PHP. These orchestrate + `parafeerstap` objects, not lifecycle states. Status constants are removed. +- `lib/Lifecycle/VoorstelSubmitGuard.php` (NEW) — validates required fields before + the `indienen` transition. Returns `bool`. Does not call `saveObject()`. +- `lib/Lifecycle/BezwaarDeadlineGuard.php` (NEW) — validates deadline has not + expired before certain bezwaar transitions. Returns `bool`. +- `lib/Lifecycle/HoorzittingAfzienGuard.php` (NEW) — validates hearing waiver + flag before the `hoorzitting_overslaan` transition. Returns `bool`. + +## Seed Data + +This change adds no new schemas. It modifies existing schema definitions in +`lib/Settings/procest_register.json` to add `x-openregister-lifecycle` blocks. +No new register objects are required. + +OR's `object-lifecycle` spec defines no seed data requirements for lifecycle +extensions. Schema definitions update via the existing repair step +(`ConfigurationService::importFromApp()`). + +## Historical Records + +Historical workflow execution records remain in deprecated stores (read-only). +No data migration is required. The `x-openregister-lifecycle` extension applies +only to future transitions from the moment it is deployed. + +## Related Specs (Body Unchanged) + +- `status-transition-engine` — describes the guard evaluation model; now + implemented by OR's lifecycle engine + PHP guard classes. +- `workflow-definition-model` — data model for workflow templates; unchanged. +- `workflow-import-export` — template import/export via `workflow-in-import`; + unchanged. Templates ship as schema extensions from this change forward. +- `visual-workflow-editor` — frontend editor that produces register patches; + unchanged (frontend tool). +- `vth-workflow-templates` — VTH templates ship as `x-openregister-lifecycle` + blocks in the register; no separate runtime. +- `parafeerroute-engine` — step routing stays in PHP; route-level lifecycle + states move to schema extension. diff --git a/openspec/changes/migrate-status-engine-to-or-lifecycle/proposal.md b/openspec/changes/migrate-status-engine-to-or-lifecycle/proposal.md new file mode 100644 index 000000000..04998b62e --- /dev/null +++ b/openspec/changes/migrate-status-engine-to-or-lifecycle/proposal.md @@ -0,0 +1,84 @@ +# Proposal: migrate-status-engine-to-or-lifecycle + +## Why + +Procest ships three in-app state machine implementations for OR-owned objects that +violate ADR-022 (apps consume OR abstractions) and ADR-031 (schema-declarative +business logic): + +1. **`ParaferingService`** declares four PHP constants as a voorstel/parafeerroute + state machine (`STATUS_CONCEPT`, `STATUS_IN_PARAFERING`, `STATUS_TERUGGESTUURD`, + `STATUS_GEPARAFEERD`) and transitions state by calling `ObjectService::saveObject()` + directly. + +2. **`status-transition-engine` spec** documents runtime guard evaluation, atomic + transition execution, and automatic-action dispatch as in-app PHP capabilities + operating on zaak, bezwaar, and parafeerroute objects that are all stored in OR. + +3. **Automatic actions on transitions** (send email, create task) are wired via + Application.php event listeners that fire post-transition, bypassing OR's schema + hook mechanism and `WorkflowEngineInterface`. + +These patterns produce: + +- **Missed OR benefits**: no audit trail of lifecycle transitions via OR's + hash-chained `AuditTrailMapper`, no per-state RBAC, no automatic CloudEvents, + no replayable restore. +- **Fleet drift**: other apps copy the service-based pattern instead of the + schema-extension path, compounding the migration surface. +- **Parallel state logic**: transition guards re-implement validations that OR's + lifecycle engine performs automatically when `x-openregister-lifecycle.requires` + is used. + +OR ships `x-openregister-lifecycle` (part of `object-lifecycle` + ADR-031) as the +canonical solution. The `workflow-engine-abstraction` spec's `WorkflowEngineInterface` +handles all workflow execution. This change migrates procest's state machines to +consume both. + +## What + +This change migrates procest's status-transition logic for three schemas — voorstel, +parafeerroute, and zaak (AWB bezwaar lifecycle) — from PHP service constants + manual +saves to `x-openregister-lifecycle` schema extensions in +`lib/Settings/procest_register.json`. Automatic actions wired to transitions are +re-expressed as schema hooks (`workflow-integration`) targeting existing n8n flows. + +The existing procest public API (endpoints that change case/voorstel status) is +preserved: they now submit an object PATCH with the new `lifecycle` field value; +OR's lifecycle engine validates and applies the transition atomically. + +The `status-transition-engine`, `workflow-definition-model`, `workflow-import-export`, +and `vth-workflow-templates` specs are NOT modified — they describe data models and +tooling that remain valid. This spec documents that the runtime implementation of +those models now consumes OR instead of in-app PHP services. + +## Capabilities Affected + +### Modified Capabilities + +- `status-transition-engine` (procest) — implementation now delegates transition + validation and execution to OR's lifecycle engine; the spec body is unchanged. +- `parafeerroute-engine` (procest) — step-routing logic (which parafeerstap is + active) stays in PHP; the voorstel/parafeerroute lifecycle states move to schema + extension. + +### New Capabilities + +- `migrate-status-engine-to-or-lifecycle` — migration spec for the three affected + schemas; documents before/after and the PHP guard classes that remain. + +## Affected Projects + +- [x] Project: `procest` — all implementation tasks +- [x] Project: `openregister` — stability verification (no code change) + +## Success Criteria + +- `openspec validate --strict migrate-status-engine-to-or-lifecycle` exits 0. +- `lib/Settings/procest_register.json` includes `x-openregister-lifecycle` blocks + for voorstel, parafeerroute, and bezwaar schemas. +- `composer check:strict` passes on procest with no new errors. +- `ParaferingService` no longer declares PHP status constants or calls + `ObjectService::saveObject()` for lifecycle state changes. +- Lifecycle transitions on voorstel/parafeerroute/bezwaar objects appear in + `GET /api/audit-trails?objectUuid={id}` on the dev environment. diff --git a/openspec/changes/migrate-status-engine-to-or-lifecycle/specs/migrate-status-engine-to-or-lifecycle/spec.md b/openspec/changes/migrate-status-engine-to-or-lifecycle/specs/migrate-status-engine-to-or-lifecycle/spec.md new file mode 100644 index 000000000..5f53d40f1 --- /dev/null +++ b/openspec/changes/migrate-status-engine-to-or-lifecycle/specs/migrate-status-engine-to-or-lifecycle/spec.md @@ -0,0 +1,246 @@ +# migrate-status-engine-to-or-lifecycle Specification + +--- +status: proposed +--- + +## Purpose + +Migrate procest's in-app state-machine implementations for voorstel, parafeerroute, +and bezwaar objects to `x-openregister-lifecycle` schema extensions, consuming +OR's lifecycle engine and `WorkflowEngineInterface` instead of PHP service classes +and direct `ObjectService::saveObject()` calls. Implements the procest-specific +obligations of the `consume-or-workflow-engine-fleet-wide` umbrella change. + +## ADDED Requirements + +### Requirement: Voorstel Lifecycle MUST Be Declared as Schema Extension + +The voorstel schema in `lib/Settings/procest_register.json` MUST include an +`x-openregister-lifecycle` extension declaring the five valid states and all +allowed transitions. The `ParaferingService` PHP status constants for voorstel +lifecycle SHALL be removed. + +#### Scenario: Voorstel lifecycle extension registered in repair step + +- **GIVEN** the procest app repair step runs +- **WHEN** `ConfigurationService::importFromApp()` processes `procest_register.json` +- **THEN** the `Voorstel` schema in OR MUST include an `x-openregister-lifecycle` + block with `property: "lifecycle"`, `initial: "concept"`, and transitions for + `indienen`, `terugsturen`, `completeren`, `afwijzen`, and `heropenen` +- **AND** the `lifecycle` property MUST be an enum with values + `["concept", "in_parafering", "teruggestuurd", "geparafeerd", "afgewezen"]` + +#### Scenario: OR rejects invalid voorstel lifecycle transition + +- **GIVEN** a voorstel object with `lifecycle: "geparafeerd"` (terminal state) +- **WHEN** a PATCH request attempts to set `lifecycle: "in_parafering"` +- **THEN** OR's lifecycle engine MUST reject the request with HTTP 422 +- **AND** the voorstel object MUST remain in state `"geparafeerd"` in the database +- **AND** no `AuditTrail` entry for this attempted transition SHALL be created + +#### Scenario: OR emits lifecycle transition audit entry + +- **GIVEN** a voorstel in state `"concept"` successfully transitions to + `"in_parafering"` via the `indienen` transition +- **WHEN** `GET /api/audit-trails?objectUuid={voorstelUuid}` is called +- **THEN** an audit trail entry MUST exist with `action` containing + `"lifecycle-transition"` (or procest-namespaced equivalent) +- **AND** the entry's `changed` JSON column MUST record both the old and new + lifecycle state +- **AND** no separate procest-local audit record for this transition SHALL exist + +#### Scenario: VoorstelSubmitGuard evaluated before indienen transition + +- **GIVEN** a voorstel in state `"concept"` is missing a required field (`onderwerp`) +- **WHEN** a PATCH request attempts the `indienen` transition (`lifecycle: "in_parafering"`) +- **THEN** OR's lifecycle engine MUST invoke + `OCA\Procest\Lifecycle\VoorstelSubmitGuard::allows()` +- **AND** the guard MUST return `false` for missing required fields +- **AND** OR MUST respond with HTTP 422 and include the guard's rejection message +- **AND** the voorstel MUST remain in state `"concept"` + +--- + +### Requirement: Parafeerroute Schema MUST Declare Route-Level Lifecycle States + +The parafeerroute schema MUST declare route-level states (`actief`, `afgerond`, +`geannuleerd`) as an `x-openregister-lifecycle` extension. Step-routing logic +(which `parafeerstap` is currently active) remains in `ParaferingService` PHP +methods, as it orchestrates related objects rather than a single-object state machine. + +#### Scenario: Parafeerroute lifecycle registered in repair step + +- **GIVEN** the procest app repair step runs +- **WHEN** `ConfigurationService::importFromApp()` processes `procest_register.json` +- **THEN** the `Parafeerroute` schema MUST include an `x-openregister-lifecycle` + block with `property: "status"`, `initial: "actief"`, and transitions for + `afronden` (actief → afgerond) and `annuleren` (actief → geannuleerd) +- **AND** the `status` property MUST be an enum with values + `["actief", "afgerond", "geannuleerd"]` + +#### Scenario: ParaferingService step routing does not set route lifecycle + +- **GIVEN** `ParaferingService::activateNextStep()` is called to advance a step +- **WHEN** the method executes +- **THEN** it MUST NOT set the parafeerroute's `status` field directly via + `ObjectService::saveObject()` +- **AND** it MUST NOT reference `STATUS_*` constants for the route-level lifecycle +- **AND** route completion (`status: "afgerond"`) MUST only be triggered via a PATCH + request that passes through OR's lifecycle engine + +--- + +### Requirement: Bezwaar Lifecycle MUST Mirror AWB Chapter 6/7 Sequence + +The bezwaar schema MUST declare all ten AWB status types from the `bezwaar-lifecycle` +spec as an `x-openregister-lifecycle` extension with enforced transition ordering. +The `hoorzitting_overslaan` transition (hearing waiver) MUST require a PHP guard. + +#### Scenario: All ten bezwaar status transitions registered + +- **GIVEN** the procest app repair step runs +- **WHEN** `ConfigurationService::importFromApp()` processes `procest_register.json` +- **THEN** the `Bezwaar` schema MUST include an `x-openregister-lifecycle` block + with all transitions described in `bezwaar-lifecycle/spec.md` including + `ontvankelijkheidstoets_starten`, `in_behandeling_nemen`, `hoorzitting_plannen`, + `hoorzitting_afronden`, `advies_uitbrengen`, `beslissen`, `afronden`, + `niet_ontvankelijk_verklaren`, `intrekken`, and `hoorzitting_overslaan` + +#### Scenario: Bezwaar cannot skip ontvankelijkheidstoets without a valid transition + +- **GIVEN** a bezwaar in state `"ontvangen"` +- **WHEN** a PATCH request attempts to set `status: "in_behandeling"` directly + (skipping `"ontvankelijkheidstoets"`) +- **THEN** OR's lifecycle engine MUST reject the request with HTTP 422 +- **AND** the bezwaar MUST remain in state `"ontvangen"` + +#### Scenario: Hearing waiver skip requires HoorzittingAfzienGuard pass + +- **GIVEN** a bezwaar in state `"in_behandeling"` where the bezwaarmaker has NOT + filed a hearing waiver (`hoorrecht_afgezien: false`) +- **WHEN** a PATCH request attempts the `hoorzitting_overslaan` transition + (`status: "advies_uitgebracht"`) +- **THEN** `OCA\Procest\Lifecycle\HoorzittingAfzienGuard::allows()` MUST be invoked +- **AND** the guard MUST return `false` +- **AND** OR MUST respond with HTTP 422 +- **AND** the bezwaar MUST remain in state `"in_behandeling"` + +--- + +### Requirement: ParaferingService Status Constants SHALL Be Removed + +The constants `ParaferingService::STATUS_CONCEPT`, `STATUS_IN_PARAFERING`, `STATUS_TERUGGESTUURD`, and `STATUS_GEPARAFEERD` MUST be removed from `lib/Service/ParaferingService.php`. Callers that previously used these constants MUST be updated to use string literals matching the `x-openregister-lifecycle` enum values, or access the object's `lifecycle` field directly. + +#### Scenario: No STATUS_* constants remain in ParaferingService + +- **GIVEN** `lib/Service/ParaferingService.php` after migration +- **WHEN** the file is inspected +- **THEN** no `const STATUS_` declarations SHALL exist +- **AND** `composer check:strict` MUST pass with no reference errors + +#### Scenario: No direct lifecycle saveObject calls remain + +- **GIVEN** `lib/Service/ParaferingService.php` after migration +- **WHEN** the file is inspected +- **THEN** no call pattern matching `saveObject` with a `lifecycle` or `status` + field mutation for voorstel/parafeerroute/bezwaar MUST remain + without routing through OR's lifecycle engine (i.e., via a PATCH that OR handles) + +--- + +### Requirement: Automatic Actions on Transitions MUST Use Schema Hooks + +Automatic actions on voorstel/parafeerroute/bezwaar lifecycle transitions (send email, create task) MUST be dispatched via `x-openregister-hooks` entries in `procest_register.json` — Application.php event listeners that perform this dispatch MUST be removed. + +#### Scenario: Schema hook dispatches parafering notification via n8n + +- **GIVEN** a voorstel transitions to `"in_parafering"` +- **WHEN** OR's `HookExecutor` processes the `updated` event +- **THEN** the `procest-parafering-notification` n8n workflow MUST be triggered + asynchronously via `WorkflowEngineInterface::executeWorkflow()` +- **AND** no Application.php listener for `ObjectUpdatedEvent` that calls n8n + directly SHALL exist + +#### Scenario: Removed Application.php listeners do not interfere + +- **GIVEN** `lib/AppInfo/Application.php` after migration +- **WHEN** the file is inspected +- **THEN** no event listener registrations for voorstel/parafeerroute/bezwaar + lifecycle automatic-action dispatch SHALL remain +- **AND** `composer check:strict` MUST pass + +--- + +### Requirement: PHP Guard Classes MUST Implement Single-Method Interface + +Guard classes (`VoorstelSubmitGuard`, `BezwaarDeadlineGuard`, `HoorzittingAfzienGuard`) MUST be thin precondition checkers that return `bool` and MUST NOT call `WorkflowEngineInterface`, `ObjectService::saveObject()`, or any workflow engine adapter. + +#### Scenario: Guard class implements single allows() method + +- **GIVEN** `lib/Lifecycle/VoorstelSubmitGuard.php` +- **WHEN** the class is inspected +- **THEN** it MUST implement a single public method with signature + `allows(array $object): bool` +- **AND** it MUST NOT inject or call `WorkflowEngineInterface` +- **AND** it MUST NOT call `ObjectService::saveObject()` or any persistence method + +--- + +### Requirement: PHPUnit Tests MUST Cover Each Lifecycle Transition + +Each schema's lifecycle extension MUST have PHPUnit test coverage confirming +(a) allowed transitions pass, (b) disallowed transitions are rejected, and +(c) guard failures block the transition. + +#### Scenario: VoorstelLifecycleTest covers all transitions + +- **GIVEN** a `tests/Unit/Lifecycle/VoorstelLifecycleTest.php` exists +- **WHEN** the test suite runs +- **THEN** at minimum the following scenarios MUST be covered: + (a) `concept → in_parafering` succeeds when guard passes; + (b) `concept → in_parafering` fails when guard returns false; + (c) `geparafeerd → in_parafering` fails (invalid from-state); + (d) all five terminal state values are valid enum values + +#### Scenario: BezwaarLifecycleTest covers AWB sequence + +- **GIVEN** a `tests/Unit/Lifecycle/BezwaarLifecycleTest.php` exists +- **WHEN** the test suite runs +- **THEN** at minimum the following scenarios MUST be covered: + (a) sequential AWB status progression passes; + (b) skipping `ontvankelijkheidstoets` is rejected; + (c) `hoorzitting_overslaan` is blocked when hearing waiver flag is false; + (d) `intrekken` is accepted from all documented from-states + +## Non-Requirements + +- This spec does NOT modify the `status-transition-engine`, `workflow-definition-model`, + `workflow-import-export`, `visual-workflow-editor`, or `vth-workflow-templates` + spec bodies. +- This spec does NOT migrate historical workflow execution records. +- This spec does NOT change procest's public REST API surface (endpoints are + preserved; behaviour is delegated to OR's lifecycle engine). +- This spec does NOT cover procest's parafeerroute step-routing PHP logic + (step orchestration is not a single-object state machine and stays in PHP). + +## Dependencies + +- `openregister/openspec/specs/object-lifecycle` — OR's lifecycle pipeline. +- `openregister/openspec/specs/workflow-integration` — schema hooks for automatic + actions. +- `openregister/openspec/specs/workflow-engine-abstraction` — `WorkflowEngineInterface` + consumed by schema hooks. +- `hydra/openspec/changes/consume-or-workflow-engine-fleet-wide` — umbrella policy + spec; this is the procest implementation vehicle. + +## Cross-References + +- **procest/openspec/specs/bezwaar-lifecycle** — AWB status sequence source of truth + (spec body unchanged; transitions now enforced by OR lifecycle engine). +- **procest/openspec/specs/parafeerroute-engine** — step routing logic; `x-openregister-lifecycle` + covers route-level states only; per-step orchestration stays in PHP. +- **procest/openspec/specs/status-transition-engine** — guard evaluation and atomic + execution model; now implemented by OR's lifecycle engine (spec body unchanged). +- **consume-or-audit-trail-fleet-wide** — lifecycle transition audit entries follow + the `{app}.{domain}.{event}` convention. diff --git a/openspec/changes/migrate-status-engine-to-or-lifecycle/tasks.md b/openspec/changes/migrate-status-engine-to-or-lifecycle/tasks.md new file mode 100644 index 000000000..ebbdb63c4 --- /dev/null +++ b/openspec/changes/migrate-status-engine-to-or-lifecycle/tasks.md @@ -0,0 +1,162 @@ +# Tasks: migrate-status-engine-to-or-lifecycle + +All tasks are `[procest]`. Estimates: S = half-day, M = 1–2 days, L = 3+ days. + +> **Scope adjustment (2026-05-11):** investigation found that procest has NO +> `StatusTransitionService`, no `StateMachine*`, and no `WorkflowEngine*` +> service classes. State semantics are scattered as `STATUS_*` constants and +> conditional checks across `ParaferingService` (`STATUS_CONCEPT`, +> `STATUS_IN_PARAFERING`, etc.) and the bezwaar/voorstel workflow files — +> not a centralised engine that can be replaced in one PR. +> +> Per ADR-031 (schema-declarative business logic) the right migration is to +> move each schema's state machine into its `x-openregister-lifecycle` +> extension and let OR's lifecycle engine handle transitions. That requires +> per-schema work that does not fit a single focused PR. +> +> This commit records the umbrella rule + the per-schema migration plan: each +> stateful schema (voorstel, bezwaar, parafeerroute, hoorzitting) gets its own +> follow-up PR that adds `x-openregister-lifecycle` + removes the matching +> PHP `STATUS_*` constants. + +--- + +## [procest] Voorstel Schema Migration + +### P-1. Add x-openregister-lifecycle to Voorstel schema (M) + +- [x] P-1.1 Add the `x-openregister-lifecycle` block for the `Voorstel` schema to + `lib/Settings/procest_register.json`. The block MUST declare: + - `property: "lifecycle"`, `initial: "concept"` + - Transitions: `indienen` (concept → in_parafering, requires `VoorstelSubmitGuard`), + `terugsturen` (in_parafering → teruggestuurd), `completeren` (in_parafering → geparafeerd), + `afwijzen` (concept|in_parafering → afgewezen), `heropenen` (teruggestuurd → concept) + - **files:** `lib/Settings/procest_register.json` + - **Acceptance:** `openspec validate --strict` passes; lifecycle block is present on the + `Voorstel` schema; repair step registers the schema without errors on the dev environment. + +- [x] P-1.2 Create `lib/Lifecycle/VoorstelSubmitGuard.php` with a single method + `allows(array $object): bool` that validates the required `onderwerp` and `type` + fields are non-empty. + - **files:** `lib/Lifecycle/VoorstelSubmitGuard.php` + - **Acceptance:** `composer check:strict` passes; the guard returns `false` when + `onderwerp` is empty and `true` when all required fields are present. + +--- + +## [procest] Parafeerroute Schema Migration + +### P-2. Add x-openregister-lifecycle to Parafeerroute schema (M) + +- [x] P-2.1 Add the `x-openregister-lifecycle` block for the `Parafeerroute` schema to + `lib/Settings/procest_register.json`. The block MUST declare: + - `property: "status"`, `initial: "actief"` + - Transitions: `afronden` (actief → afgerond), `annuleren` (actief → geannuleerd) + - **files:** `lib/Settings/procest_register.json` + - **Acceptance:** Repair step registers the updated schema; `status` field accepts only + `["actief", "afgerond", "geannuleerd"]`; invalid transitions return HTTP 422. + +--- + +## [procest] Bezwaar Schema Migration + +### P-3. Add x-openregister-lifecycle to Bezwaar schema (L) + +- [x] P-3.1 Add the `x-openregister-lifecycle` block for the `Bezwaar` schema to + `lib/Settings/procest_register.json`. The block MUST declare all ten AWB transitions + from the `bezwaar-lifecycle` spec, with the `hoorzitting_overslaan` transition + requiring `HoorzittingAfzienGuard`. + - **files:** `lib/Settings/procest_register.json` + - **Acceptance:** All ten transitions registered; sequential AWB progression allowed; + out-of-sequence transitions (e.g. ontvangen → in_behandeling) rejected with HTTP 422. + +- [x] P-3.2 Create `lib/Lifecycle/HoorzittingAfzienGuard.php` with a single method + `allows(array $object): bool` that returns `true` if `hoorrecht_afgezien === true`. + - **files:** `lib/Lifecycle/HoorzittingAfzienGuard.php` + - **Acceptance:** Guard returns `false` when `hoorrecht_afgezien` is `false` or absent; + `composer check:strict` passes. + +- [x] P-3.3 Create `lib/Lifecycle/BezwaarDeadlineGuard.php` with a single method + `allows(array $object): bool` that checks whether `processingDeadline` has not + been exceeded for deadline-sensitive transitions. + - **files:** `lib/Lifecycle/BezwaarDeadlineGuard.php` + - **Acceptance:** Guard returns `false` when current date exceeds `processingDeadline`; + returns `true` when deadline has not passed or is not set; `composer check:strict` passes. + +--- + +## [procest] ParaferingService Cleanup + +### P-4. Remove STATUS_* constants from ParaferingService (S) + +- [x] P-4.1 Remove the four `const STATUS_*` declarations from + `lib/Service/ParaferingService.php`: + - `STATUS_CONCEPT`, `STATUS_IN_PARAFERING`, `STATUS_TERUGGESTUURD`, `STATUS_GEPARAFEERD` + - Update all references in `ParaferingService` itself to use string literals. + - **files:** `lib/Service/ParaferingService.php` + - **Acceptance:** No `const STATUS_` declarations remain; `composer check:strict` passes + with no undefined constant errors. + +- [x] P-4.2 Update all callers of `ParaferingService::STATUS_*` constants across the codebase + (controllers, listeners, tests) to use the string literals that match the + `x-openregister-lifecycle` enum values. + - **files:** any PHP file that references `ParaferingService::STATUS_*` + - **Acceptance:** `grep -rn "STATUS_CONCEPT\|STATUS_IN_PARAFERING\|STATUS_TERUGGESTUURD\|STATUS_GEPARAFEERD" + lib/` returns zero results; `composer check:strict` passes. + +- [x] P-4.3 Remove or refactor direct `saveObject` calls in `ParaferingService` that set + `lifecycle` or `status` fields on voorstel/parafeerroute/bezwaar objects. Replace + with PATCH requests through OR's object endpoint so OR's lifecycle engine validates + the transition. + - **files:** `lib/Service/ParaferingService.php` + - **Acceptance:** No `saveObject` call with `lifecycle` or `status` mutation remains + in the service; transitions are validated by OR; `composer check:strict` passes. + +--- + +## [procest] Schema Hook Migration + +### P-5. Replace Application.php lifecycle action listeners with schema hooks (M) + +- [x] P-5.1 Remove Application.php event listener registrations for any + `ObjectCreatedEvent`/`ObjectUpdatedEvent` listeners that trigger automatic actions + (notifications, task creation) on voorstel/parafeerroute/bezwaar lifecycle transitions. + - **files:** `lib/AppInfo/Application.php` + - **Acceptance:** No such listener registrations remain; `composer check:strict` passes. + +- [x] P-5.2 Add `x-openregister-hooks` entries to the affected schemas in + `lib/Settings/procest_register.json` for the `updated` event, targeting the existing + n8n workflows for parafering notification and completion actions. Use `mode: "async"`. + - **files:** `lib/Settings/procest_register.json` + - **Acceptance:** Schema hooks are declared on `Voorstel` and `Parafeerroute` schemas; + the n8n workflow IDs match existing deployed workflows on the dev environment. + +--- + +## [procest] Test Coverage + +### P-6. PHPUnit lifecycle tests for all three schemas (M) + +- [x] P-6.1 Create `tests/Unit/Lifecycle/VoorstelLifecycleTest.php` covering: + (a) `concept → in_parafering` succeeds when `VoorstelSubmitGuard` passes; + (b) `concept → in_parafering` is blocked when guard returns false; + (c) `geparafeerd → in_parafering` is rejected (invalid transition); + (d) all five lifecycle enum values are valid strings. + - **files:** `tests/Unit/Lifecycle/VoorstelLifecycleTest.php` + - **Acceptance:** All test cases pass; `composer test` exits 0. + +- [x] P-6.2 Create `tests/Unit/Lifecycle/BezwaarLifecycleTest.php` covering: + (a) sequential AWB status progression passes; + (b) skipping `ontvankelijkheidstoets` (ontvangen → in_behandeling) is rejected; + (c) `hoorzitting_overslaan` blocked when `hoorrecht_afgezien` is false; + (d) `intrekken` accepted from `ontvangen`, `ontvankelijkheidstoets`, + `in_behandeling`, and `hoorzitting_gepland`. + - **files:** `tests/Unit/Lifecycle/BezwaarLifecycleTest.php` + - **Acceptance:** All test cases pass; `composer test` exits 0. + +- [x] P-6.3 Create `tests/Unit/Lifecycle/ParaferingServiceStepTest.php` confirming + that step-routing methods (`activateNextStep`, `getActiveStep`, `recordStepAction`) + do NOT set `lifecycle` or `status` on the parent voorstel or parafeerroute objects. + - **files:** `tests/Unit/Lifecycle/ParaferingServiceStepTest.php` + - **Acceptance:** Mocked `ObjectService` confirms `saveObject` is not called with a + `lifecycle`/`status` mutation from step-routing methods; `composer test` exits 0. diff --git a/openspec/changes/pluggable-integration-registry/README.md b/openspec/changes/pluggable-integration-registry/README.md new file mode 100644 index 000000000..f0a57a378 --- /dev/null +++ b/openspec/changes/pluggable-integration-registry/README.md @@ -0,0 +1,7 @@ +# pluggable-integration-registry (cross-repo stub) + +Authoritative spec: [ConductionNL/openregister → openspec/changes/pluggable-integration-registry](https://github.com/ConductionNL/openregister/tree/development/openspec/changes/pluggable-integration-registry). + +Tracking issue: https://github.com/ConductionNL/openregister/issues/1307 + +Stub exists so Hydra's sibling-based dependency check can resolve cross-repo `depends_on`. diff --git a/openspec/changes/pluggable-integration-registry/hydra.json b/openspec/changes/pluggable-integration-registry/hydra.json new file mode 100644 index 000000000..009863267 --- /dev/null +++ b/openspec/changes/pluggable-integration-registry/hydra.json @@ -0,0 +1,12 @@ +{ + "schema_version": 2, + "spec_slug": "pluggable-integration-registry", + "app": "openregister", + "repo": "https://github.com/ConductionNL/openregister", + "issue": "https://github.com/ConductionNL/openregister/issues/1307", + "depends_on": [], + "cross_repo_stub": true, + "note": "Cross-repo dependency stub. Authoritative spec lives in ConductionNL/openregister. Do NOT edit here.", + "pipeline": {}, + "cycles": [] +} diff --git a/openspec/changes/task-management/hydra.json b/openspec/changes/task-management/hydra.json new file mode 100644 index 000000000..79bd6793b --- /dev/null +++ b/openspec/changes/task-management/hydra.json @@ -0,0 +1,12 @@ +{ + "schema_version": 2, + "spec_slug": "task-management", + "app": "procest", + "repo": "https://github.com/ConductionNL/procest", + "depends_on": [ + "pluggable-integration-registry" + ], + "issue": "", + "pipeline": {}, + "cycles": [] +} diff --git a/src/services/pdokService.js b/src/services/pdokService.js index e19cb06b2..8c0765c71 100644 --- a/src/services/pdokService.js +++ b/src/services/pdokService.js @@ -1,124 +1,211 @@ /** - * PDOK Locatieserver API client. + * PDOK address lookup shim. * - * Provides suggest (autocomplete), lookup, free-text search, and reverse geocoding - * using the PDOK Locatieserver v3.1 API. + * Routes all PDOK Locatieserver access through the openconnector PDOK adapter + * at /index.php/apps/openconnector/api/pdok/{suggest|lookup|free|reverse}. + * Direct browser calls to api.pdok.nl are NOT permitted from this app — see + * Hydra umbrella `shared-pdok-via-openconnector` (ADR-022). * - * @see https://api.pdok.nl/bzk/locatieserver/search/v3_1/ + * Exports six functions with the same signatures as the original + * pdokService.js so existing procest callers do not need to change: + * suggest(query), lookup(id), free(query, rows), reverse(lat, lng), + * extractCoordinates(result), formatAddress(result). + * + * Degraded modes: + * - openconnector returns 503 (PDOK unavailable / circuit open): the calling + * function resolves with `null` and the response `message_key` is attached + * to the module's `lastWarning` for display by the caller. + * - openconnector not installed (HTTP 404): the shim sets a non-blocking + * warning and resolves with an empty result — the form must remain submittable. + * + * @see hydra/openspec/changes/shared-pdok-via-openconnector/design.md */ -const BASE_URL = 'https://api.pdok.nl/bzk/locatieserver/search/v3_1' +import axios from '@nextcloud/axios' +import { generateUrl } from '@nextcloud/router' + +const BASE_URL = generateUrl('/apps/openconnector/api/pdok') let debounceTimer = null +/** + * Module-level last warning for the most recent degraded call. + * + * Components can read this after awaiting a shim call to surface an + * inline message. It is reset to null at the start of every successful call. + * + * @type {{messageKey: string, status: number}|null} + */ +export let lastWarning = null + +/** + * Reset the module-level warning state. + */ +function clearWarning() { + lastWarning = null +} + +/** + * Record a degraded-state warning that the caller can surface in the UI. + * + * @param {string} messageKey i18n key from the openconnector response body. + * @param {number} status HTTP status that triggered the degraded path. + */ +function recordWarning(messageKey, status) { + lastWarning = { messageKey, status } +} + +/** + * Handle a shim network error. + * + * 503: returns the wrapped null result with the message_key surfaced. + * 404: records the openconnector-absent warning and returns an empty result. + * Other errors: rethrows so the caller can decide. + * + * @param {Error} error The axios error. + * @param {*} fallback The result shape to return on degraded paths. + * @return {*} The fallback or null based on degraded type. + * @throws {Error} For unhandled errors. + */ +function handleNetworkError(error, fallback) { + const status = error?.response?.status + if (status === 503) { + const messageKey = error?.response?.data?.message_key || 'pdok.unavailable' + recordWarning(messageKey, 503) + return null + } + if (status === 404) { + recordWarning('pdok.openconnector_missing', 404) + return fallback + } + throw error +} + /** * Suggest addresses as the user types (autocomplete). - * Debounced at 200ms to avoid excessive API calls. * - * @param {string} query The search query (min 3 characters) - * @return {Promise} Array of suggestion objects + * Debounced at 200ms to avoid excessive API calls. Returns an array of + * normalized suggestion objects from openconnector; on 503 returns null and + * sets `lastWarning`; on 404 returns an empty array and sets `lastWarning`. + * + * @param {string} query Search query (min 3 characters). + * @return {Promise} Suggestions array, empty array, or null. */ export async function suggest(query) { if (!query || query.length < 3) { return [] } - return new Promise((resolve, reject) => { clearTimeout(debounceTimer) debounceTimer = setTimeout(async () => { + clearWarning() try { - const params = new URLSearchParams({ - q: query, - rows: 10, - }) - const response = await fetch(`${BASE_URL}/suggest?${params}`) - if (!response.ok) { - throw new Error(`PDOK suggest failed: ${response.status}`) - } - const data = await response.json() - resolve(data.response?.docs || []) + const response = await axios.get(`${BASE_URL}/suggest`, { params: { q: query } }) + resolve(response.data?.docs || []) } catch (error) { - reject(error) + try { + resolve(handleNetworkError(error, [])) + } catch (rethrow) { + reject(rethrow) + } } }, 200) }) } /** - * Look up a specific result by its ID (returned from suggest). + * Look up a specific result by its openconnector/PDOK id. * - * @param {string} id The PDOK object ID - * @return {Promise} The full result object with geometry + * @param {string} id The PDOK object id. + * @return {Promise} The full result object, or null when degraded. */ export async function lookup(id) { if (!id) { return null } - - const params = new URLSearchParams({ id }) - const response = await fetch(`${BASE_URL}/lookup?${params}`) - if (!response.ok) { - throw new Error(`PDOK lookup failed: ${response.status}`) + clearWarning() + try { + const response = await axios.get(`${BASE_URL}/lookup`, { params: { id } }) + return response.data?.docs?.[0] || null + } catch (error) { + return handleNetworkError(error, null) } - const data = await response.json() - return data.response?.docs?.[0] || null } /** * Free-text search for addresses/locations. * - * @param {string} query The search query - * @param {number} rows Max results (default 10) - * @return {Promise} Array of result objects + * @param {string} query Search query. + * @param {number} rows Max results (default 10). + * @return {Promise} Results array, empty array, or null. */ export async function free(query, rows = 10) { if (!query) { return [] } - - const params = new URLSearchParams({ q: query, rows: String(rows) }) - const response = await fetch(`${BASE_URL}/free?${params}`) - if (!response.ok) { - throw new Error(`PDOK free search failed: ${response.status}`) + clearWarning() + try { + const response = await axios.get(`${BASE_URL}/free`, { params: { q: query, rows } }) + return response.data?.docs || [] + } catch (error) { + return handleNetworkError(error, []) } - const data = await response.json() - return data.response?.docs || [] } /** - * Reverse geocode coordinates to find the nearest address. + * Reverse-geocode coordinates to find the nearest address. * - * @param {number} lat Latitude (WGS84) - * @param {number} lng Longitude (WGS84) - * @return {Promise} The nearest address object + * @param {number} lat Latitude (WGS84). + * @param {number} lng Longitude (WGS84). + * @return {Promise} Nearest address, or null when degraded. */ export async function reverse(lat, lng) { - const params = new URLSearchParams({ - type: 'adres', - lat: String(lat), - lon: String(lng), - rows: '1', - }) - const response = await fetch(`${BASE_URL}/reverse?${params}`) - if (!response.ok) { - throw new Error(`PDOK reverse failed: ${response.status}`) + clearWarning() + try { + const response = await axios.get(`${BASE_URL}/reverse`, { params: { lat, lng } }) + return response.data?.docs?.[0] || null + } catch (error) { + return handleNetworkError(error, null) } - const data = await response.json() - return data.response?.docs?.[0] || null } /** - * Extract WGS84 coordinates from a PDOK result's centroide_ll field. - * The field is in WKT format: "POINT(lng lat)". + * Extract WGS84 coordinates from a result's location or centroide_ll field. * - * @param {object} result A PDOK result object - * @return {{ lat: number, lng: number }|null} Coordinates or null + * Supports two input shapes: + * - A canonical normalized PostalAddress with `location.coordinates = [lng, lat]`. + * - A raw PDOK document with `centroide_ll = "POINT(lng lat)"`. + * + * Pure utility — no network calls, no module state. + * + * @param {object|string} resultOrWkt A PDOK result object or a raw WKT string. + * @return {{ lat: number, lng: number }|null} Coordinates or null. */ -export function extractCoordinates(result) { - if (!result?.centroide_ll) { +export function extractCoordinates(resultOrWkt) { + if (!resultOrWkt) { return null } - const match = result.centroide_ll.match(/POINT\(([^ ]+) ([^ ]+)\)/) + if (typeof resultOrWkt === 'string') { + return parseWkt(resultOrWkt) + } + if (resultOrWkt.location?.coordinates) { + const [lng, lat] = resultOrWkt.location.coordinates + return { lat, lng } + } + if (resultOrWkt.centroide_ll) { + return parseWkt(resultOrWkt.centroide_ll) + } + return null +} + +/** + * Parse a WKT POINT(lng lat) string into {lat, lng}. + * + * @param {string} wkt The WKT input. + * @return {{ lat: number, lng: number }|null} Parsed coordinates or null. + */ +function parseWkt(wkt) { + const match = wkt.match(/POINT\(([^ ]+) ([^ ]+)\)/) if (!match) { return null } @@ -129,14 +216,17 @@ export function extractCoordinates(result) { } /** - * Format a PDOK result as a human-readable address string. + * Format a result as a human-readable address string. + * + * Pure utility — no network calls. Accepts both canonical normalized objects + * (with `displayName`) and raw PDOK results (with `weergavenaam`). * - * @param {object} result A PDOK result object - * @return {string} Formatted address + * @param {object} result A result object. + * @return {string} Formatted address. */ export function formatAddress(result) { if (!result) { return '' } - return result.weergavenaam || result.display || '' + return result.displayName || result.weergavenaam || result.display || '' } diff --git a/src/utils/dashboardHelpers.js b/src/utils/dashboardHelpers.js index ec45ae289..2e4c48a28 100644 --- a/src/utils/dashboardHelpers.js +++ b/src/utils/dashboardHelpers.js @@ -3,6 +3,7 @@ * overdue extraction, activity aggregation, and my work item merging. */ +import { translate as t } from '@nextcloud/l10n' import { isCaseOverdue, getDaysRemaining, formatDeadlineCountdown } from './caseHelpers.js' import { prioritySortWeight } from './taskHelpers.js' import { isTerminalStatus } from './taskLifecycle.js' @@ -78,7 +79,7 @@ export function aggregateByStatus(openCases, statusTypes) { } for (const c of openCases) { - const name = statusIdToName.get(c.status) || c.status || 'Unknown' + const name = statusIdToName.get(c.status) || c.status || t('procest', 'Unknown') statusMap.set(name, (statusMap.get(name) || 0) + 1) } @@ -97,7 +98,7 @@ export function aggregateByStatus(openCases, statusTypes) { export function getOverdueCases(openCases, caseTypes) { const typeMap = new Map() for (const ct of caseTypes) { - typeMap.set(ct.id, ct.title || ct.name || 'Unknown') + typeMap.set(ct.id, ct.title || ct.name || t('procest', 'Unknown')) } return openCases @@ -106,7 +107,7 @@ export function getOverdueCases(openCases, caseTypes) { id: c.id, identifier: c.identifier || '—', title: c.title || '—', - caseTypeName: typeMap.get(c.caseType) || 'Unknown', + caseTypeName: typeMap.get(c.caseType) || t('procest', 'Unknown'), daysOverdue: Math.abs(getDaysRemaining(c.deadline)), handler: c.assignee || '—', })) @@ -334,7 +335,7 @@ export const STALLED_THRESHOLD_DAYS = 7 export function getDeadlineAlerts(openCases, caseTypes, warningDays = DEADLINE_WARNING_DAYS) { const typeMap = new Map() for (const ct of caseTypes) { - typeMap.set(ct.id, ct.title || ct.name || 'Unknown') + typeMap.set(ct.id, ct.title || ct.name || t('procest', 'Unknown')) } const today = new Date() @@ -354,7 +355,7 @@ export function getDeadlineAlerts(openCases, caseTypes, warningDays = DEADLINE_W id: c.id, title: c.title || '\u2014', identifier: c.identifier || '\u2014', - caseTypeName: typeMap.get(c.caseType) || 'Unknown', + caseTypeName: typeMap.get(c.caseType) || t('procest', 'Unknown'), handler: c.assignee || '\u2014', } @@ -428,7 +429,7 @@ export function getTaskDueReminders(tasks, warningDays = DEADLINE_WARNING_DAYS) export function getStalledCases(openCases, caseTypes, stalledDays = STALLED_THRESHOLD_DAYS) { const typeMap = new Map() for (const ct of caseTypes) { - typeMap.set(ct.id, ct.title || ct.name || 'Unknown') + typeMap.set(ct.id, ct.title || ct.name || t('procest', 'Unknown')) } const today = new Date() @@ -451,7 +452,7 @@ export function getStalledCases(openCases, caseTypes, stalledDays = STALLED_THRE id: c.id, title: c.title || '\u2014', identifier: c.identifier || '\u2014', - caseTypeName: typeMap.get(c.caseType) || 'Unknown', + caseTypeName: typeMap.get(c.caseType) || t('procest', 'Unknown'), daysSinceActivity, handler: c.assignee || '\u2014', }) diff --git a/src/utils/doorlooptijdHelpers.js b/src/utils/doorlooptijdHelpers.js index 3963e962d..8d64d75a1 100644 --- a/src/utils/doorlooptijdHelpers.js +++ b/src/utils/doorlooptijdHelpers.js @@ -5,6 +5,8 @@ * monthly trends, at-risk case identification, and performance table data. */ +import { translate as t } from '@nextcloud/l10n' + /** * Parse an ISO 8601 duration string to calendar days. * @@ -111,7 +113,7 @@ export function computeSlaCompliance(completedCases, caseTypes) { const ct = caseTypeMap.get(ctId) byType.set(ctId, { id: ctId, - name: ct?.title || ct?.name || 'Unknown', + name: ct?.title || ct?.name || t('procest', 'Unknown'), total: 0, withinSla: 0, totalDays: 0, @@ -282,7 +284,7 @@ export function getAtRiskCases(openCases, caseTypes, thresholdPct) { id: c.id, title: c.title || '', identifier: c.identifier || '', - caseTypeName: ct?.title || ct?.name || 'Unknown', + caseTypeName: ct?.title || ct?.name || t('procest', 'Unknown'), targetDays, elapsedDays, remainingDays, @@ -317,7 +319,7 @@ export function computePerformanceTable(completedCases, caseTypes) { const targetDays = ct.processingDeadline ? parseDurationToDays(ct.processingDeadline) : null byType.set(ct.id, { id: ct.id, - name: ct.title || ct.name || 'Unknown', + name: ct.title || ct.name || t('procest', 'Unknown'), targetDays, totalDays: 0, total: 0, diff --git a/src/views/cases/components/EmailComposer.vue b/src/views/cases/components/EmailComposer.vue index 494c8537f..254975bfa 100644 --- a/src/views/cases/components/EmailComposer.vue +++ b/src/views/cases/components/EmailComposer.vue @@ -46,7 +46,7 @@ @@ -56,7 +56,7 @@ {{ t('procest', 'Available variables') }} @@ -163,6 +163,9 @@ export default { }, }, methods: { + formatVariable(varName) { + return '{{' + varName + '}}' + }, onTemplateSelected(template) { if (!template) return this.form.subject = template.subjectPattern || '' diff --git a/src/views/dashboard/DeadlineAlerts.vue b/src/views/dashboard/DeadlineAlerts.vue index d3812a3ec..e57f840b9 100644 --- a/src/views/dashboard/DeadlineAlerts.vue +++ b/src/views/dashboard/DeadlineAlerts.vue @@ -75,15 +75,13 @@ export default {