Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,15 @@ Vrij en open source onder de EUPL-1.2-licentie.
install has no history to move.
-->
<step>OCA\Dossiq\Repair\MigrateAiOversightToHermiq</step>
<!--
bacAdviceRequest carried the objection id under the name of the
SCHEMA it $refs (`objectionProceeding`) instead of the property
the schema declares (`bezwaar`) — so a required property was
never written, and BezwaarDetail's advice-request widgets, which
filter on `bezwaar`, showed nothing at all. Idempotent (rows that
already have `bezwaar` are skipped) and non-fatal.
-->
<step>OCA\Dossiq\Repair\BackfillAdviceRequestObjection</step>
</post-migration>
<!--
FRESH INSTALL. Nextcloud does NOT run post-migration on a first install:
Expand Down Expand Up @@ -401,6 +410,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\MigrateTenantsCommand</command>
<command>OCA\Dossiq\Command\SeedBezwaarBeroepCommand</command>
</commands>
Expand Down
47 changes: 36 additions & 11 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,13 @@
['name' => 'subsidie#create', 'url' => '/api/subsidies', 'verb' => 'POST'],
['name' => 'subsidie#createTussenrapportage', 'url' => '/api/subsidies/uitvoeringen/{uitvoeringId}/tussenrapportages', 'verb' => 'POST'],
['name' => 'subsidie#approveTussenrapportage', 'url' => '/api/subsidies/tussenrapportages/{reportId}/beoordelen', 'verb' => 'POST'],
['name' => 'subsidie#finalizeVaststelling', 'url' => '/api/subsidies/vaststellingen/{vaststellingId}/vast', 'verb' => 'POST'],
['name' => 'subsidie#signBeschikking', 'url' => '/api/subsidies/beschikkingen/{beschikkingId}/sign', 'verb' => 'POST'],
['name' => 'subsidie#publishBeschikking', 'url' => '/api/subsidies/beschikkingen/{beschikkingId}/publish', 'verb' => 'POST'],
// The placeholder names bind BY NAME to the method parameters — see the
// note on the parafeer-route block below. These three were
// {vaststellingId}/{beschikkingId} against $determinationId/$decisionId
// and answered HTTP 400 on every call.
['name' => 'subsidie#finalizeVaststelling', 'url' => '/api/subsidies/vaststellingen/{determinationId}/vast', 'verb' => 'POST'],
['name' => 'subsidie#signBeschikking', 'url' => '/api/subsidies/beschikkingen/{decisionId}/sign', 'verb' => 'POST'],
['name' => 'subsidie#publishBeschikking', 'url' => '/api/subsidies/beschikkingen/{decisionId}/publish', 'verb' => 'POST'],
['name' => 'subsidie#transition', 'url' => '/api/subsidies/{id}/transition', 'verb' => 'POST'],
['name' => 'subsidie#createBeschikking', 'url' => '/api/subsidies/{id}/beschikking', 'verb' => 'POST'],

Expand Down Expand Up @@ -258,15 +262,32 @@
// ── Parafeerroute (B&W parafering engine) ───────────────────────
// CRUD on parafeerroute objects is served by OpenRegister's auto-exposed
// /api/objects/<register>/<schema> endpoints — only engine routes remain.
['name' => 'parafeerRoute#start', 'url' => '/api/parafeer-route/voorstel/{voorstelId}/start', 'verb' => 'POST'],
['name' => 'parafeerRoute#completeStep', 'url' => '/api/parafeer-route/voorstel/{voorstelId}/complete-step', 'verb' => 'POST'],
['name' => 'parafeerRoute#skipStep', 'url' => '/api/parafeer-route/voorstel/{voorstelId}/skip-step', 'verb' => 'POST'],
['name' => 'parafeerRoute#addStep', 'url' => '/api/parafeer-route/voorstel/{voorstelId}/add-step', 'verb' => 'POST'],
// ⚠️ THE PLACEHOLDER NAME IS LOAD-BEARING. Nextcloud's Dispatcher binds a
// controller argument by PARAMETER NAME (`$this->request->getParam($param)`),
// not by position. These four were `{voorstelId}` while every target method
// signs `string $proposalId` — so the argument resolved to null, the string
// typehint threw a TypeError, and the Dispatcher answered HTTP 400. Measured
// against the running instance before the fix: start, complete-step and
// register-besluit all returned 400 with an empty body, on every call, for
// any input.
//
// The Dutch→English vocabulary sweep renamed the method parameters and left
// the URLs behind. Nothing caught it: the route exists, the method exists,
// and gate-6 (route-reachability) checks that a route's target method is
// present — which it is. Only the NAMES disagree.
// The four parafeer-route engine routes were RETIRED. They were a second
// implementation of a flow /api/parafeer-actie already runs, and they had
// never served a request: the placeholder bound no argument, so every call
// answered 400. `start` is covered by the `besluitvormingActivate`
// transition action, `complete-step` and `skip-step` by parafeeractie's
// own `parafered`/`accorded`/`skipped` vocabulary, and `add-step` has no
// live equivalent — that button went with them.

// Voorstel → besluit registration delegates to a decidesk report-adoption
// Decision (dossiq-delegate-remaining-decisions-to-decidesk, ADR-019).
// The parafeerroute above is untouched; only the besluit decision moves.
['name' => 'voorstelBesluit#registerBesluit', 'url' => '/api/voorstellen/{voorstelId}/register-besluit', 'verb' => 'POST'],
// Same placeholder-name defect as the four routes above — see that note.
['name' => 'voorstelBesluit#registerBesluit', 'url' => '/api/voorstellen/{proposalId}/register-besluit', 'verb' => 'POST'],

// NOTE: ParaferingController + ParaferingService were superseded scaffolding
// that operated entirely in-memory (no persistence, client-supplied state).
Expand Down Expand Up @@ -324,7 +345,8 @@
// Related-case linking — typed peer relations (relevanteAndereZaken).
['name' => 'caseRelation#list', 'url' => '/api/cases/{caseId}/relations', 'verb' => 'GET'],
['name' => 'caseRelation#create', 'url' => '/api/cases/{caseId}/relations', 'verb' => 'POST'],
['name' => 'caseRelation#destroy', 'url' => '/api/cases/{caseId}/relations/{targetId}/{aardRelatie}', 'verb' => 'DELETE'],
// {aardRelatie} bound nothing: the method signs $natureRelationship. HTTP 400.
['name' => 'caseRelation#destroy', 'url' => '/api/cases/{caseId}/relations/{targetId}/{natureRelationship}', 'verb' => 'DELETE'],
// Dashboard KPI aggregation endpoint.
['name' => 'kpi#index', 'url' => '/api/dashboard/kpis', 'verb' => 'GET'],

Expand Down Expand Up @@ -577,8 +599,11 @@

// ── Besluitvorming workflow ──────────────────────────────────────
['name' => 'besluitvorming#activateTemplate', 'url' => '/api/besluitvorming/templates/{slug}/activate', 'verb' => 'POST'],
['name' => 'agenda#addToAgenda', 'url' => '/api/besluitvorming/cases/{id}/agenda', 'verb' => 'POST'],
['name' => 'agenda#updateAgendaItem', 'url' => '/api/besluitvorming/cases/{id}/agenda', 'verb' => 'PUT'],
// The two `agenda#` routes were removed with the agenda compiler: decidiq
// owns agenda-building and meetings, and it surfaces them here through the
// `decidesk-decisions` integration leaf. They served only the compiler view
// and became unreachable the moment it went, which is the dead-endpoint
// shape this programme already retired once on the AVG page.
['name' => 'publication#publish', 'url' => '/api/besluitvorming/cases/{id}/publish', 'verb' => 'POST'],
['name' => 'mandaat#mandaatCheck', 'url' => '/api/besluitvorming/cases/{id}/mandaat-check', 'verb' => 'GET'],

Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
82 changes: 58 additions & 24 deletions docs/user-guide/admin/02-automatic-actions.md
Original file line number Diff line number Diff line change
@@ -1,59 +1,93 @@
---
sidebar_position: 2
title: Set up automatic actions
description: "Create rules that fire on case events: create tasks, send notifications, transition status, kick off integrations."
description: "Automatic actions are OpenRegister flows. Migrate the old records, then build and run actions in the flow editor."
---

# Set up automatic actions

Automatische acties (automatic actions) are the rule engine that turns case events into tasks, notifications, and follow-up transitions. A rule fires on a *trigger* (a status change, a deadline crossing, a document upload), evaluates a *condition* (always, or only when a field matches), and runs an *action* (create a task, send an email, transition the case, post to an integration).
Automatic actions are **OpenRegister flows**. Dossiq contributes the action nodes — send an email, notify a role, call a webhook, create a document, merge a template, schedule a reminder — and OpenRegister's flow engine runs them.

:::warning What changed, and why this page was wrong

Dossiq used to have its own **Automatische acties** settings page. It has been retired.

An earlier version of this page described a rule engine with *triggers*, *conditions* and a *Last run* column. **None of that existed.** The stored record had no trigger field and no condition field, and nothing in the application ever executed one — the code that fires actions on a status change reads a different, separate definition that lives on the case type. A rule created through that page was saved and then never ran.

If you configured actions there, nothing was lost: the records are still stored, and the migration below turns each into a flow that does run.
:::

## Goal

By the end you will have created an automatic action that fires on a specific status transition and creates a task assigned to a role on the case.
By the end you will have migrated any existing automatic actions to flows, and know where to build new ones.

## Prerequisites

- Administrator role on the Nextcloud instance.
- At least one case type configured (see [Configure case types and workflows](./01-configure-case-types.md)).
- A clear idea of which trigger / condition / action you want to chain.
- Administrator role on the Nextcloud instance, and shell access for the migration command.
- OpenRegister installed (Dossiq requires it).
- The user you migrate as must belong to an organisation — a flow takes its owner and organisation from that user, permanently.

## Migrate existing automatic actions

## Steps
Run a dry run first. It writes nothing and shows exactly what would be created:

1. From the Dossiq navigation, click **Automatische acties** (in the configuration block). The list opens with the standard Cards/Table toggle and an **Add Item** button.
```bash
occ dossiq:actions:migrate-to-flows --user=<uid> --dry-run
```

![Automatic actions list](/screenshots/tutorials/admin/02-automatic-actions-01.png)
```
dossiq:actions:migrate-to-flows (dry run — nothing was written)
total = 1
created = 1
updated = 0
skipped = 0
failed = 0
[created] dossiq:automaticAction:<tenant>:<slug> — dry run — no write
```

2. Click **Add Item**. The rule dialog opens with four sections: *Algemeen* (name, description, active toggle), *Trigger*, *Condition*, *Action*.
Then run it for real:

![New automatic action dialog](/screenshots/tutorials/admin/02-automatic-actions-02.png)
```bash
occ dossiq:actions:migrate-to-flows --user=<uid>
```

3. Set the **Trigger**. Pick *Status change* and select the case-type plus the source and target statuses. Other triggers include *Deadline approaching*, *Document uploaded*, *Case created*, *Case closed*.
The command is safe to re-run: the second run reports `updated` rather than creating a duplicate.

![Configuring the trigger](/screenshots/tutorials/admin/02-automatic-actions-03.png)
:::caution The migrated flows are enabled
These actions have never fired before. Migrating them makes them runnable, so review each one in the flow editor before triggering it — particularly anything that sends email to an address outside your organisation. Each migrated flow uses a **manual** trigger, so it runs only when someone runs it; it will not start firing on its own.
:::

4. Set the **Condition** (optional). Leave blank for "always fire" or add a field match (e.g. *Confidentiality = Public*) so the rule only fires on matching cases. Conditions support AND/OR groups.
Reading the summary:

![Conditions](/screenshots/tutorials/admin/02-automatic-actions-04.png)
| Outcome | Meaning |
|---|---|
| `created` | A new flow was made for this action. |
| `updated` | The flow this command made earlier was refreshed. |
| `skipped` | No node implements that action's type, so no flow was written. A flow around a node that does not exist would report success and do nothing. |
| `failed` | The record is missing its tenant or slug and cannot be identified. Fix the record and re-run. |

5. Set the **Action**. Pick *Create task*, fill the task title, description, assignee role, and due-date offset. Save the rule with **Save**. Confirm the rule is *Active* in the list: only active rules fire.
## Build and run actions

![Action and save](/screenshots/tutorials/admin/02-automatic-actions-05.png)
1. From the Dossiq navigation, open **Automatische acties** in the configuration block. It takes you to OpenRegister's **Flows** page.
2. Use **New flow**, or open a migrated flow to review it.
3. A runnable flow needs an entry and an exit: a trigger node, your action node(s), and an end node, wired with edges. The migration builds exactly that shape.
4. Dossiq's action nodes appear in the node catalogue under `dossiq.action.*`.

## Verification

You have set up the action correctly when: triggering the configured status transition on a test case produces the new task in the case's **Tasks** sidebar tab, the task is assigned to the configured role, and the rule's *Last run* column in the **Automatische acties** list updates to the most recent fire time.
You have it working when the flow appears on the **Flows** page with app `dossiq`, and running it produces the effect you configured — the email arrives, the webhook is called, the document is generated.

## Common issues

| Symptom | Fix |
|---|---|
| Rule is configured but never fires | The rule's *Active* toggle is off, or the trigger case-type does not match the case-type you tested on. |
| Task is created but unassigned | The configured assignee role is not present on the case; add it to the case's Participants or update the rule to use a role that exists. |
| Action runs but no notification arrives | The notification action uses Nextcloud's mail; SMTP must be configured at the server level (the dev environment intentionally disables outgoing mail: see [PROJECT MEMORY notes on mail](../../#mail-notifications)). |
| Condition does not match what you expect | Open the rule's *Last run* details: the recorded field values clarify why the condition skipped. |
| `occ dossiq:actions:migrate-to-flows` says `--user is required` | It has no default on purpose: the created flows inherit that user's identity and organisation permanently. Pass a real uid. |
| The command reports `OpenRegister exposes no FlowService on this instance` | OpenRegister is missing or too old. Flows live in OpenRegister; Dossiq only contributes nodes. |
| An action was `skipped` | Its `type` is not one Dossiq implements a node for. The six are `sendEmail`, `notifyRole`, `callWebhook`, `createDocument`, `mergeTemplate` and `scheduleReminder`. |
| A flow saves but will not run | Check it has both a trigger node and an end node. OpenRegister reports a flow with neither as not runnable. |
| Actions attached to a status transition are not on this page | Those are a different mechanism: they live on the case type's workflow, not here. See [Configure case types and workflows](./01-configure-case-types.md). |

## Reference

- [Configure case types and workflows](./01-configure-case-types.md): defines the triggers (statuses, deadlines).
- [Case management](../../Features/case-management.md): the underlying event stream rules listen to.
- [Configure case types and workflows](./01-configure-case-types.md): status-transition actions, which are configured on the case type.
- [Case management](../../Features/case-management.md): the case lifecycle these actions hang off.
4 changes: 2 additions & 2 deletions lib/AppInfo/Registrar/ListenerRegistrar.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public function register(IRegistrationContext $context): void {
(new BezwaarListenerRegistrar())->register(context: $context);
(new WorkflowListenerRegistrar())->register(context: $context);

// ADR-065: OpenRegister owns the flow engine; procest contributes the six
// ADR-065: OpenRegister owns the flow engine; dossiq contributes the six
// things a case can DO, because every one of OpenRegister's own nineteen
// nodes is control-flow or data and none of them acts outward.
//
Expand All @@ -71,7 +71,7 @@ public function register(IRegistrationContext $context): void {
if (class_exists(\OCA\OpenRegister\Service\Flow\RegisterFlowNodesEvent::class) === true) {
$context->registerEventListener(
\OCA\OpenRegister\Service\Flow\RegisterFlowNodesEvent::class,
\OCA\Dossiq\Flow\ProcestFlowNodeListener::class
\OCA\Dossiq\Flow\DossiqFlowNodeListener::class
);
}
}//end register()
Expand Down
46 changes: 36 additions & 10 deletions lib/AppInfo/Registrar/WorkflowListenerRegistrar.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,22 @@
* @spec openspec/changes/termijnbewaking-dwangsom-engine-02-termijn-binding-lifecycle/tasks.md
*/
class WorkflowListenerRegistrar {
/**
* Every spelling of the decision-concluded event, newest first.
*
* A cross-app event class name is a RUNTIME lookup: this app cannot move it,
* only follow it. When the other app renamed its namespace without an alias,
* naming one spelling meant the listener silently stopped registering — so
* both are listed, and the listener attaches to whichever exists. The old
* entry can be dropped once no supported install still ships it.
*
* @var array<int, string>
*/
private const DECISION_CONCLUDED_EVENTS = [
'OCA\Decidiq\Event\DecisionConcludedEvent',
'OCA\Decidesk\Event\DecisionConcludedEvent',
];

/**
* Register the termijn and decision listeners.
*
Expand Down Expand Up @@ -94,16 +110,26 @@ private function registerTermListeners(IRegistrationContext $context): void {
* @spec openspec/changes/dossiq-delegation-via-events/specs/contract-decision-delegation/spec.md#requirement-req-pdcd-003-the-zgw-besluit-is-materialised-from-the-decisionconcludedevent
*/
private function registerDecisionListeners(IRegistrationContext $context): void {
if (class_exists('\\OCA\\Decidesk\\Event\\DecisionConcludedEvent') === false) {
return;
}

// FQN string (not ::class) so there is no hard compile-time dependency
// on the optional decidesk app — mirrors the OpenRegister approval-event
// BOTH spellings, and the old one is not optional politeness — it is the
// only thing that keeps this integration working during an upgrade where
// the two apps move at different times.
//
// The app renamed its namespace from OCA\Decidesk to OCA\Decidiq with no
// compatibility alias. This guard named only the OLD class, so from the
// moment that landed it returned false, the listener was never
// registered, and every concluded decision stopped materialising a ZGW
// Besluit here. Nothing errored: a class_exists() guard that goes false
// looks exactly like the optional app not being installed.
//
// FQN strings (not ::class) so there is no hard compile-time dependency
// on the optional app — mirrors the OpenRegister approval-event
// registration in BezwaarListenerRegistrar.
$context->registerEventListener(
event: 'OCA\Decidesk\Event\DecisionConcludedEvent',
listener: DecisionConcludedListener::class
);
foreach (self::DECISION_CONCLUDED_EVENTS as $event) {
if (class_exists('\\' . $event) === false) {
continue;
}

$context->registerEventListener(event: $event, listener: DecisionConcludedListener::class);
}
}//end registerDecisionListeners()
}//end class
Loading
Loading