feat(case): copy a case, start a sub-process, plan a follow-up - #1935
Merged
Conversation
added 10 commits
September 8, 2026 14:23
CaseCopyService carries the type, requester, confidentiality, priority, intake channel and properties, relates the copy back to its source and links the source documents by reference when asked. The number, deadline, result, status history, decisions and publications are never copied, and the status is left to the case type prefill so there is one write path for it.
CaseActionsController carries copy, startable-flows, plan and planned. Every method is NoAdminRequired and asks CaseAccessGuard before the service: mutation access for the two writes, read access for the two reads. CaseFlowActions holds the flow half and resolves every OpenRegister class by name, because dossiq declares no app dependency on it. A flow is a row in oc_openregister_flows, not a register object, so startableFlows holds plain uuids rather than a $ref: a $ref addresses a schema in a register and there is no flow schema to name.
The entry is open-modal onto CaseCopyDialog, not the handler type the design named: a handler action resolves against effectiveManifest.actions, a JSON map that cannot hold a function, and dossiq declares no such key, so the entry would warn to the console and do nothing. The id is copy-case because copy is a reserved id CnActionsBar drops. Not CnCopyDialog either: 2.41.0 offers three naming patterns over a fixed name and carries no slots, so there is nowhere to put Include documents and no way to type a title that is not one of the three.
caseType gains startableFlows and the case gains hasStartableFlows, a materialised boolean over it, because the Start entry must be hidden on a type that lists none and an actions local visibleWhen can only see the case record. Both schema versions are bumped: OpenRegister fast-skips a schema whose version did not move, so the properties would be stored in this file and absent from every install. The list is plain strings and not a $ref: a flow is a row in oc_openregister_flows, not an object in a register.
The dialog lists what the case type allows and posts the chosen flow to
OpenRegisters own run endpoint with the case as subject {uuid, register,
schema}, the three keys FlowRunRow reads, so the run lands in the Flow
runs widget beside it and dossiq stores no copy of a run.
The entry is hidden by the cases materialised hasStartableFlows. The
dialog still says so when the list comes back empty, because that gate is
a save-time value and a case type edited since the last case save has not
been recomputed yet.
The plan writes one scheduled flow: a schedule trigger pinned to the date with an explicit runAs, feeding dossiqs createSubCase node. It is written disabled, published, then enabled, because a run is refused unless a published sound version exists. The engine has no one-shot trigger and five cron fields cannot say once, so PlannedFollowUpSweepJob switches a planned flow off after it has fired. Left alone, a follow-up planned for October 2026 would also open a case in October 2027 with nothing reporting it.
A planned follow-up is a scheduled flow and not a case, so the related widget cannot list it: it reads related objects. CasePlannedWidget wraps the librarys own CnRelatedObjectsWidget and hands it the planned rows as an extraSections group, so the built-in related content is untouched. The widget declares its own registry TYPE rather than type custom: a custom widget resolves through the pages widget-<id> slot, which CnDetailPage renders per layout grid item only, and a tab child has no grid item. CnTabsWidget resolves a tab child by type and renders nothing, silently, when no key answers.
Copy with and without documents, the Start list following the case type, Start hidden on a type that allows none, and a planned follow-up on the Related cases tab. Two scenarios are re-annotated @e2e exclude rather than written as tests that cannot fail. A reader cannot copy: Playwright signs in as admin and cannot take a lesser role, and PHPUnit asserts the guard runs before the service. A run appears on the case: running a flow needs an adopted, published and enabled flow, which a fresh install deliberately has not got, so arranging one would make it a test of the adoption path.
PlannedFollowUpDocument is split out of CaseFlowActions: the flow a planned follow-up is written as, and read back from, is pure, which drops the class complexity under the phpmd threshold and makes the two rules that matter testable without an instance. PHPStan caught a real one. CARRIED and NEVER_COPIED are provably disjoint, so the runtime strip between them was dead code. The ban is now a rule about the constants, asserted by the unit test both against the written payload and as a disjointness check. The date is parsed without DateTimeImmutable::createFromFormat, whose static call is a phpmd finding, and the shape check it would do is done in full anyway.
rubenvdlinde
requested review from
Rem-Dam,
SudoThijn,
WilcoLouwerse,
bbrands02,
remko48 and
rjzondervan
as code owners
September 8, 2026 13:09
The only conflict is the tail of the l10n catalogues: development added keys where this branch did, in both en.json and nl.json. Resolved by taking developments file and re-adding this branchs 25 keys onto it, then regenerating en.js and nl.js with the build script rather than by hand.
Contributor
Quality Report — ConductionNL/dossiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| check-vue3-compile | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 106/106 | |||
| npm | ✅ | ✅ 541/541 | |||
| 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-08 13:19 UTC
Download the full PDF report from the workflow artifacts.
Clean. It brings 2ed1e85, which declares @uses on the four test classes whose eight risky results this PR inherited: they name StatusTypeLookup and TransitionSpecReader, both from developments own checklist-per-status work and neither touched here. The failure arrived because a pull_request check tests the branch merged into the base, not the branch.
Contributor
Quality Report — ConductionNL/dossiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| check-vue3-compile | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 106/106 | |||
| npm | ✅ | ✅ 541/541 | |||
| 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-08 13:27 UTC
Download the full PDF report from the workflow artifacts.
… a regex CodeQL flagged the key being escaped into a pattern as incomplete sanitisation, correctly: escaping a callers string into a regex is a shape that is wrong more often than it is right, and it bought nothing here. The entry is a fixed two-line shape in a file this test reads whole, so slicing forward from the key answers the same question with no pattern at all, and it slices to the closing brace so a neighbouring entrys kind can never answer for this one. Two negative cases are added beside it: the helper decides seven assertions, so it is asserted to be capable of saying no.
Contributor
Quality Report — ConductionNL/dossiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| check-vue3-compile | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 106/106 | |||
| npm | ✅ | ✅ 541/541 | |||
| 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-08 13:35 UTC
Download the full PDF report from the workflow artifacts.
Three conflicts, all "both sides added an entry beside each other", all resolved by keeping both: an icon import, three registry dialogs, and a materialised boolean on the case. The versions are the load-bearing part. Development moved caseType to 1.5.0 and case to 1.18.0 for its own statusHiddenInLists, so this branch goes to 1.6.0 and 1.19.0: OpenRegister fast-skips a schema whose version did not move, and a merge that kept 1.18.0 would have shipped hasStartableFlows into a file nothing re-reads. A sibling vitest pinned the case version to exactly 1.18.0, which fails on the next change that legitimately bumps the same schema. It now asserts at least 1.18.0, which is the rule it meant.
Two conflicts. The registry import is both sides adding one, kept both. The manifest is development adding subtitleField, breadcrumbs and two notes ABOVE the page note this branch extended: their keys are kept and their _note dropped, because this branchs _note is theirs verbatim plus a paragraph on the three new actions. Verified: every sentence of theirs is present in the merged note.
One conflict, the CaseDetail page note: development replaced the audit history sentence with a longer one about the version-history tab moving to the timeline, in the same string this branch appended a paragraph to. Resolved as the union, developments note plus this branchs paragraph on the three new actions, so neither side loses a sentence.
Contributor
Quality Report — ConductionNL/dossiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| check-vue3-compile | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 106/106 | |||
| npm | ✅ | ✅ 541/541 | |||
| 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-08 14:22 UTC
Download the full PDF report from the workflow artifacts.
This was referenced Sep 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the merged OpenSpec change
case-type-authoring-extras(rows A10, A11, A29, A30, A31 of the round 2 placement). One noun: the case type's blueprint — what a status looks like, where a type gets its blueprint from, how types are grouped, what personal data it processes, and how it leaves and enters the system.Scenario to e2e map
Every
@e2e-tagged scenario in the five spec deltas, intests/e2e/case-type-authoring-extras.spec.ts(13 tests):a coloured status draws its board column in that coloura hidden status keeps its cases off the list, and Closed brings them backthe case page draws the current status in its status's coloura child that declares nothing shows its parent's four statuses, marked Inheriteda child's own deadline beats its parent'sa parent that descends from the type is refused, and the message names the cyclepicking a folder narrows the Case types index to that categoryan attribute saved without a case type is listed on every type, marked Sharedthe personal data block reads back the categories and the basisExport starts a download whose name carries the type's identifierDuplicate lands you on the copy, with the same statusesa draft with findings lists them and stays a drafta valid draft is published with its change note, and the version says soTwo scenarios stay
@e2e excludeexactly as the deltas wrote them: Import needs an OS file dialog (covered by the controller and service tests), and a verwerkingsactiviteit code cannot be refused until OpenRegister exposes the register as a referenceable schema (task 4.3, still blocked).Where the design and the code disagree, and why
Five decisions in
design.mdcould not be implemented as written. Each is recorded intasks.mdbeside its task.run-actionis not a header-action type. The manifest schema enumerates handler, open-modal, open-page, navigate, object-op, export, open-form, refresh, api-call, agent and toggle, andCnActionButtonsresolves exactly those. All four actions as designed would have been refused bycheck:manifest, and had they passed they would have rendered four buttons that dispatch nothing. Export is anapi-callwithdownload: true; the other three are dialogs, each for a reason a declarative action cannot meet — Import takes a file, Publish must show its findings before asking for a change note, Duplicate must land on the copy.folderSidebar.source: "facet"does not exist. CnIndexPage resolves register, field, custom and files, and an unknown source falls through tocustom— whose folder list is the absentfoldersarray, so the pane renders empty and says nothing. It issource: "field"overcategory.$ref.status.hiddenInListsis dropped by OpenRegister, which is a default filter that silently lists everything. The case now carriesstatusHiddenInLists, a materialisedx-openregister-calculationsentry over@ref.statusType.hiddenInLists— the same shapeisFinalStatusalready used.object-listcolumn. An object-list fetches OpenRegister itself, so the only question it can ask iscaseType = @objectId: a child that inherits its lifecycle would render an empty table.GET /api/case-types/{id}/blueprintmerges the chain server-side;case-type-blueprintrenders it, and stays out of any tab strip because atype: "custom"widget named as a tab child resolves by registry type and renders an empty panel without logging.validate.CaseDefinitionController::validate()validates an uploaded package, andZgwZtcRulesService::validatePublish()reads the type's own statuses — so a child would be refused with "give it a status" while its page showed four.CaseTypePublishServicevalidates through the resolver.What the checks found
Five defects that no other check saw, all fixed on the branch:
appinfo/routes.phpstill carriedcaseDefinition#blueprintandcaseDefinition#publishafter the controller they pointed at was reverted. A route with no target method is a 500 at dispatch.#[NoAdminRequired]over an admin-only body, which is what the change's own task text asked for. Both methods now carry#[AuthorizedAdminSetting(AdminSettings::class)]and the body guard is gone.assertNoCycle()was defined and never called. Wired intoCaseTypePublishService::validate(), which is the only write dossiq owns — a case type is saved straight to OpenRegister's object API by the page, so "refused on save" can be met nowhere else. Recorded as a limitation.@spec.ManifestColumnBindingTest: the Versions list bound its Updated column toupdated, whichworkflowTemplatedoes not declare — a column that renders a dash in every row. It is@self.updated.Known limitations, recorded in tasks.md
case.statuscarries anx-relation-filterandcase.caseTypeanx-openregister-prefill; both are OpenRegister's, both read the child's own rows, and neither can express a chain. So the status picker on a new case of a child type offers only the child's own statuses, and the case form does not pick up a shared attribute.effectiveCaseType()inheritsinitialStatusso every PHP reader is right; the picker is filed as an OpenRegister request.Four findings from a sibling change, checked against this one
run-actionis not a header-action type. Not used here: all three entries areopen-modal, andnpm run check:manifestpasses.open-modalaction forwards props verbatim. Handled: none of the three passes props at all, and each dialog reads the case from the route.sourceoffacetdoes not exist. No sidebar source is touched here.$ref. No filter is added here, and the one cross-object read this change needs is exactly a materialised calculation:case.hasStartableFlowsover@ref.caseType.startableFlows.Checks, by exit code
Run locally on this branch, AFTER merging
developmentin (four l10n files conflicted, both sides having appended entries at the tail of the translations object; resolved as the union and the.jscatalogues regenerated). E2E was NOT run: it runs on the development push, and apull_requestcheck cannot prove an E2E fix.npm run lintnpx vitest run(72 files, 820 tests)npm run check:manifestnode tests/l10n/check-l10n.jsnode scripts/check-schema-l10n.jsnpm run check:l10n-jsnpm run formatcomposer lintcomposer phpcslib/directorycomposer psalmcomposer phpstan./vendor/bin/phpunit(3262 tests)HYDRA_GATE_BASE_REF=origin/development run-hydra-gates.shcomposer check:strictwas run as its separate legs: as one command it exceeds a 300s budget. phpmd was swept per directory because printing nothing is its OOM signature, so a whole-lib/pass that says nothing proves nothing.🤖 Generated with Claude Code