Skip to content

fix(approval-routes): advance stages through OR's patch path, and feed the type picker from the registry - #1104

Merged
rubenvdlinde merged 2 commits into
developmentfrom
fix/route-advance-and-type-picker
Sep 2, 2026
Merged

fix(approval-routes): advance stages through OR's patch path, and feed the type picker from the registry#1104
rubenvdlinde merged 2 commits into
developmentfrom
fix/route-advance-and-type-picker

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Fixes two defects from the live rig proof (DC#1101 seam, DC#1099 registry).

Defect: partial stage saves 400, routes can never advance (HIGH)

ApprovalRouteService::completeAndAdvance and its siblings saved PARTIAL DecisionStage payloads (status/outcome/decidedAt) with a uuid. OpenRegister validates a uuid-bearing save as a FULL REPLACE, so every stage transition 400d on the required sequence, stageType, decisionMakerType and label it omitted — no approval route raised over the dossiq seam could advance or conclude, and POST /api/approval-routes/actions returned 400 (reproduced twice on the rig).

Fix chosen: OR's sanctioned patch path, not hand-rolled read-modify-write. The vendored contract (conduction/hydra-gates v1.11.0) and OR development both carry patchObject(), the RFC 7386-shaped merge counterpart to the replacing save. RegisterObjectStore::patch() wraps it; every stage transition now goes through it, so schema validation, the audit trail and event dispatch still apply and an explicit null still clears outcome/decidedAt on a rewind.

Sweep (one fix is not the class). The same class was found and fixed at:

  • ApprovalRouteService: 5 sites (complete, advance, terminal return, rewind reset x2)
  • ApprovalStageTaskProjector: 2 sites (taskUuid linkage set/clear)
  • AgendaService: 3 sites — advanceBobPhase, processHamerstukken and reorderItems all 400d the same way (agenda-item requires title, itemType, orderNumber)

Left deliberately on the replacing save: the declared-state upserts in GovernanceBodyCommandService (upsert, upsertMembership) and ApprovalRouteCommandService::holdRoute — their payloads carry every required property and the seam's documented semantics are "the producer states it", where a merge would keep stale fields a producer meant to clear. ApiController::write documents its PUT semantics on the public surface and is likewise intentional.

Tests that can now fail. The route-engine fakes MERGED uuid-bearing saves — the exact behaviour live OR does not have — so the suite was green while every advance 400d in production. The fakes in ApprovalRouteServiceTest, ParaferingRouteRuntimeTest and ApprovalRouteCommandServiceTest now REPLACE and validate required properties per schema exactly like live OR (and merge only through patchObject). Negative control: the old code fails 9 tests against the corrected fakes with the rig's own "required properties … are missing" refusal; the fixed code passes all of them, pinning route advance and conclusion end-to-end.

Defect: type picker ignores the registry (MEDIUM)

CnDecisionsTab and CnDecisionsWidget hardcoded 5 decision types in the create-proposal picker; the DecisionTypeRegistry (13 seeded, admin-extensible) was consulted only for validation, so an admin-added type never appeared.

  • New GET /api/v1/decision-types (DecisionTypesController) serves the registry's vocabulary — the pickers render inside foreign apps' pages, where decidiq initial state never loads, so an endpoint is the only channel that reaches them. Same guarded pattern as the integration hub: #[NoAdminRequired] + session check, 401 for anonymous callers.
  • Both pickers build their schema from a shared proposalFormSchema() in decisionLink.js; an unreachable registry degrades to the shipped 13-type seed instead of blocking creation.
  • The 13 shipped types carry translated labels (enumLabels) — 9 new catalogue keys translated into every locale the app ships, Dutch and English included; an admin-added type renders as its own slug.
  • Coverage: vitest pins that an admin-added type appears in the picker schema and that the fallback engages; a PHPUnit controller test pins the endpoint's answer and the 401.

Verification

  • composer lint / phpcs / phpmd / psalm / phpstan: green (run individually)
  • PHPUnit: 1356 tests, 0 failures; vitest: 388 tests, 0 failures
  • test:l10n, check:l10n-js, check:schema-l10n (ratchet lowered by 1), check:manifest, eslint (0 errors, warning count unchanged from development)
  • hydra gates v1.11.0 --scope-to-diff vs origin/development: 56/56 applicable gates pass

…d the type picker from the registry

Two defects from the live rig proof.

A partial DecisionStage payload saved with a uuid is a FULL REPLACE to
OpenRegister, which validates it whole and 400s on the required
sequence, stageType, decisionMakerType and label it omits — so no
approval route raised over the dossiq seam could ever advance or
conclude. Every stage transition now goes through
RegisterObjectStore::patch(), a thin wrapper over OR's sanctioned
patchObject() merge path. The sweep found the same class in
AgendaService (advanceBobPhase, processHamerstukken, reorderItems all
400d the same way) and in ApprovalStageTaskProjector's taskUuid
linkage; all are on the patch path now. The route-engine test fakes
MERGED uuid-bearing saves, which live OR does not, so the suite was
green while every advance 400d — they now replace and validate
required properties exactly like live OR, and fail 9 tests against the
old code.

The create-proposal pickers hardcoded five decision types, so a type
an administrator added to the decision_types registry validated fine
at the write path and never appeared in any picker. A new
GET /api/v1/decision-types endpoint serves the registry's vocabulary,
both pickers build their schema from it through a shared
proposalFormSchema(), and the thirteen shipped types carry translated
labels in every locale. An unreachable registry degrades to the
shipped seed instead of blocking creation.
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/decidiq @ 4effc0e

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-nav-ceiling
test-l10n
format
check-l10n-js
check-schema-l10n
composer ✅ 104/104
npm ✅ 537/537
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright ⏭️ deferred — runs on the promotion into beta/main, not on a pull request into development
Hydra gates

Quality workflow — 2026-09-02 18:41 UTC

Download the full PDF report from the workflow artifacts.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/decidiq @ 38ec835

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-nav-ceiling
test-l10n
format
check-l10n-js
check-schema-l10n
composer ✅ 104/104
npm ✅ 537/537
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright ⏭️ deferred — runs on the promotion into beta/main, not on a pull request into development
Hydra gates

Quality workflow — 2026-09-02 19:14 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde
rubenvdlinde merged commit 2458edc into development Sep 2, 2026
49 checks passed
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