Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
007cd85
chore(sync): carry main back into beta
github-actions[bot] Aug 30, 2026
b1b0150
Merge pull request #1517 from ConductionNL/sync/main-to-beta-20260830…
rubenvdlinde Aug 30, 2026
0ac8777
chore(sync): carry beta back into development
github-actions[bot] Aug 30, 2026
dea4408
Merge pull request #1518 from ConductionNL/sync/beta-to-development-2…
rubenvdlinde Aug 30, 2026
4cdbb5a
chore(dependabot): hold back vue-router 5 as well (#1534)
rubenvdlinde Aug 30, 2026
87f29a1
docs(dependabot): correct the vue-router holdback to what was measure…
rubenvdlinde Aug 30, 2026
915ea93
fix(lhs): read the stored recommendation before deciding who may over…
rubenvdlinde Aug 31, 2026
1bffcae
fix(deps): resolve the peer chain so npm ci works (#1540)
rubenvdlinde Aug 31, 2026
4e3d1fc
feat(bezwaar): hold advisory committees in the decision app, and run …
rubenvdlinde Aug 31, 2026
8c42e2d
feat(workflow): project workflow definitions onto OpenRegister flows …
rubenvdlinde Aug 31, 2026
e4ab7c1
chore(deps): take @conduction/nextcloud-vue 2.25.1 for the KPI card (…
rubenvdlinde Aug 31, 2026
c09f556
build(deps-dev): bump stylelint-config-recommended-vue (#1525)
dependabot[bot] Aug 31, 2026
4e1afd9
build(deps-dev): bump postcss-html from 1.8.1 to 2.0.0 (#1524)
dependabot[bot] Aug 31, 2026
809240d
build(deps): bump marked from 12.0.2 to 18.0.11 (#1521)
dependabot[bot] Aug 31, 2026
f82fdf3
feat(parafering): hold parafeerroutes in the decision app, and refuse…
rubenvdlinde Aug 31, 2026
963d4fe
test(e2e): drive the four surfaces this round of work actually change…
rubenvdlinde Aug 31, 2026
d1e09d7
fix(deps): put the stylelint packages on one generation (#1550)
rubenvdlinde Aug 31, 2026
c0c6607
build(deps-dev): bump commander from 12.1.0 to 15.0.0 (#1526)
dependabot[bot] Aug 31, 2026
e8e1eee
fix(e2e): stop the settings-menu test depending on the session locale…
rubenvdlinde Aug 31, 2026
cb853c7
fix(e2e): the seed must project the workflow flows, not hope for them…
rubenvdlinde Aug 31, 2026
a218355
fix(e2e): project the workflow definitions onto flows in the CI seed …
rubenvdlinde Aug 31, 2026
895d3c2
refactor(dmn): consume OpenRegister's shared decision-table evaluator…
rubenvdlinde Aug 31, 2026
d56fc44
chore(release): 0.3.8-unstable.20260831052324 (#1563)
github-actions[bot] Aug 31, 2026
f5ed03a
chore(deps): refresh the shared Conduction locks (#1562)
rubenvdlinde Aug 31, 2026
03033f7
fix(dmn): carry rule priority, and pass the uuid as the uuid to delet…
rubenvdlinde Aug 31, 2026
9a82880
chore(deps): take @conduction/nextcloud-vue 2.27.0 (#1567)
rubenvdlinde Aug 31, 2026
33feff7
Merge pull request #1529 from ConductionNL/development
rubenvdlinde Aug 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ updates:
ignore:
- dependency-name: "typescript"
update-types: ["version-update:semver-major"]
# vue-router 5 peers `vite: ^7.3.0 || ^8.0.0` and expects a Vite
# toolchain. Measured, not assumed, and the result is SPLIT: integriq and
# zaakafhandelapp fail their build on `Can't resolve 'vue-router'`, while
# openregister and learniq build clean on 5.3.0. Held because a bump that
# breaks some apps and not others cannot be merged unattended, and the
# difference is not yet understood. Adopting it fleet-wide is a
# Vite migration, not a bump. versioniq is already on Vite and is the
# natural pilot.
- dependency-name: "vue-router"
update-types: ["version-update:semver-major"]
- dependency-name: "webpack-cli"
update-types: ["version-update:semver-major"]
- dependency-name: "@babel/core"
Expand Down
26 changes: 25 additions & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Vrij en open source onder de EUPL-1.2-licentie.

**Ondersteuning:** Voor ondersteuning, neem contact op via support@conduction.nl. Voor een Service Level Agreement (SLA), neem contact op via sales@conduction.nl.
]]></description>
<version>0.3.6-unstable.20260830174315</version>
<version>0.3.8-unstable.20260831052324</version>
<licence>EUPL-1.2</licence>
<author mail="info@conduction.nl" homepage="https://www.conduction.nl/">Conduction</author>
<namespace>Dossiq</namespace>
Expand Down Expand Up @@ -259,6 +259,29 @@ Vrij en open source onder de EUPL-1.2-licentie.
<step>OCA\Dossiq\Repair\VthSeedDataRepairStep</step>
<step>OCA\Dossiq\Repair\CaseFlowSeedDataRepairStep</step>
<step>OCA\Dossiq\Repair\SeedDeadlineMonitoringData</step>
<!--
Holds each local parafeerroute as an ApprovalRoute in the
decision app and notes the id back on the local row. Idempotent
twice over: it skips a route that already carries an id, and the
seam it dispatches to resolves on (sourceApp, externalReference)
before writing, so even a lost note costs one redundant dispatch
rather than a duplicate route. Skips cleanly when the decision
app is not installed — it is an optional runtime dependency and
an install without it keeps working on its local routes.
-->
<step>OCA\Dossiq\Repair\MigrateParafeerroutesToDecidiq</step>
<!--
Raises each local bezwaaradviescommissie as a GovernanceBody in
the decision app and notes the id back on the local row.
Idempotent twice over: it skips a committee that already carries
an id, and the seam it dispatches to resolves on
(sourceApp, externalReference) before writing, so even a lost
note costs one redundant dispatch rather than a duplicate body.
Skips cleanly when the decision app is not installed — it is an
optional runtime dependency and an install without it keeps
working on its local committees.
-->
<step>OCA\Dossiq\Repair\MigrateCommitteesToDecidiq</step>
<!--
Must run AFTER the register sync that adds the English columns, so it
can tell the rename case from the back-fill case. Matches BOTH the
Expand Down Expand Up @@ -421,6 +444,7 @@ Vrij en open source onder de EUPL-1.2-licentie.
<commands>
<command>OCA\Dossiq\Command\BackfillLegalHoldsCommand</command>
<command>OCA\Dossiq\Command\MigrateAutomaticActionsToFlowsCommand</command>
<command>OCA\Dossiq\Command\MigrateWorkflowDefinitionsToFlowsCommand</command>
<command>OCA\Dossiq\Command\MigrateTenantsCommand</command>
<command>OCA\Dossiq\Command\SeedBezwaarBeroepCommand</command>
</commands>
Expand Down
157 changes: 157 additions & 0 deletions lib/Command/MigrateWorkflowDefinitionsToFlowsCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
<?php

/**
* Dossiq dossiq:actions:migrate-to-flows command.
*
* Projects stored `automaticAction` objects onto OpenRegister flows, so the
* configuration becomes executable. It is a command rather than a repair step
* because `FlowService` refuses to create a flow without a signed-in owner and
* an active organisation, and an upgrade runs as nobody — see
* {@see \OCA\Dossiq\Service\Workflow\WorkflowTemplateFlowMigrator}.
*
* @category Command
* @package OCA\Dossiq\Command
*
* @author Conduction Development Team <info@conduction.nl>
* @copyright 2026 Conduction B.V.
* @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* SPDX-License-Identifier: EUPL-1.2
* SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>
*
* @version GIT: <git-id>
*
* @link https://conduction.nl
*
* @spec openspec/changes/workflow-definitions-to-flow/specs/workflow-definitions-to-flow/spec.md
*/

declare(strict_types=1);

namespace OCA\Dossiq\Command;

use OCA\Dossiq\Service\Workflow\WorkflowTemplateFlowMigrator;
use OCP\IUserManager;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

/**
* Project Dossiq workflow definitions onto OpenRegister flows.
*
* @spec openspec/changes/workflow-definitions-to-flow/specs/workflow-definitions-to-flow/spec.md
*/
class MigrateWorkflowDefinitionsToFlowsCommand extends Command {
/**
* Wire the command against the migrator.
*
* @param WorkflowTemplateFlowMigrator $migrator The migrator.
* @param IUserManager $userManager Resolves the acting user.
*/
public function __construct(
private readonly WorkflowTemplateFlowMigrator $migrator,
private readonly IUserManager $userManager,
) {
parent::__construct();
}//end __construct()

/**
* Define the command name, description and options.
*
* `--user` is REQUIRED and has no default. The created flows inherit that
* user's identity and organisation permanently, so guessing an owner would
* hand every migrated flow to whoever the guess landed on.
*
* @return void
*
* @spec openspec/changes/workflow-definitions-to-flow/specs/workflow-definitions-to-flow/spec.md
*/
protected function configure(): void {
$this->setName(name: 'dossiq:workflows:migrate-to-flows')
->setDescription(
'Project Dossiq workflow definitions onto OpenRegister flows (idempotent). '
. 'Per ADR-065 OpenRegister is the only home for a flow engine. The flows arrive '
. 'DISABLED: the definitions still drive cases, and enabling a projection without '
. 'retiring its definition would move every case twice. Use --dry-run first.'
)
->addOption(
name: 'user',
mode: InputOption::VALUE_REQUIRED,
description: 'UID the created flows belong to; also supplies the active organisation.'
)
->addOption(
name: 'dry-run',
mode: InputOption::VALUE_NONE,
description: 'Report what would be created or updated, and write nothing.'
);
}//end configure()

/**
* Run the migration and report per-action outcomes.
*
* @param InputInterface $input Console input.
* @param OutputInterface $output Console output.
*
* @return int Symfony command exit code.
*
* @spec openspec/changes/workflow-definitions-to-flow/specs/workflow-definitions-to-flow/spec.md
*/
protected function execute(InputInterface $input, OutputInterface $output): int {
$uid = (string)($input->getOption('user') ?? '');
if ($uid === '') {
$output->writeln('<error>--user is required: a flow needs an owner and an organisation.</error>');
return Command::INVALID;
}

$user = $this->userManager->get($uid);
if ($user === null) {
$output->writeln('<error>No such user: ' . $uid . '</error>');
return Command::INVALID;
}

$dryRun = (bool)$input->getOption('dry-run');
$summary = $this->migrator->migrate(user: $user, dryRun: $dryRun);

return $this->report(summary: $summary, dryRun: $dryRun, output: $output);
}//end execute()

/**
* Print the summary, and decide the exit code from it.
*
* A failed row exits non-zero. Reporting success over a partial migration is
* how a caller ends up believing data moved that did not.
*
* @param array<string, mixed> $summary The migrator's summary.
* @param bool $dryRun Whether this was a dry run.
* @param OutputInterface $output Console output.
*
* @return int Symfony command exit code.
*/
private function report(array $summary, bool $dryRun, OutputInterface $output): int {
if (isset($summary['note']) === true && $summary['note'] !== '') {
$output->writeln('<comment>' . (string)$summary['note'] . '</comment>');
return Command::SUCCESS;
}

$prefix = 'dossiq:workflows:migrate-to-flows';
if ($dryRun === true) {
$prefix .= ' (dry run — nothing was written)';
}

$output->writeln('<info>' . $prefix . '</info>');
foreach (['total', 'created', 'updated', 'skipped', 'failed'] as $key) {
$output->writeln(' ' . str_pad($key, 8) . ' = ' . (string)$summary[$key]);
}

foreach ($summary['rows'] as $row) {
$output->writeln(' [' . $row['outcome'] . '] ' . $row['marker'] . ' — ' . $row['detail']);
}

if ($summary['failed'] > 0) {
return Command::FAILURE;
}

return Command::SUCCESS;
}//end report()
}//end class
8 changes: 4 additions & 4 deletions lib/Controller/DecisionTableController.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
namespace OCA\Dossiq\Controller;

use OCA\Dossiq\AppInfo\Application;
use OCA\Dossiq\Service\Dmn\DecisionEngine;
use OCA\Dossiq\Service\Dmn\DecisionEvaluationException;
use OCA\OpenRegister\Service\Dmn\DecisionTableEvaluator;
use OCA\OpenRegister\Service\Dmn\DecisionEvaluationException;
use OCA\Dossiq\Service\Dmn\DecisionTableService;
use OCA\Dossiq\Settings\AdminSettings;
use OCP\AppFramework\Controller;
Expand Down Expand Up @@ -71,14 +71,14 @@ class DecisionTableController extends Controller {
*
* @param IRequest $request The request object.
* @param DecisionTableService $tableService The decision-table storage service.
* @param DecisionEngine $engine The pure evaluation engine.
* @param DecisionTableEvaluator $engine The pure evaluation engine.
* @param IUserSession $userSession The user session.
* @param IGroupManager $groupManager The group manager.
*/
public function __construct(
IRequest $request,
private DecisionTableService $tableService,
private DecisionEngine $engine,
private DecisionTableEvaluator $engine,
private IUserSession $userSession,
private IGroupManager $groupManager,
) {
Expand Down
75 changes: 63 additions & 12 deletions lib/Controller/LhsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
* manifest renderer — this controller only owns the engine actions:
* - POST /api/lhs/recommend (matrix lookup + persistence)
* - POST /api/lhs/recommendations/{id}/override (apply inspector override)
* Body: recommendationId (or a `recommendation` object, from which only the
* id is read), intervention, justification, and optionally userRole.
*
* @category Controller
* @package OCA\Dossiq\Controller
Expand Down Expand Up @@ -161,6 +163,61 @@ public function recommend(): JSONResponse {
return new JSONResponse($recommendation);
}//end recommend()

/**
* The id of the recommendation an override applies to.
*
* The ID, not the row. The service reads the recommendation back from the
* store, because the escalation guard compares against what the MATRIX
* recommended — a value a request must not be able to supply.
*
* A whole `recommendation` object is still accepted for callers that have
* not been updated, but ONLY its id is read from it; everything else in it
* is ignored.
*
* @return string The id, or an empty string when none was usable.
*
* @spec openspec/specs/enforcement-lhs/spec.md
*/
private function resolveRecommendationId(): string {
$recommendationId = (string)$this->request->getParam('recommendationId', '');
if ($recommendationId !== '') {
return $recommendationId;
}

$recommendation = $this->request->getParam('recommendation');
if (is_array($recommendation) === false) {
return '';
}

return (string)($recommendation['id'] ?? ($recommendation['@self']['id'] ?? ''));

}//end resolveRecommendationId()

/**
* Map an engine refusal onto its HTTP status.
*
* An escalation refused for want of the manager role is a 403 and not a
* 422: the request was well-formed and the caller simply may not do it, and
* a client that cannot tell those apart will offer a retry that can never
* succeed.
*
* @param RuntimeException $exception The engine's refusal.
*
* @return JSONResponse The mapped response.
*
* @spec openspec/specs/enforcement-lhs/spec.md
*/
private function overrideRefusal(RuntimeException $exception): JSONResponse {
$message = $exception->getMessage();
$status = Http::STATUS_UNPROCESSABLE_ENTITY;
if ($message === 'Verzwaring vereist managerrol') {
$status = Http::STATUS_FORBIDDEN;
}

return new JSONResponse(['error' => $message], $status);

}//end overrideRefusal()

/**
* Override an existing LHS recommendation.
*
Expand Down Expand Up @@ -189,13 +246,13 @@ public function override(): JSONResponse {
);
}

$recommendation = $this->request->getParam('recommendation');
$recommendationId = $this->resolveRecommendationId();
$intervention = (string)$this->request->getParam('intervention', '');
$justification = (string)$this->request->getParam('justification', '');
$hasBlank = in_array('', [$intervention, $justification], true);
if (is_array($recommendation) === false || $hasBlank === true) {
$hasBlank = in_array('', [$recommendationId, $intervention, $justification], true);
if ($hasBlank === true) {
return new JSONResponse(
['error' => 'recommendation, intervention en justification zijn verplicht'],
['error' => 'recommendationId, intervention en justification zijn verplicht'],
Http::STATUS_BAD_REQUEST,
);
}
Expand All @@ -209,19 +266,13 @@ public function override(): JSONResponse {

try {
$updated = $this->lhsService->override(
recommendation: $recommendation,
recommendationId: $recommendationId,
intervention: $intervention,
justification: $justification,
userRole: $userRole,
);
} catch (RuntimeException $e) {
$message = $e->getMessage();
$status = Http::STATUS_UNPROCESSABLE_ENTITY;
if ($message === 'Verzwaring vereist managerrol') {
$status = Http::STATUS_FORBIDDEN;
}

return new JSONResponse(['error' => $message], $status);
return $this->overrideRefusal(exception: $e);
} catch (Throwable $e) {
$this->logger->error('Dossiq LHS override failed: ' . $e->getMessage());
return new JSONResponse(
Expand Down
Loading
Loading