feat(fp): WP-23 — org-template backend + admin role
Second template axis (org identity: letterhead, footer, signature, margins) server-side: OrgTemplateStore with JSON version history, publish/rollback, sent-brief version pinning, admin role + capability, 5 admin endpoints, org-logo upload category. FE seam widened only (Role/Capability unions, interceptor); WP-24/26 consume it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
114
docs/backlog/WP-23-org-template-backend.md
Normal file
114
docs/backlog/WP-23-org-template-backend.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# WP-23 — Org-template backend + admin role
|
||||
|
||||
Status: done
|
||||
Phase: 6 — Brief v2 (edit-on-the-letter, org templates, server-rendered preview)
|
||||
|
||||
## Why
|
||||
|
||||
PRD "Brief opstellen v2" splits a rendered letter over two orthogonal template axes:
|
||||
the **case-type template** (section structure + placeholders — exists, unchanged) and
|
||||
a new **organization template** (appearance/identity per sub-organization: letterhead,
|
||||
footer, signature, margins). This WP builds the second axis server-side plus the
|
||||
`admin` role that will edit it (WP-26). Everything downstream (canvas WP-24, preview
|
||||
WP-25, editor WP-26) reads what this WP serves.
|
||||
|
||||
## Read first
|
||||
|
||||
- `docs/prd` — the Brief v2 PRD §2a/§3 (two axes, OrgTemplate model, invariants)
|
||||
- `backend/src/BigRegister.Api/Data/BriefStore.cs` (store idiom + `BriefSeed`)
|
||||
- `backend/src/BigRegister.Api/Domain/Authorization/Authz.cs` (emit+enforce single source)
|
||||
- `backend/src/BigRegister.Api/Data/AppDbContext.cs` (JSON-column precedent, WP-22)
|
||||
- `docs/backlog/WP-18-abac-capability-spine.md` (how the capability spine works)
|
||||
|
||||
## Decisions (pre-made, don't relitigate)
|
||||
|
||||
- **No case model, no sub-org auth scoping.** `GET /brief` stays; the brief just
|
||||
gains a `SubOrgId`. Two seeded sub-orgs (`cibg-registers`, `cibg-vakbekwaamheid`)
|
||||
exist purely so the admin editor can demo isolation.
|
||||
- **One row per sub-org**, versions as a JSON history column
|
||||
(`OrgTemplateEntity { SubOrgId PK, Draft json, PublishedVersion, History json }`) —
|
||||
the WP-22 JSON-column precedent; no extra tables. Publish = append draft snapshot
|
||||
to history + version++. Rollback = copy `History[v]` into `Draft` (history stays
|
||||
append-only; admin republishes).
|
||||
- **Sent letters are immutable**: `Send` pins `SentOrgTemplateVersion`; a sent
|
||||
brief's `BriefViewDto.orgTemplate` resolves from history, never from the current
|
||||
published version. Unsent briefs always follow the current published version —
|
||||
that is the point of the admin editor.
|
||||
- **Admin = third `X-Role` value** (`PrincipalRole.Admin`), capability
|
||||
`orgtemplate:edit` via `GET /me`. Approve/reject gain an explicit
|
||||
`Role == Approver` condition so the new role cannot slip through the SoD-only
|
||||
check. The existing `X-Admin` document-deletion seam stays untouched.
|
||||
- **Trimmed model** (PRD §3 minus): no signature image asset, no structured address
|
||||
objects, no per-template fonts. Return address / footer contact are multiline
|
||||
strings. Margins are 4 bounded ints (mm, 10–50) — server-validated.
|
||||
- **Logo = existing upload machinery**: seed an `org-logo` category (png/jpeg, 1 MB)
|
||||
under a `org-template` wizardId; the template stores only `logoDocumentId`.
|
||||
- Seed values come from the sample artifact `voorbeeldbrief-inschrijving.pdf`
|
||||
(A. de Vries / Hoofd Registratie / Postbus 00000 / info@voorbeeld.example — all fictitious).
|
||||
|
||||
## Files
|
||||
|
||||
- `backend/src/BigRegister.Api/Contracts/Dtos.cs` — `MarginsDto`, `OrgTemplateDto`,
|
||||
`OrgTemplateVersionDto`, `OrgTemplateAdminViewDto`, `SaveOrgTemplateRequest`,
|
||||
`PublishOrgTemplateResponse`, `SubOrgSummaryDto`; `BriefViewDto` + `OrgTemplate`.
|
||||
- `backend/src/BigRegister.Api/Data/OrgTemplateStore.cs` (new) — entity + store + seed.
|
||||
- `backend/src/BigRegister.Api/Data/AppDbContext.cs` — OrgTemplates DbSet + JSON converters.
|
||||
- `backend/src/BigRegister.Api/Data/Migrations/*` — new migration.
|
||||
- `backend/src/BigRegister.Api/Data/BriefStore.cs` — `SubOrgId`, `SentOrgTemplateVersion`, pin at `Send`.
|
||||
- `backend/src/BigRegister.Api/Domain/Authorization/Authz.cs` — `Admin` role, capability, gate.
|
||||
- `backend/src/BigRegister.Api/Domain/Documents/DocumentCategory.cs` — `org-logo` category.
|
||||
- `backend/src/BigRegister.Api/Program.cs` — 5 admin endpoints, `ToView` orgTemplate resolution.
|
||||
- `backend/tests/BigRegister.Tests/OrgTemplateEndpointTests.cs` (new).
|
||||
- Regenerated: `backend/swagger.json`, `src/app/shared/infrastructure/api-client.ts`.
|
||||
- FE seam only: `src/app/shared/domain/role.ts`, `shared/domain/capability.ts`,
|
||||
`shared/infrastructure/role.ts`, `shared/infrastructure/role.interceptor.ts`.
|
||||
|
||||
## Steps
|
||||
|
||||
1. DTOs (above).
|
||||
2. `OrgTemplateEntity` + `OrgTemplateStore` (list/get/saveDraft/publish/rollback/
|
||||
published/versionPayload; margin validation; seed-on-first-access, 2 sub-orgs,
|
||||
draft == published v1) + AppDbContext mapping + migration.
|
||||
3. `PrincipalRole.Admin`; `ResolvePrincipal` reads `admin`; `RoleCapabilities(Admin)`
|
||||
→ `orgtemplate:edit`; approve/reject checks require `Approver` explicitly.
|
||||
4. Endpoints: `GET /admin/org-templates`, `GET|PUT /admin/org-template/{subOrgId}`,
|
||||
`POST …/publish` (returns impact count = unsent briefs of that sub-org),
|
||||
`POST …/rollback/{version}`. All 403 for non-admin via `Authz`.
|
||||
5. `BriefEntity.SubOrgId` (seed `cibg-registers`) + `SentOrgTemplateVersion`; `Send`
|
||||
pins; `ToView` resolves published-vs-pinned into `BriefViewDto.orgTemplate`.
|
||||
6. Seed `org-logo` upload category.
|
||||
7. `npm run gen:api`.
|
||||
8. FE: widen `Role`/`Capability` unions, `currentRole()`, interceptor URL filter
|
||||
(nothing consumes them yet — WP-24/26 do).
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [x] Publish increments `publishedVersion` and appends to history; rollback copies an
|
||||
old version into the draft without rewriting history.
|
||||
- [x] Every admin endpoint returns 403 for drafter/approver, 200 for `X-Role: admin`.
|
||||
- [x] A sent brief keeps its pinned org-template version after a republish; an unsent
|
||||
brief follows the new published version (both asserted in one test).
|
||||
- [x] Publish impact count = number of unsent briefs of that sub-org.
|
||||
- [x] `PUT` with out-of-bounds margins → 400.
|
||||
- [x] `GET /brief` carries `orgTemplate`; existing brief tests stay green.
|
||||
- [x] `GET /me` with `X-Role: admin` → `["orgtemplate:edit"]`.
|
||||
- [x] Full GREEN (FE untouched functionally, but lint/test/build/storybook all pass).
|
||||
|
||||
## Verification
|
||||
|
||||
`cd backend && dotnet test`; GREEN one-liner; curl smoke: admin list/save/publish
|
||||
(200) vs drafter (403); sent-brief pin walk-through per acceptance.
|
||||
|
||||
## Out of scope
|
||||
|
||||
The canvas (WP-24), HTML preview endpoints + archive-at-send (WP-25), the admin UI
|
||||
(WP-26). Template approval chains (draft→publish is enough for the POC; flagged as
|
||||
an open question in the PRD). Sub-org-scoped brief authorization.
|
||||
|
||||
## Risks
|
||||
|
||||
`BriefViewDto` gains a field — additive, but the FE `parseBriefView` boundary and
|
||||
generated client must be regenerated in the same WP to keep the drift check green.
|
||||
Adding `PrincipalRole.Admin` touches the approve/reject SoD path: the explicit
|
||||
`Role == Approver` condition must preserve today's Forbidden-before-Conflict order
|
||||
(existing tests prove it).
|
||||
Reference in New Issue
Block a user