Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-05-11
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# Design: migrate-parafering-to-or-approval-workflow

## Context

OR's `approval-workflow` spec provides `ApprovalChain` CRUD and `ApprovalStep` decision
endpoints. The exact PHP DI class for approval-chain CRUD is to be confirmed during task
OR-1.1 in the umbrella change; this design refers to it as `ApprovalWorkflowService`
(or the concrete mapper/service discovered during that task). The spec intentionally uses
the OR REST API endpoint surface as the stable reference; if the DI class is unavailable,
procest MAY call OR's REST API from the backend via an HTTP client.

## File-by-File Mapping

### `lib/Controller/ParaferingController.php` — endpoint surface unchanged

The controller's public API is preserved in full. All existing routes, request parameters,
and response shapes stay the same so that callers (including the procest frontend) require
no changes.

The controller stops managing step state directly. It delegates to `ParaferingService`
for all chain and step operations. No business logic moves into the controller; it becomes
a thin adapter between the HTTP layer and `ParaferingService`.

### `lib/Service/ParaferingService.php` — rewrite to delegate to OR ApprovalChain

`ParaferingService` is rewritten to translate parafeerroute concepts into OR's ApprovalChain
model:

| Existing operation | New implementation |
|---|---|
| Create parafeerroute (chain) | `POST /api/approval-chains` (or OR DI class) with steps derived from the route definition |
| Get parafeerroute status | `GET /api/approval-chains/{id}/objects` |
| Advance step on parafering | `POST /api/approval-steps/{id}/approve` with optional comment |
| Return voorstel (terugsturen) | `POST /api/approval-steps/{id}/reject` with mandatory comment |
| Skip step | `POST /api/approval-steps/{id}/approve` with `_meta.action: skipped` in JSON comment |
| Delegate parafering | `POST /api/approval-steps/{id}/approve` with `_meta.actorType: delegate`, `_meta.onBehalfOf`, `_meta.mandate` in JSON comment |
| Advisory step | `POST /api/approval-steps/{id}/approve` with `_meta.action: advised`, `_meta.advice` in JSON comment |

App-specific parafering semantics (delegation actorType, mandate reference, advisory text,
skipped reason) are encoded in the `comment` field as a JSON object with `text` (human-readable)
and `_meta` (machine-readable structured fields). This is the metadata-in-comment pattern
defined in the umbrella design.

The `_parafeerRouteId` stored on the voorstel object is updated to store the OR `ApprovalChain`
UUID rather than a local parafeerroute UUID. Existing parafeerroute UUIDs in legacy rows are
left as-is (frozen, read-only).

### `lib/Service/ParaferingNotificationService.php` — keep; update event source

Notifications are a procest concern and remain in procest. The service is updated to listen
on OR's `ApprovalStep` state-change events (`ApprovalStepUpdated` dispatched by OR via
Nextcloud's `IEventDispatcher`) instead of parafeer-local events.

OR dispatches `ApprovalStepApprovedEvent` and `ApprovalStepRejectedEvent` after each step
state change (see `openregister/openspec/changes/add-approval-step-events`). The service
registers `IEventListener` implementations on these two event classes; polling is no longer
required.

The notification payload (actor display name, step label, voorstel title) is unchanged from
the user perspective.

### `lib/Settings/procest_register.json` — deprecate parafeerroute schema

The `Parafeerroute` schema in `procest_register.json` is marked deprecated by adding
`"deprecated": true` and `"deprecatedSince": "<migration-release>"` fields to the schema
object. The schema is NOT deleted — existing rows remain readable via the OR API until sunset
(one major release after the migration ships).

New code MUST NOT create `Parafeerroute` objects. The repair step is updated to skip
`Parafeerroute` schema registration on new installs after migration.

## Concept Mapping Reference

| Parafeerroute concept | OR ApprovalChain equivalent |
|---|---|
| Parafeerroute (named route) | `ApprovalChain.name` |
| Parafeerder/adviseur per step | `ApprovalStep.role` = NC group ID |
| Step order | `ApprovalStep.order` |
| `pending` (active step) | `ApprovalStep.status: pending` |
| `waiting` (not yet active) | `ApprovalStep.status: waiting` |
| Paraferen | `POST .../approve` |
| Terugsturen | `POST .../reject` |
| Advance-on-parafering | OR's automatic advance-on-approval |
| Comment/reden | `comment` plain string or `{"text": "..."}` |
| actorType / onBehalfOf / mandate | `comment._meta.actorType` / `.onBehalfOf` / `.mandate` |
| Advisory text | `comment._meta.action: "advised"`, `comment._meta.advice` |
| Skip reason | `comment._meta.action: "skipped"`, `comment.text` |

## DEFERRED_QUESTIONS

1. **OR DI class name**: confirm whether `OCA\OpenRegister\Service\ApprovalChainService` or
`OCA\OpenRegister\Db\ApprovalChainMapper` is the correct DI entry point for ApprovalChain
CRUD from a PHP app (resolved during umbrella task OR-1.1 before `opsx-apply` starts).
2. **OR ApprovalStep IEventDispatcher event**: RESOLVED — OR dispatches
`OCA\OpenRegister\Event\ApprovalStepApprovedEvent` and
`OCA\OpenRegister\Event\ApprovalStepRejectedEvent` after each step state change, defined
in `openregister/openspec/changes/add-approval-step-events`. Polling is not required;
`ParaferingNotificationService` registers as an `IEventListener` on both event classes.

## Seed Data

No new schemas are introduced in OR. The `Parafeerroute` schema is deprecated (not deleted)
in `procest_register.json`. The only data-layer change is the deprecation annotation on
that schema.

Existing `Parafeerroute` rows in OR are frozen and remain accessible read-only until the
sunset release removes the schema entirely.

## Related ADRs

- **ADR-022** (primary) — apps consume OR abstractions; approval-chain is the specific
abstraction this migration delegates to.
- **ADR-031** — schema-declarative business logic; marking the schema deprecated is the
correct pattern (deprecation annotation in the register JSON, not a code-side guard).
- **ADR-008** — testing contract; end-to-end test exercising OR's approval-workflow store
is required.
- **Umbrella spec** — `hydra/openspec/changes/consume-or-approval-workflow-fleet-wide`
(policy contract this migration satisfies).
- **OR approval-workflow spec** — `openregister/openspec/specs/approval-workflow/spec.md`
(the API this migration consumes).
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Proposal: migrate-parafering-to-or-approval-workflow

## Why

Procest ships a full step-routing engine for parafering (signing routes): `ParaferingController`,
`ParaferingService`, and `ParaferingNotificationService` implement ordered steps, role-gated
decisions, advance-on-approval logic, and a `Parafeerroute` schema persisted in OR. This is
a bespoke approval-chain engine.

OpenRegister has shipped `approval-workflow` (status: implemented). It provides exactly the
same abstraction: named chains with ordered steps, each step bound to a Nextcloud group,
`pending`/`waiting`/`approved`/`rejected` state transitions, automatic advance-on-approval,
full decision history, and a REST API.

Maintaining a parallel approval-chain engine in procest violates **ADR-022** (Apps Consume
OpenRegister Abstractions). The concrete harms:

- **Duplicate role-gating logic**: procest re-implements `IUserSession` group-membership checks
that OR's `approval-workflow` already provides and tests.
- **Drift risk**: the parafeerroute step engine evolves independently — edge cases (delegation,
skip, resubmit) accumulate divergent handling.
- **No cross-app approval queries**: a manager cannot ask "all pending steps across apps"
without a single OR approval store.
- **Orphaned code**: three service classes (`ParaferingService`, `ParaferingNotificationService`,
`ParaferingController`) that replicate what OR provides — maintenance surface with no
architectural benefit.

## What

This spec migrates procest's parafering implementation to use OR's `approval-workflow` API as
the chain-state backend, while fully preserving the existing procest API surface for callers:

1. `ParaferingService` is rewritten to create and manage `ApprovalChain` objects in OR via OR's
approval-workflow API (or the equivalent OR DI class), translating parafeerroute concepts
into ApprovalChain terms.
2. `ParaferingNotificationService` is updated to listen on OR's `ApprovalStep` events instead of
parafeer-local events.
3. `ParaferingController` endpoint surface is **unchanged** — callers continue to use procest's
parafering endpoints; the controller internally delegates to the rewritten `ParaferingService`.
4. The `Parafeerroute` schema in `procest_register.json` is marked deprecated; no new rows are
written after migration; existing rows remain readable until sunset.

The existing procest specs (`parafering-actions`, `parafering-audit-trail`, `role-based-step-routing`)
remain as the consumer-facing contract surface and are not modified. The migration is
server-side only.

## Capabilities

### New Capabilities

- `parafering-via-or-approval`: Parafering chains are now backed by OR's `ApprovalChain` entity,
giving procest access to OR's role enforcement, advance-on-approval, decision history, and
cross-app approval queries for free.

### Modified Capabilities

- `parafering-actions` (spec: `procest/openspec/specs/parafering-actions/spec.md`) —
consumer-facing contract unchanged; implementation now routes through OR's approval-workflow API.
- `role-based-step-routing` (spec: `procest/openspec/specs/role-based-step-routing/spec.md`) —
step-role enforcement is now performed by OR's `approval-workflow` role check; the spec
surface for callers is unchanged.

## Affected Projects

- [x] Project: `procest` — all implementation tasks are in this repo
- [x] Project: `openregister` — no code change; verify DI class for ApprovalChain CRUD (tracked
in umbrella OR-1.1)

## Out of Scope

- Procest's parafering UX/frontend: the migration is server-side only.
- Parafering audit trail migration (covered by `migrate-parafering-to-or-audit`).
- Modifying OR's `approval-workflow` spec or API.
- Historical backfill of existing `Parafeerroute` rows into OR's ApprovalChain tables.
- Modifying `parafering-actions`, `parafering-audit-trail`, or `role-based-step-routing` specs.

## Success Criteria

- `openspec validate --strict migrate-parafering-to-or-approval-workflow` exits 0.
- `GET /api/approval-chains` returns procest parafering chains after migration.
- Existing procest parafering API tests pass without modification.
- No new `Parafeerroute` objects are created in OR after the migration ships.
- `ParaferingService`, `ParaferingController`, and `ParaferingNotificationService` contain
no bespoke step-routing state-machine code.
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# parafering-via-or-approval Specification

---
status: proposed
---

## Purpose

Define the procest-side contract for routing parafering (signing-route) chains through
OR's `approval-workflow` capability. Procest's consumer-facing API surface is preserved;
the server-side implementation delegates to OR's ApprovalChain and ApprovalStep entities.

## ADDED Requirements

### Requirement: Parafering Initiation Creates an OR ApprovalChain

SHALL be the requirement that when a voorstel is submitted for parafering, procest creates
an OR `ApprovalChain` with one step per parafeerder/adviseur in the route. No new
`Parafeerroute` rows are created after this migration ships.

#### Scenario: Voorstel submission creates OR ApprovalChain

- GIVEN a voorstel object with UUID `voorstel-abc` stored in a procest OR register
- AND a parafeerroute configured with three parafeerders in order: teamleider, afdelingshoofd, directeur
- WHEN the steller submits the voorstel for parafering
- THEN an `ApprovalChain` SHALL be created in OR with three steps (order 1, 2, 3)
- AND each step `role` SHALL be the NC group ID for the respective parafeerder/role
- AND the chain SHALL be accessible via `GET /api/approval-chains`
- AND no new `Parafeerroute` object SHALL be created in OR

#### Scenario: Existing parafeerroute rows remain read-only

- GIVEN a legacy `Parafeerroute` object with UUID `legacy-route-001` exists in OR
- WHEN procest code runs after migration
- THEN the legacy object SHALL remain readable via `GET /api/objects/{register}/{schema}/legacy-route-001`
- AND procest SHALL NOT create new `Parafeerroute` objects for any new voorstel submissions

---

### Requirement: Step Transitions Emit Via OR's Approval-Workflow API

SHALL be the requirement that all parafering step decisions (paraferen, terugsturen, adviseren,
overslaan) are emitted through OR's approval-step decision endpoints.

#### Scenario: Paraferen advances via OR approve endpoint

- GIVEN an ApprovalStep with `status: pending` for `voorstel-abc`
- AND the requesting user is a member of the step's `role` group
- WHEN the parafeerder clicks "Paraferen"
- THEN procest SHALL call `POST /api/approval-steps/{id}/approve` (or equivalent OR DI class)
- AND OR SHALL set `status: approved` and advance the next waiting step to `pending`
- AND the response from procest's parafering endpoint SHALL reflect the updated state

#### Scenario: Terugsturen emits via OR reject endpoint

- GIVEN an ApprovalStep with `status: pending` for `voorstel-abc`
- WHEN the parafeerder clicks "Terugsturen" with comment "Financiele paragraaf ontbreekt"
- THEN procest SHALL call `POST /api/approval-steps/{id}/reject` with the comment
- AND OR SHALL set `status: rejected` with `decidedAt` and `decidedBy`
- AND no next step SHALL be advanced

#### Scenario: Advisory step uses approve endpoint with meta

- GIVEN an ApprovalStep for an advisory step with `status: pending`
- WHEN the adviseur submits advice text
- THEN procest SHALL call `POST /api/approval-steps/{id}/approve` with a JSON comment
containing `{"text": "<advice>", "_meta": {"action": "advised", "advice": "<advice>"}}`
- AND OR SHALL advance the next waiting step

---

### Requirement: Notifications Observe OR ApprovalStep Events

MUST be the requirement that `ParaferingNotificationService` listens on OR's ApprovalStep
state changes to determine when to notify the next parafeerder, rather than operating on
parafeer-local events. The notification payload (actor name, step label, voorstel title)
is unchanged from the user perspective.

#### Scenario: Next parafeerder notified after step approval

- GIVEN ApprovalStep order-1 for `voorstel-abc` is approved by the teamleider
- AND OR advances ApprovalStep order-2 to `pending`
- WHEN OR dispatches an `ApprovalStepApprovedEvent`
- THEN `ParaferingNotificationService` SHALL send a Nextcloud notification to the NC user(s)
in the group bound to step order-2
- AND the notification text SHALL identify the voorstel and the requesting step

#### Scenario: Steller notified on terugsturen

- GIVEN ApprovalStep order-2 for `voorstel-abc` is rejected with comment "Paragraaf ontbreekt"
- WHEN OR dispatches an `ApprovalStepRejectedEvent`
- THEN `ParaferingNotificationService` SHALL notify the voorstel's steller
- AND the notification SHALL include the rejecting actor's name and the comment text

---

### Requirement: No New Parafeerroute Rows After Migration

MUST NOT be violated: after this migration ships, no code path in procest creates new
`Parafeerroute` objects in OR. The schema is deprecated. All new parafering chains are
OR `ApprovalChain` objects.

#### Scenario: Procest code does not write to deprecated schema

- GIVEN the migration is deployed
- WHEN any procest endpoint is called that initiates or advances a parafering flow
- THEN no OR object of schema type `Parafeerroute` SHALL be created or updated
- AND the OR object store for `Parafeerroute` SHALL contain only pre-migration rows

---

### Requirement: Existing Parafeerroute Rows Remain Readable Until Sunset

SHALL be the requirement that existing `Parafeerroute` rows written before the migration
are preserved read-only and accessible via the OR API until the schema is sunset (one major
procest release after migration). No historical backfill into OR ApprovalChain tables occurs.

#### Scenario: Legacy parafeerroute readable via OR API

- GIVEN `Parafeerroute` objects exist from before the migration
- WHEN `GET /api/objects/{register}/{schema}` is called for the parafeerroute schema
- THEN all pre-migration objects SHALL be returned with `200 OK`
- AND no write operations (POST, PUT, PATCH) SHALL succeed on the deprecated schema

---

### Requirement: End-to-End Test Exercises OR Approval-Workflow Store

MUST be the requirement that the procest test suite includes at least one end-to-end test
that creates a parafering chain via procest's API and verifies the chain and step records
exist in OR's approval-workflow store.

#### Scenario: E2E parafering test uses OR approval store

- GIVEN the test environment has OR's approval-workflow enabled
- WHEN the E2E test submits a voorstel for parafering and approves all steps
- THEN the test SHALL assert that `GET /api/approval-chains` returns the chain
- AND the test SHALL assert that all steps have `status: approved` in OR's approval tables
- AND the test SHALL NOT assert against any procest-local `Parafeerroute` table
Loading
Loading