Files
atomic-design-poc/docs/backlog/WP-23-org-template-backend.md
Edwin van den Houdt 5a610c10f0 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>
2026-07-05 11:17:05 +02:00

6.5 KiB
Raw Blame History

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, 1050) — 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.csMarginsDto, 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.csSubOrgId, SentOrgTemplateVersion, pin at Send.
  • backend/src/BigRegister.Api/Domain/Authorization/Authz.csAdmin role, capability, gate.
  • backend/src/BigRegister.Api/Domain/Documents/DocumentCategory.csorg-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

  • Publish increments publishedVersion and appends to history; rollback copies an old version into the draft without rewriting history.
  • Every admin endpoint returns 403 for drafter/approver, 200 for X-Role: admin.
  • 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).
  • Publish impact count = number of unsent briefs of that sub-org.
  • PUT with out-of-bounds margins → 400.
  • GET /brief carries orgTemplate; existing brief tests stay green.
  • GET /me with X-Role: admin["orgtemplate:edit"].
  • 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).