A process owner can set up invoice approval with separation of duties, without a developer
- Dominant language
- C#
- Stars
- 6
- Forks
- 7
- Avg merge
- 4h 42m
- Merged PRs (30d)
- 307
Description
A concrete acceptance test for the configure-not-code goal, narrower than #338 and worth doing first because it is the scenario that sells the product.
> A process owner with no coding experience spins up barakoCMS, defines an invoice in the admin UI, gives one group permission to raise invoices and another group permission to approve them, and on approval the invoice is emailed to the supplier. They configure the email themselves.
Traced against the code, step by step. Four of the six steps already work.
## What works today
**Spin up and define the type.** `admin/src/app/(admin)/schemas/new` is a real content type builder, and `FieldTypeRegistry` accepts money, decimal, date and the rest. A process owner can define an invoice's fields without help.
**A group that can only raise invoices.** `ContentTypePermission` is per content type slug with separate Create, Read, Update and Delete rules, and `PermissionResolver` enforces them server side. The roles and user-groups pages configure it.
**A workflow that emails.** `EmailAction` is registered, the workflow builder exists at `workflows/new`, and `ITemplateVariableExtractor` fills `{{Title}}` style variables with a picker in the UI.
## What blocks it
### 1. There is no approve, so the separation of duties cannot be expressed
`PermissionResolver.CanPerformActionAsync` accepts exactly four actions: create, read, update, delete. `Features/Content/ChangeStatus/Endpoint.cs:58` checks `"update"`.
So the person who fills in an invoice can approve it. That is not a missing convenience, it is the control the scenario exists to demonstrate, and it is the first thing an auditor asks about. Separation of duties is the whole point of having two groups.
What it needs is permission on the transition rather than on the record: who may move this type from one state to another. That is a different question from who may edit it, and no amount of Create and Update configuration approximates it.
### 2. There is no approved state
`ContentStatus` is `Draft`, `Published`, `Archived`, in the core, for every content type. An invoice is Draft, Submitted, Approved, Sent, Paid.
`Published` can be pressed into service as "approved" for a demo, and doing so is how a demo becomes a system nobody can explain. This is #338's second gap and it is the reason this issue depends on it.
### 3. A workflow cannot trigger on approval
`WorkflowDefinition.TriggerEvent` is Created or Updated. "When an invoice becomes Approved" is a transition, and an Updated trigger fires on every keystroke-level save, so routing to the supplier on approval would also route on every edit.
This is the smallest of the three and it is what makes the other two visible: a transition trigger has no meaning until transitions do.
### 4. Email is not configurable from the admin UI
`ResendEmailService` reads `IConfiguration`, so SMTP or API credentials come from appsettings or environment variables. The process owner in the scenario cannot set up email themselves; someone has to edit the deployment.
`SystemSetting` already exists as a stored, admin-editable settings document, and there is a settings page. The gap is that email does not read from it.
This one carries a real constraint rather than being purely mechanical: an API key stored in a document is a secret at rest, so it wants the same treatment as `MfaSecretProtector`, and the note in `SECURITY.md` about rotating that key applies. Configuration and stored settings also have to have a defined precedence, or an operator will change one and watch the other win.
## Why this is worth separating from #338
#338 is the full modelling epic: relations, computed fields, numbering, schedules. This scenario needs none of them. An invoice with a supplier email typed into a field is enough to demonstrate the whole loop end to end, and the loop is what makes the case for the rest.
The order is 2, then 1, then 3, then 4. State first because permissions and triggers both hang off it. 4 is independent and can go any time.
## Done when
The scenario at the top can be performed by someone who has never opened a terminal, and a user in the raise-invoices group receives a 403 when they try to approve their own invoice, asserted by a test rather than by the button being hidden. CLAUDE.md section 9 is explicit that the admin UI hiding a control is not access control, and this is the case that rule was written for.
Depends on #338 for the lifecycle. Related: #325 for routing to something other than email.
Contributor guide
Assessment
This issue has not been assessed yet.