Skip to content

Release: merge development into beta#18

Open
github-actions[bot] wants to merge 583 commits into
betafrom
development
Open

Release: merge development into beta#18
github-actions[bot] wants to merge 583 commits into
betafrom
development

Conversation

@github-actions
Copy link
Copy Markdown
Contributor

Automated PR to sync development changes to beta for beta release.

Merging this PR will trigger the beta release workflow.

Reminder: Add a major, minor, or patch label to this PR to control the version bump. Default is patch.

rubenvdlinde and others added 27 commits May 11, 2026 12:05
…anifest, keep calc service (#373)

Aligns the legesberekening surface with the manifest renderer pattern
established by procest-manifest-v1 (#320) and mirrored by the
parafeerroute refactor: tariff and history CRUD is now declared in
src/manifest.json against three OpenRegister schemas, while the
domain logic in LegesCalculationService / LegesExportService and the
non-CRUD endpoints (/api/leges/calculate, /recalculate, /verrekening,
/teruggaaf, /export) remain untouched.

Schemas (lib/Settings/procest_register.json)
- legesverordening: municipal fee regulation (year, effectiveDate,
  status, isActive, globalMaximum, reference)
- legesartikel: tariff line with type vast | percentage | staffel |
  maximum | combinatie, grondslagField, brackets, subArtikelen,
  category, order, scoped by verordening
- legesberekening: per-case calculation result with breakdown,
  version history (version, previousVersion, previousTotal,
  difference, correctionReason), status concept | opgelegd |
  verrekend | teruggegeven, exportedAt + exportFormat

All three are registered on the procest register so OpenRegister
exposes the standard CRUD endpoints — no bespoke controllers needed.

Manifest pages (src/manifest.json)
- Legesverordeningen — index of regulations, columns name/year/
  effectiveDate/status/isActive
- LegesverordeningDetail — detail with sidebar tabs Overview,
  Artikelen (related-index on legesartikel filtered by verordening,
  with create/edit/delete), Audit trail
- Legesberekeningen — index of calculation history, columns case/
  verordening/total/version/status/calculatedBy/calculatedAt,
  read-only actions
- LegesberekeningDetail — detail with Overview and Audit trail
- Two new settings-section menu entries (Legesverordeningen at
  order 96, Legesberekeningen at order 97)

Preserved
- LegesController::calculate, recalculate, verrekening, teruggaaf,
  export — all five remain (domain endpoints, no CRUD proxying)
- LegesCalculationService (vast / percentage / staffel / maximum /
  combinatie rules + verrekening + teruggaaf) — untouched
- LegesExportService (CSV / XML / JSON export to financial system)
  — untouched
- /api/leges/* routes in appinfo/routes.php — all five kept

Deletions
- None required. The pre-existing controller already exposes only
  domain endpoints; no CRUD methods, routes, or Vue views for
  leges tariffs / history existed prior to this change.

Validation
- jq parses both JSON files; new schemas + register list verified
- node tests/validate-manifest.js — structural lint PASS, 21 pages
- php -l on LegesController, LegesCalculationService,
  LegesExportService, appinfo/routes.php — no syntax errors
… manifest, keep workflow + reminder (#374)

Move adviesAanvraag CRUD (index/show/create/update/destroy) off the
bespoke AdviceController onto the manifest renderer (OpenRegister
objects endpoint). The controller now exposes only the two operations
the renderer cannot replicate: status transitions (which trigger the
adviseur / requester notifications) and manual reminder dispatch.

Deletions
- AdviceController: index, create, show, update, destroy (CRUD proxies)
- AdviceService: createAdvice, receiveAdvice (replaced by transitionStatus)
- routes.php: advice#index/create/show/update/destroy
- adviceApi.js: getAdvice, updateAdvice, deleteAdvice, createAdvice
  (replaced by createAdviceWithNotification chaining OR + transition)

Kept (workflow / cron / detail panel)
- AdviceController::transitionStatus, dispatchReminder
- AdviceService::transitionStatus, dispatchReminder, applyWorkflowGuard,
  getOpenAdvice, expireAdvice, getAdviceForCase
- BackgroundJob/AdviceDeadlineJob (entirely — now calls dispatchReminder)
- src/views/cases/components/AdviesPanel.vue (case-detail sidebar tab)
- src/views/cases/components/AdviesAanvraagDialog.vue (workflow trigger)

New manifest pages
- index: /advice  (admin view across all advice requests)
- detail: /advice/:id  (requester / advisor view)
- CaseDetail.sidebarTabs: new "advies" tab rendered via AdviesPanel

Schema adviesAanvraag (lib/Settings/procest_register.json) unchanged.
… manifest, keep middleware + provision (#375)

Generic tenant CRUD (list / create / update / delete) no longer needs a
bespoke controller, service wrapper, Vue list page, or REST routes — the
shared manifest renderer drives those declaratively against the tenant
schema in procest_register.json. Domain-specific logic stays in PHP.

Deletions
- lib/Controller/TenantController::index() / ::create() (proxy CRUD)
- lib/Service/TenantService::createTenant() and its slugify() helper
- appinfo/routes.php — tenant#index, tenant#create
- src/services/tenantApi.js (whole file — manifest pages talk to OR)
- src/views/settings/tabs/TenantSettingsTab.vue (orphan; replaced by
  manifest index page)
- src/views/cases/components/TenantSwitcher.vue (orphan; never mounted)

Preserved (deliberate)
- lib/Middleware/TenantMiddleware — cross-cutting tenant scoping;
  unchanged, still in DI and still exempts TenantController.
- lib/Controller/TenantController::provision() / ::usage() / ::current()
  — provisioning workflow, resource-usage aggregation, and current-tenant
  resolution are domain logic the manifest cannot express.
- lib/Service/TenantService::provisionTenant(), ::getResourceUsage(),
  ::getTenantForUser(), ::getTenantByGroupId(), ::isUserInTenant(),
  ::isPlatformAdmin() — all kept.
- appinfo/routes.php — tenant#current, tenant#provision, tenant#usage.

New manifest pages (src/manifest.json)
- Tenants (index, /settings/tenants) — schema-driven list with row
  actions for provision (emit) and view-usage (navigate to detail).
- TenantDetail (detail, /settings/tenants/:id) — three sidebar tabs:
  Overview (data + metadata), Usage (data), Provisioning history
  (audit-trail). Header actions for provision + refresh-usage.
- Menu entry "Tenants" added to the settings section with
  permission: "admin" so non-admins never see the link.

Admin-only guarantee
- The new menu entry is gated by CnAppNav's permission filter
  (permission: "admin"). src/App.vue now injects "admin" into the
  permissions array when window.OC.isUserAdmin() is true, matching the
  backend TenantService::isPlatformAdmin() check.
- Row / header action buttons also carry permission: "admin" so
  CnRowActions / CnActionsBar hide them for non-admins.
- The three preserved REST endpoints (provision / usage) keep their
  isPlatformAdmin() check — backend authority is unchanged.
…, declare manifest pages (#376)

Generic CRUD on parafeerroute objects is now served by OpenRegister's
auto-exposed /api/objects/<register>/<schema> endpoints and rendered by
the @conduction/nextcloud-vue manifest engine (CnIndexPage + CnDetailPage).
The bespoke ParafeerRouteController/Service/Vue CRUD layer is removed.

Workflow engine logic (start, complete-step, skip-step, add-step) is
preserved as legitimate domain endpoints on /api/parafeer-route/voorstel/*.

Deleted:
- ParafeerRouteController::{index, show, create, update, destroy}
- ParafeerRouteService::{listRoutes, getRoute, createRoute, updateRoute,
  deleteRoute, clearExistingDefault, bootstrapRoute}
- src/views/settings/components/ParafeerRoutesTab.vue
- src/views/settings/components/ParafeerRouteDialog.vue
- src/views/settings/ParafeerRouteAdmin.vue (already orphaned)
- parafeerRouteApi.js CRUD wrappers (listRoutes, getRoute, createRoute,
  updateRoute, deleteRoute)
- 5 CRUD route entries from appinfo/routes.php
- ParafeerRoutesTab section in AdminRoot.vue

Added:
- Manifest page id=Parafeerroutes (type=index, /settings/parafeerroutes)
- Manifest page id=ParafeerrouteDetail (type=detail, /settings/parafeerroutes/:id)
- Menu entry id=ParafeerroutesMenu under settings section

Schema (lib/Settings/procest_register.json#parafeerroute) was already
complete (name, voorstelType, caseType, steps[], isDefault, description) —
no schema changes required.

Kept as domain logic (engine, not CRUD):
- ParafeerRouteController::{start, completeStep, skipStep, addStep}
- ParafeerRouteService::{startParafering, completeStep, skipStep,
  addAdhocStep, advanceVoorstel, activateStep, normalizeSteps,
  appendAuditTrail, bootstrapVoorstel, requireConfig, requireUserId, toArray}
- src/views/settings/components/ParafeerStapEditor.vue (still useful as
  step-editor; not currently re-mounted under detail page — flagged for
  human review whether to expose it via customComponents slot)
- src/utils/parafeerEngine.js helpers (getNextStep, isActiveActor, etc.)
- AddStepDialog.vue, SkipStepDialog.vue (call engine endpoints)
…ia manifest, keep transfer + public-token logic (#377)

The case-sharing feature owned controller + service methods that were
just thin proxies onto OpenRegister CRUD. Those move to the manifest
renderer; the share-token cryptography, audit logging, transfer
workflow and unauthenticated public-token endpoints stay in app code.

Deleted (pure OR proxies):
- CaseSharingController::listShares (GET /api/shares/{caseId})
- CaseSharingController::modifyShare (PUT /api/shares/{shareId})
- CaseSharingController::listPartners (GET /api/partners)
- CaseSharingController::createPartner (POST /api/partners)
- CaseSharingController::updatePartner (PUT /api/partners/{partnerId})
- CaseSharingService::getSharesByCase
- CaseSharingService::modifyShare
- CaseSharingService::listPartners
- CaseSharingService::createPartner (groupId derivation should be a
  schema default or pre-save hook)
- CaseSharingService::updatePartner

Kept (domain logic, not CRUD):
- CaseSharingController::createShare — generates 128-bit token, applies
  default field exclusions, writes audit log
- CaseSharingController::revokeShare — writes audit log + tracks
  revokedBy + revokedAt (soft delete with attribution)
- CaseSharingController::initiateTransfer / handleTransfer — workflow
  endpoints around CaseTransferService accept/reject
- PublicShareController (all four methods) — unauthenticated token-
  based access with password lockout
- CaseSharingService: generateToken, createTokenShare, createPartnerShare,
  revokeShare, validateToken, getFilteredCaseData, maskBsn,
  recordFailedAttempt, resetFailedAttempts, storeExternalDocument

Added schemas to lib/Settings/procest_register.json (caseShare,
partnerOrganization, casetransfer) so the manifest renderer can serve
them via OpenRegister. SettingsService slug mapping aligned to the
'casetransfer' slug.

Added manifest pages:
- CaseShares  (index)  /cases/:caseId/shares      schema=caseShare
- Partners    (index)  /settings/partners         schema=partnerOrganization
- PartnerDetail (detail) /settings/partners/:id
- Transfers   (index)  /transfers                 schema=casetransfer
- TransferDetail (detail) /transfers/:id

Menu entries added: Transfers (main, order 80), Partner organisations
(settings, order 97).

Security guarantees preserved: token generation via random_bytes(16),
bcrypt password hashing, 5-attempt 15-minute lockout, field-level
exclusions, BSN masking, public endpoints require valid token + remain
CSRF-exempt.
Add OpenSpec change proposal for a reusable MapComponent.vue that wraps
CnMapWidget and serves as the shared map surface for case detail map tab,
dashboard map widget, and public case page.

Includes proposal, design, tasks (T01-T05 + V01-V04), and REQ-MC-1..6
delta spec covering the wrapper, prop API, events, interactive mode,
NL Design System / accessibility, and customComponents registration.

Sister spec to case-location and case-map-overview.
Centralised PDOK integration layer for Procest covering Locatieserver
(suggest/free/lookup/reverse), BAG (nummeraanduiding/verblijfsobject/pand),
Kadaster (perceel), default basemap config, admin settings, shared APCu
cache, rate limiting, and outage degraded-mode handling. Consolidates the
private PdokLocatieserverClient from case-location into a shared service
so case-location, case-map-overview, and map-component all consume the
same integration surface.

Spec only — no code changes.
Add openspec change wms-wfs-layers — config-driven WMS/WFS overlay
extension to map-component. Introduces wmsLayer schema, manifest-driven
admin index page, per-case-type layer subscriptions (caseType.layerIds),
WmsWfsService client (proxy-only), CaseMap layerIds prop, legend
renderer with toggle/opacity, queryable-aware popups, and WFS extent
guard. Spec only — no code changes.

REQ-WMS-1..8 with Given/When/Then scenarios. Strict validation passes.
Manifest-first apply of openspec change `process-step-configuration`.

Schema:
- Bump workflowTemplate version 1.0.0 → 1.1.0 with additive
  documentation of the per-step `config` sub-object
  ({sla, requiredFields, autoActions, escalationRule}).
  No new top-level register schema.

Validator:
- Add pure-function StepConfigValidator (no DI, no I/O) implementing
  the seven rules from design.md. Returns structured {path, code,
  message} errors; never raw exception text.

Editor:
- Extend the existing StepConfigPanel.vue with a collapsible
  "Geavanceerd" panel for sla / requiredFields / escalationRule.
- Pass `read-only` from WorkflowEditor.vue via a new `isPublished`
  computed (template.isDraft === false), per REQ-PSC-7-002.

Deferred (host services not yet on development):
- T03 publish hook, T04/T05 status-transition-engine consumption,
  T06 task creator, T09 bezwaar backfill, T10 spec flip.
Expose the existing workflowTemplate schema with a draft → published →
deprecated lifecycle, version pinning, a consumer contract for
status-transition-engine + role-based-step-routing, and a backfill
repair step — without growing bespoke CRUD.

Schema (manifest-first; pure CRUD goes through the manifest renderer +
OpenRegister auto-routing):
- lib/Settings/procest_register.json — workflowTemplate gains a
  lifecycleStatus enum (draft|published|deprecated), version bumped to
  1.1.0; caseType gains a workflowDefinition uuid reference for the
  pinned active version.
- lib/Service/SettingsService.php — workflow_definition_schema alias
  mirrors workflow_template_schema so consumer specs can resolve the
  schema id without depending on the legacy slug.

Manifest pages (no bespoke Vue components):
- WorkflowDefinitions (index) at /settings/workflow-definitions
- WorkflowDefinitionDetail (detail) with overview / steps /
  transitions / audit-trail sidebar tabs and publish / deprecate /
  clone actions wired to the controller endpoints.
- Settings menu entry "Workflow definitions".

Backend services (legitimate domain logic, not CRUD):
- WorkflowDefinitionService — lifecycle (publish, deprecate,
  cloneDefinition) and read-only consumer contract
  (getActiveDefinitionFor, getDefinition, getDefinitionForCase,
  listVersions). Atomic publish deprecates the previously active
  version and pins caseType.workflowDefinition; deprecate refuses to
  strand open cases; clone increments version. Referential integrity
  check rejects publishing a draft whose transitions reference foreign
  statusType ids. All errors logged + static error strings returned —
  no raw exception leakage.
- WorkflowDefinitionController — five action-only endpoints (publish,
  deprecate, clone, active, for-case). No CRUD methods.

Routes (action endpoints only, /api/workflow-definitions/...):
- POST /{id}/publish, /{id}/deprecate, /{id}/clone
- GET  /active/{caseTypeId}, /for-case/{caseId}

Migration:
- lib/Repair/MigrateWorkflowDefinitions.php — idempotent backfill that
  synthesises one published workflowTemplate per existing caseType from
  its statusType ordering, pins caseType.workflowDefinition, and binds
  open cases to workflowVersion 1. Skips caseTypes that already have a
  workflowDefinition reference or any existing template.
- appinfo/info.xml — repair step registered.

Implements openspec change workflow-definition-model.
Add the parafering-audit-trail change covering:
- paraferingAuditEntry schema (REQ-PAT-1)
- Event-sourced audit writes via ParafeerTransitionEvent (REQ-PAT-2)
- Append-only validator (REQ-PAT-3)
- Reuse of OpenRegister audit-trail-immutable (REQ-PAT-4)
- Content snapshot at transition moment (REQ-PAT-5)
- Archive export endpoint with MDTO 1.0 envelope (REQ-PAT-6)
- Manifest index page for auditor browsing (REQ-PAT-7)
- Retention policy aligned with Archiefwet — 20y decisions / 7y other (REQ-PAT-8)

Sister to parafeerroute-engine, parafering-actions, voorstel-management.
Strict validation passes: 'openspec validate --type change --strict' OK.

No code.
Implements the drag-and-drop visual editor for workflow templates as
defined in openspec/changes/visual-workflow-editor. Frontend-only:
the data layer flows through the existing workflow Pinia store
(createObjectStore on workflowTemplate), so no PHP / backend changes.

What this adds:
- @vue-flow/core, @vue-flow/background, @vue-flow/controls dependencies
- src/components/workflow/VisualWorkflowEditor.vue — main editor
- src/components/workflow/NodePalette.vue — draggable node templates
- src/components/workflow/WorkflowCanvas.vue — vue-flow wrapper
- src/components/workflow/NodeProperties.vue — selected-node panel
- src/components/workflow/EdgeProperties.vue — selected-edge panel
- src/components/workflow/validator.js — live graph validator
  (NO_FINAL_STATUS, UNREACHABLE_FINAL, ORPHAN_NODE, DANGLING_EDGE,
   DUPLICATE_TRANSITION, MISSING_LABEL, CYCLE_NO_EXIT)
- Manifest custom page WorkflowTemplateEditor at
  /settings/workflow-templates/:id/edit (the ONE acceptable custom
  page for this spec)
- Registration of VisualWorkflowEditor in src/customComponents.js
- JSON import/export round-trip through the workflowTemplate schema
- openspec/changes/visual-workflow-editor/builds/build.json artifact
Introduce a reusable role-routing engine (RoleResolverService + StrategyRegistry +
5 built-in strategies) that any workflow step or status transition can use to
compute its assignee set. Migrate ParafeerRouteService.activateStep to consume
the resolver for role-typed actors so parafering inherits delegation + workload
features. Add POST /api/cases/{id}/reroute as an action endpoint for manual
recompute, plus a role-mutation listener that invalidates the APCu cache layer.
Schema: role gains delegate/delegateFrom/delegateUntil; workflowTemplate steps +
transitions document the optional routingRule object (legacy assigneeRole and
allowedRoles are normalised on read).
Manifest-first declarative automatic-action registry: CRUD on action
definitions is fully handled by OpenRegister manifest pages
(/settings/automatic-actions index + detail); the dispatch engine ships
as PHP services in lib/Service/Actions and lib/Service/Transitions.

- Add automaticAction schema (slug, type, tenantId, title, config,
  version, isPublished, active) to procest_register.json; bump
  register version 0.6.0 -> 0.7.0.
- Register automatic_action_schema config key + automaticAction slug
  mapping in SettingsService.
- Add AutomaticActions (type:index) and AutomaticActionDetail
  (type:detail) manifest pages, plus AutomaticActionsMenu under
  settings.
- ActionRegistry: per-tenant, per-request cached slug lookup; rejects
  unpublished, unknown, and cross-tenant references; logs each miss
  via PSR-3 with the requested tenant + owner tenant.
- SideEffectDispatcher: resolves {ref:<slug>} entries via the
  registry and keeps inline action JSON working for backward compat;
  records one audit entry per action (type, ref, ok, error, data)
  on statusRecord.dispatchedActions; never aborts on handler failure.
- Six ActionHandlerInterface implementations: SendEmail,
  CreateDocument, NotifyRole, CallWebhook, MergeTemplate,
  ScheduleReminder. All honour $transitionContext['dryRun'] (compute
  the projected effect, return it in ActionResult.data, do NOT
  mutate live state). All catch \\Throwable, log via LoggerInterface,
  and return ActionResult::failure with a static error code -
  never $e->getMessage() in ActionResult.error.
- Shared HandlesTemplates trait provides deterministic
  {{case.path}} template rendering used by every handler so dry-run
  and live output match exactly.
Apply the status-transition-engine change: introduce the engine that turns
workflowTemplate.transitions[] into deterministic case state changes — the
single write path for case.status across Procest.

Engine core (PHP services, non-CRUD per manifest-first):
- StatusTransitionService: getAvailableTransitions, execute (with guard
  re-evaluation), executeFreeForm (admin-only), replay
- WorkflowTemplateLoader: per-request cached active-template lookup
- GuardRegistry + 4 built-in guards (checklist, requiredField,
  requiredDocument, roleGuard); roleGuard hides silently via details.silent
- ActionHandlerRegistry + SideEffectDispatcher + 6 built-in handlers
  (sendEmail, createTask, createSubCase, webhook, setField, notify);
  failures logged with static error codes, never roll back the status change
- GuardFailedException with failed-guard snapshots
- Registries expose registerEvaluator/registerHandler hooks so downstream
  specs (bezwaar-lifecycle, parafeerroute-engine) can plug in via DI

REST controller (action-style, not CRUD):
- GET  /api/case/{caseId}/available-transitions
- POST /api/case/{caseId}/transition           {transitionId, comment?}
- POST /api/case/{caseId}/transition-freeform  {toStatusId, comment?} (admin)
- GET  /api/case/{caseId}/transition-history
- Static error messages only; never returns $e->getMessage()

Schema + manifest (manifest-first):
- statusRecord v1.1.0: + transitionLabel, fromStatus, evaluatedGuards,
  dispatchedActions, noWorkflowTemplate
- Manifest pages StatusRecords (admin index) + StatusRecordDetail with
  guard/action audit tabs — no bespoke CRUD controller

Legacy ZGW backfill annotation (T13, partial): rulesStatussenCreate gains
a docblock pointer to the engine. Full migration of
ZrcController::handleEindstatusEffect into a SetFieldHandler is deferred to
a follow-up so the live ZGW request lifecycle stays intact.

Out of scope this PR: frontend AvailableTransitionsPanel/TransitionConfirmDialog
(T18-T20) — admin browsing is via the new manifest pages.
Add the shared MapComponent Vue wrapper around CnMapWidget plus the
mapFormatters registry, and register MapComponent in customComponents so
manifest pages MAY mount it by name.

This is the manifest-first slice of openspec/changes/map-component (T01,
T02). Consumer migrations (CaseMapTab T03, CaseMapWidget T04,
PublicCaseView T05) remain owned by case-location, case-map-overview,
and the public-case-page specs respectively — see builds/build.json
deferred list for the trail.
Convert the existing `/map` page from a hand-written `CaseMapView.vue`
custom component to a manifest-driven `type: 'map'` page consuming
`CnMapPage` from @conduction/nextcloud-vue beta.30 (ADR-008).

Changes:
- src/manifest.json: CaseMap page entry switched to `type: 'map'` with
  full config block — register/schema, geometryField, filter ids,
  marker.formatter, clustering (with status-priority order
  blocked > in_progress > open > closed), bboxQuery threshold (5k pins),
  pdok-brt tile layer, sidebar config, and inline emptyState (nl).
- src/services/mapFormatters.js: new — exports `caseMarkerFormatter`,
  `statusColor`, `statusIcon`. Handles Point + Polygon centroid + null
  geometry. Uses NL Design System color tokens (var(--color-status-*),
  no hardcoded hex).
- src/main.js: registers the mapFormatters registry on Vue.prototype
  and on the App props (mirrors customComponents pattern).
- src/customComponents.js: drop the CaseMapView import + registration.
- src/views/CaseMapView.vue: deleted (replaced by CnMapPage).
- builds/build.json: record application; verification tasks deferred
  per strict watchdog (lint + jq only) and 5k-pin perf benchmark
  follows in a separate change.
Add shared PHP services for PDOK Locatieserver and BAG so every consumer
in Procest (case-location, case-map-overview, map-component) talks to a
single ingress instead of carrying its own HTTP client.

- PdokLocatieserverService: suggest, free, lookup, reverse. 24h cache for
  lookup/reverse, 5min for suggest, cache key includes filter (fq) params.
  Outage degradation: 3x 5xx in 60s -> degraded for 5min; suggest returns
  [] so LocationService can fall back to free-text.
- PdokBagService: nummeraanduiding / verblijfsobject / pand. Response
  normalised to a stable Procest-internal shape (snake -> camel, bouwjaar
  int, oppervlakte int m2, gebruiksdoel array).
- Both services route via an OpenConnector source slug when the matching
  pdok_*_source config key is non-empty; otherwise they call PDOK
  directly. OpenConnector remains an optional dependency.
- Extend SettingsService with the 11 pdok_* config keys (endpoint URLs,
  source slugs, cache TTLs, rate ceiling, outage banner nl+en).

Per manifest-first watchdog: no new admin Vue components, no new
controllers, no new manifest pages. Admin config surface is the existing
SettingsService allow-list. T03/T05/T06/T07/T08 are deferred and tracked
in builds/build.json; case-location LocationService refactor will land
in the case-location apply.
Apply the enforcement-lhs change manifest-first:

- Add `lhsMatrix` and `lhsRecommendation` schemas to procest_register.json
  with the 3-D matrix (3 ernst x 4 gedrag x 4 actorType = 48 cells) and
  the per-enforcement recommendation row. Matrix is versioned and
  immutable per REQ-LHS-8; historical recommendations freeze matrixVersion.
- Wire schemas into SettingsService config keys + slug map.
- Register manifest pages LhsMatrices/LhsMatrixDetail (admin-only) and
  LhsRecommendations/LhsRecommendationDetail for audit browsing — no
  bespoke CRUD controller; OpenRegister serves CRUD.
- Implement OCA\Procest\Service\Vth\LhsRecommendationService with
  recommend(ernst, gedrag, actorType, lhsVersion?) and
  override(recommendation, intervention, justification, userRole) —
  override-up is gated to manager role (REQ-LHS-5/6); identity is
  server-derived from IUserSession (REQ-LHS-3).
- LhsController owns the engine actions POST /api/lhs/recommend and
  POST /api/lhs/override.
- SeedLhsMatrix repair step idempotently imports the Landelijke
  Handhavingsstrategie 2024 (48 cells) from
  lib/Settings/seed/lhs-matrix-2024.json.

STRICT watchdog: php -l + jq only; no composer check; no i18n.

Refs openspec/changes/enforcement-lhs/tasks.md T01-T03, T05.
Seed six canonical VTH workflow templates as published workflowTemplate
v1 objects via WorkflowDefinitionService::createDraft() + publish().

- Add lib/Settings/seed/vth-workflow-templates/ catalog with:
  - aanvraag-omgevingsvergunning (Awb 4:13, 8 weken)
  - toezichtbezoek (spawn-link naar handhavingstraject)
  - handhavingstraject (Awb 5:24 redelijke termijn)
  - bezwaar (cross-link naar bezwaar-lifecycle, geen redefinitie)
  - klacht-toezicht (Awb 9:11, 6 weken)
  - spoedig-herstel (Awb 5:31, onverwijld)

- Add lib/Repair/SeedVthWorkflowTemplates.php idempotent repair step;
  registered as post-migration repair step in appinfo/info.xml.
- Repair step resolves caseType slug + statusType names to UUIDs at
  install time, generates deterministic UUID5 ids for steps/transitions,
  and routes every workflowTemplate mutation through the lifecycle
  service to respect the immutability invariant from workflow-
  definition-model. Soft-deps on base-register-seed-data: missing
  caseTypes warn + skip.
- Add WorkflowDefinitionService::createDraft() for seed/import flows.

Manifest-first: no new schemas or manifest pages — templates are seeded
data. UI / controller / Vue store deferred to a follow-up apply
(documented in builds/build.json deferred list).
Apply the inspection-checklists OpenSpec change in manifest-first style:

- Add two OpenRegister schemas in lib/Settings/procest_register.json:
  - inspectionChecklistTemplate (REQ-IC-1): name, caseType, version, status
    (draft|active|retired), sections[] with nested items[], failureAction
    enums, seedKey
  - inspectionChecklistRun (REQ-IC-2): case, inspection, template,
    templateVersion, templateSnapshot, inspector (server-derived),
    responses[], photos[], overallResult, syncState, followUpType
- Wire schema slugs into SettingsService::DEFAULT_SCHEMA_CONFIG_KEYS so the
  manifest renderer can route auto-generated index + detail pages
- Add lib/Service/Inspection/ChecklistService.php — createRun snapshot
  freeze + IUserSession-derived inspector, submitRun with REQ-IC-3
  validation, REQ-IC-6 aggregateResult, REQ-IC-7 follow-up dispatch
  (herinspectie / handhavingstaak via handhavingsactie / documentVerzoek)
  and REQ-IC-8 append-only assertion
- Add lib/Listener/ChecklistRunImmutabilityListener.php — rejects
  ObjectUpdatedEvent on submitted runs with "Checklist run is append-only"
- Register the new listener on ObjectUpdatedEvent in Application.php
- Record the change in builds/build.json
Manifest-first apply of bezwaar-lifecycle:
- bezwaar schema with status enum (10 states) + AWB-grounded fields
- x-openregister-calculations on bezwaar: decisionDeadline (AWB 7:10),
  dwangsom (AWB 4:17) — no BezwaarDeadlineService
- SeedBezwaarWorkflowDefinition repair step publishes the canonical
  AWB workflow definition (12 transitions, requiredField guards on
  awbReference for Niet-ontvankelijk + Ingetrokken)
- BezwaarLifecycleListener observes object events and routes them onto
  the status-transition-engine — no bespoke BezwaarController and no
  BezwaarLifecycleService
- Manifest pages: Bezwaren (index) + BezwaarDetail (Overview/Advice/
  Hearing/Decision/Audit tabs) + Bezwaren menu item
Manifest-first apply of the case-location change. Adds a `location` schema to
the Procest register with a back-reference to `case`, the case detail
"Locaties" sidebar tab (related-index widget filtered by case UUID), and
admin index/detail pages for browsing all case locations. The location
entity CRUD is handled entirely by the OpenRegister auto-form rendered by
the manifest — no bespoke LocationController.

LocationService.php exposes the server-side helpers the manifest cannot:
- validate() — cross-field rules from design.md §Validation Rules
- reverseGeocode() — delegates to PdokLocatieserverService (pdok-integration)
  with graceful null fallback when PDOK is unavailable
- attachToCase() — persists a validated location through the OpenRegister
  object store, scoped to a case
- listForCase() — server-side helper for workflow guards and map clustering

Settings: adds `location_schema` to the app-config allow-list and the
`location` → `location_schema` slug mapping so the schema UUID is resolvable
at runtime.

Spec tasks applied: T01 (schema + config key), T02 (back-reference relation
docs), T04 (LocationService validate/reverseGeocode/attachToCase). Deferred:
T03 (PDOK client supplied by pdok-integration #390), T05 (no bespoke
LocationController — manifest covers CRUD), T06 (manifest-driven tab
replaces Vue-side CaseLocationsTab.vue for MVP), T07/T08 (CSV
import/export — follow-up), V01-V04 (test scope — strict watchdog lint+jq
only).
Manifest-first implementation of the regulator-grade, append-only audit
trail for the parafeerroute lifecycle.

- Schema: add paraferingAuditEntry to procest_register.json (action,
  actor, actorRole, timestamp, reason, contentSnapshot, ipAddress,
  auditEntryHash) + register slug list + new x-pages index page at
  /voorstellen/:voorstelId/audit-trail (sorted by timestamp desc).
- Service: lib/Service/Parafering/AuditTrailService.php — record() builds
  the canonical entry + SHA-256 hash and persists via ObjectService;
  export() returns the MDTO 1.0 envelope with 20yr/7yr retention;
  assertAppendOnly() rejects UPDATE/DELETE with OCSForbiddenException
  "Audit entries are append-only".
- Domain event: lib/Event/ParafeerTransitionEvent.php — emitted by
  ParafeerRouteService (startParafering, completeStep, skipStep,
  addAdhocStep, completed) and ParafeerActieService.recordAction.
- Listener: lib/Listener/ParaferingAuditListener.php subscribes to the
  event and persists one entry per transition; failures are swallowed
  so audit outages never block operational transitions.
- Validator: lib/Validator/ParaferingAuditAppendOnlyValidator.php hooks
  OR pre-save (ObjectCreating/Updating/Deleting) events to enforce
  append-only on the audit schema.
- Endpoint: GET /api/voorstellen/{id}/audit-trail/export (action, not
  CRUD) — RBAC via auditors/secretariaat/beheerders groups; returns
  metadata + entries envelope.
- Registry: SettingsService gets the parafering_audit_entry_schema
  config key and slug→key mapping.

CRUD listing is served by OpenRegister's auto-exposed
/api/objects/<register>/<schema> endpoint via the manifest index page;
no bespoke CRUD controller introduced.
rubenvdlinde and others added 30 commits May 25, 2026 09:58
REQ-001 AI REST surface for classify/extract/ask/summarize/suggestRouting/suggestNext,
REQ-002 Human-in-the-loop action recording and audit-trail listing,
REQ-003 Global + per-feature AI enablement flags,
REQ-004 Dutch PII stripping before model dispatch,
REQ-005 Settings management and AI-model health check.

Ghost change: retrofit-2026-05-24-ai-assistance.

Refs #565.
REQ-001 Implement IMcpToolProvider with stable app id and hardcoded tool catalogue,
REQ-002 listProcesses tool with bounded limit and optional status filter,
REQ-003 getProcessDetails tool returning case + history,
REQ-004 Per-object authorisation (assignee/role/admin) inside invokeTool,
REQ-005 Standard error envelopes and result-cap.

Ghost change: retrofit-2026-05-24-mcp-integration.

Refs #565.
)

Specifies procest's DSO intake adapter as a thin client to openconnector's
full DSO protocol spec. Named *-client to avoid overlapping with
openconnector's spec.

REQ-001 DSO vergunningaanvraag intake creates procest zaak,
REQ-002 DSO-specific case properties stored as side records,
REQ-003 Procedure-type deadline duration lookup.

Ghost change: retrofit-2026-05-24-dso-omgevingsloket-client.

Refs #565.
REQ-001 Aggregate health-check JSON endpoint with status/version/checks shape,
REQ-002 Three sub-checks (database/OpenRegister/filesystem) with severity tiering,
REQ-003 HTTP status reflects aggregate health (200 ok, 503 error/degraded).

Ghost change: retrofit-2026-05-24-ops-observability.

Refs #565.
Describes observed behavior of the backend GIS proxy (GisProxyController +
GisProxyService) as 2 new REQs on the map-component capability spec. Code
already exists — this change retroactively specifies it.

Refs #565
* retrofit: draft zgw-autorisaties-api spec + annotate 16 methods

Reverse-spec the VNG ZGW Autorisaties (AC) API served by AcController.
The coverage scan mislabeled this cluster bezwaar-advisory-committee
("AC" guessed as Adviescommissie); it is actually the Autorisaties
component managing applicaties via OpenRegister ConsumerMapper.

5 observed-behavior REQs: CRUD+mapping (REQ-001), ac-001 clientId
uniqueness (REQ-002), ac-002 heeftAlleAutorisaties consistency
(REQ-003), ac-003 scope-based field requirements (REQ-004), JWT auth +
safe degradation (REQ-005). Notes flag the authorization gap (JWT
authenticated but scopes never checked; UUID-only object resolution;
write-path skips validation) as observed, not fixed.

* retrofit: archive zgw-autorisaties-api change

Merge the spec delta into openspec/specs/zgw-autorisaties-api/spec.md
and move the ghost change to archive/, following the repo retrofit
convention (cf. retrofit-2026-05-24-annotate-procest).

* chore: ignore reverse-spec annotation commit in git blame
…#568)

Drafts 5 numbered REQs describing the procest-side ZGW API surface
(ZRC/ZTC/DRC/BRC/NRC controllers, ZgwService + helpers, ZgwAuthMiddleware,
LoadDefaultZgwMappings repair step) and annotates every file in the cluster
with a per-task @SPEC pointer.

Ghost change: retrofit-2026-05-24-zgw-api-mapping (delta merged into main spec).

Refs #565.
…s) (#569)

Drafts 5 numbered REQs describing the rules-service implementation surface
(ZgwRulesBase, ZgwBusinessRulesService dispatcher, BRC/DRC/ZTC rules services)
invoked by ZGW controllers before write persistence.

Ghost change: retrofit-2026-05-24-zgw-business-rules-compliance.

Refs #565.
Drafts 5 numbered REQs describing the action-handler framework
(interface contract, registry, notification + content + reminder handler families).

Ghost change: retrofit-2026-05-24-automatic-actions (delta merged).

Refs #565.
… / 7 files) (#571)

Drafts 5 numbered REQs (REQ-101..REQ-105) describing the case-sharing,
transfer, email and public-share surface in the case-management capability.

Ghost change: retrofit-2026-05-24-case-management.

Refs #565.
Ports the unique outbound WFS-export capability from PR #483
(feature/462/gis-integration) onto the current development baseline.
That branch had diverged from an unrelated history root and could not
be merged directly without reverting large amounts of merged dev work,
so only the genuinely-unique, self-contained additions are carried over:

- lib/Service/WfsExportService.php — builds a GeoJSON FeatureCollection
  of case locations from OpenRegister (uses SettingsService.getObjectService
  + findObjects, already on dev) plus a WFS GetCapabilities descriptor.
- lib/Controller/WfsExportController.php — GET /api/gis/wfs (GetFeature)
  and /api/gis/wfs/capabilities, NoAdminRequired with an authenticated-
  session guard, typeName/outputFormat/maxFeatures/bbox/status/caseType params.
- appinfo/routes.php — two additive wfsExport routes alongside the existing
  gis_proxy / wms_wfs proxy routes.
- Unit tests for both classes.

This complements dev's existing proxy-based GIS work (GisProxy/WmsWfs);
the WFS export (procest cases AS a layer) was absent on dev.
Add documentationUrl config to the FeaturesRoadmap roadmap page so the
footer nav item resolves, and bump @conduction/nextcloud-vue to beta.101.
Backs the shared @conduction/nextcloud-vue CnSupportDialog auto-mount's
per-user seen-flag. Adds GET/PUT /api/preferences/{key} backed by
Nextcloud IConfig user values (NoAdminRequired + NoCSRFRequired). Keys
are sanitised to a safe charset and namespaced under pref_. When the
caller is unauthenticated the widget falls back to localStorage.
Intercept the not-yet-implemented case-email-integration change so it
consumes the OpenRegister `email` integration leaf (NC Mail; id `email`,
group `comms`, storage `link-table`) for all email display, compose, link
and unlink — rather than building a bespoke mail client.

Re-pointed to the leaf (display + compose + link):
- emailMessage/emailThread schemas, CaseEmailService send/transport,
  EmailComposer.vue, EmailThread.vue, EmailTab.vue, UnlinkedQueue.vue,
  SMTP send config and send/list/link/unlink endpoints — all removed.
  Display/widget via the leaf tab + CnEmailCard on the `case` detail page
  (ADR-019/ADR-024); compose in NC Mail; link via the leaf's
  POST /api/objects/{register}/{schema}/{id}/email.

Kept in-app as leaf extensions (no leaf equivalent):
- emailTemplate schema + EmailTemplateService versioning + Dutch defaults
  (per-zaaktype templating; prefills an NC Mail draft).
- Email -> PDF -> caseDocument archival + retry (Archiefwet/ZGW).

Documented ADR-022 exception:
- InboundEmailJob polls a shared/functional mailbox (owner-less, so the
  link-only leaf cannot ingest it) and auto-links by case-number subject
  regex, recording links through the leaf endpoint. Justified in new
  openspec/architecture/adr-002-shared-mailbox-poller-exception.md.

openspec validate case-email-integration --strict: valid.
…to-leaf

chore(case-email-integration): re-point to email leaf per ADR-022
Add x-openregister-notifications to the case and task schemas so a newly
created case or task assigned to a user raises an in-app notification to
that assignee via the OpenRegister notification engine.
…te-16 to zero (#611)

Bulk-inserts additive /** @SPEC openspec/... */ comment lines above every
in-scope backend and frontend method flagged by the spec-coverage gate
(csc.py --mode report). 1561 annotations across 310 files; uncovered_count
0 -> 0. Targets map each method to its file's existing class-level @SPEC
reference where present, otherwise to the matching openspec change (tasks.md)
or archived spec (spec.md) by feature area. Comment-only, no code logic
changed (diff is purely additive: 1561 insertions, 0 deletions).
…hares/analytics/forms

Five SPECS-ONLY migration changes moving procest from in-app NC features to
OpenRegister integration leaves per hydra ADR-022:

- migrate-maps-to-maps-leaf: Leaflet/WMS/WFS map UI -> maps leaf (geo data kept)
- migrate-appointments-to-calendar-leaf: LocalBackend -> calendar leaf;
  Qmatic/JCC flagged as ADR-022 exception (external municipal systems)
- migrate-public-share-to-shares-leaf: PublicShareController token sharing -> shares leaf
- migrate-sla-dashboard-to-analytics-leaf: doorlooptijd charts -> analytics leaf
  (SLA compliance calc stays in-app as case-domain logic)
- migrate-inspection-forms-to-forms-leaf: checklist/advice forms -> forms leaf,
  inspection photos -> photos leaf (immutability + photo-gate rules stay)

CMMN/ZGW engine + parafering e-signature stay in-app (documented exceptions).
All five pass 'openspec validate --strict'. No code applied.
chore(openspec): ADR-022 leaf-migration specs (maps/calendar/shares/analytics/forms)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant