eho 3ed130de25 feat(portal-frontend): add router with worklist and case-detail routes
Empty placeholder components for worklist-list and case-detail,
wired into app.routes.ts (legacy/:id and owned/:id share one page
component since the rendered shape is identical). Replaces the CLI
welcome template with a minimal shell rendering <router-outlet>.
2026-07-31 08:47:13 +02:00

Strangler seam demo: Behandel portaal

A reference demo, not a product. It makes four integration seams and three write paths between a legacy system and its replacement runnable, so a migration strategy can be watched instead of slide-decked.

Run it

docker compose up -d --build

Then open http://localhost:8080. That's the only host port published — legacy-backend and case-framework are deliberately unreachable from the host (see §4 below).

Memory: SQL Server (legacy-db) needs roughly 2GB of RAM; budget ~6GB total for Docker/Podman. First start takes a minute or two while SQL Server initialises (the healthcheck has a 60s start_period) — the app containers wait on it before running their own migrations and seed data.

Verify everything end to end:

./scripts/smoke.sh

Run this against a freshly started stack — it depends on the untouched seed data (legacy ids 10011012, owned ids REG-2026-0001..0005).

What's built so far (Session 1 — backend)

Nine containers: two frontends (one placeholder, see below), three backends, three databases, one proxy. new-frontend in this session is a deliberately plain, unstyled HTML page (new-frontend/index.html) that exercises the same API a real UI would — it exists to prove the backend before a real Angular portal replaces it in Session 2, not to be a good UI.

The seams and write paths

Seam / write path Direction Implementation Proven by
A — Read ACL new backend → legacy API new/src/New.Infrastructure.Legacy/LegacyCaseSource.cs, LegacyWorklistReader.cs GET /api/worklist returns 17 merged items
B — Write-through ACL new backend → legacy API new/src/New.Infrastructure.Legacy/LegacyDetailsWriteThroughTranslator.cs valid edit persists to legacy-db; a 3-field-invalid payload returns 3 mapped field errors
C — Redirect new frontend → legacy frontend CaseDetailResponseFactory.BuildLegacyActions (new/src/New.Api/Contracts/CaseDetailResponseFactory.cs) a legacy case's recordAssessment action has mode: "redirect"
D — Conformist new backend → case framework new/src/New.Infrastructure.CaseFramework/CaseFrameworkGateway.cs case-framework's 409-on-open-task rule surfaces as closurePending on assessment
Redirect write path legacy enforces legacy/src/Legacy.Web/Pages/Beoordeling.cshtml outbound button, not a form
Write-through write path legacy enforces PUT /api/worklist/legacy/{id}/details Gevalideerd door het legacy systeem-equivalent: every legacy error surfaces, none invented
Owned write path new domain enforces New.Application.Assessments.RecordOwnedAssessmentHandler, UpdateOwnedApplicantDetailsHandler direct invalid payload to the assessment endpoint returns 422
Take ownership the strangler step New.Application.Ownership.TakeOwnershipHandler POST /api/worklist/legacy/{id}/take-ownership flips the resolver, seam inspector, and legacy's MIGRATED flag together
Release ownership reversal New.Application.Ownership.ReleaseOwnershipHandler 204 with no edits, 409 once domain_writes_since > 0

The single component that knows both sources exist is New.Api.Resolution.ApplicationSourceResolver — enforced by Architecture.Tests (rule 7), along with 10 other rules (project-reference direction, no bare Status in the domain, no SQL Server package reference anywhere under new/, ...). Run them with:

cd new && dotnet test tests/Architecture.Tests

Why two database engines

legacy-db is SQL Server 2022; new-db and case-db are PostgreSQL 16. This isn't decoration — a single shared engine would let an implementer quietly join across schemas or share a DbContext, and the seam would evaporate. Two engines force the read ACL to be a real HTTP call (§7.2), force the take-ownership step ordering in TakeOwnershipHandler to be a real constraint rather than a stylistic choice (no distributed transaction is available across them), and make the legacy type vocabulary (CHAR/BIT/DATETIME2, space-padded BSNs, local-time timestamps) into real work for LegacyAanvraagMapper instead of a copy-paste.

Deliberate substitutions and omissions

  • Legacy.Web (Razor Pages) stands in for WinUI. WinUI can't be containerised; a server-rendered, table-heavy, deliberately dated UI reads as "legacy" just as effectively.
  • No auth. Out of scope for the whole demo — see docs/adr/ for what is in scope.
  • No data sync between the two databases — documented, not built. See docs/sync-not-implemented.md for its two visible consequences (adopted legacy rows show as stale-and-locked, and ownership release is blocked once edits exist).
  • No bulk migration tooling. Ownership is taken one legacy case at a time, as an interim mechanism — see docs/adr/ADR-003-ownership-is-taken-per-case.md for why, and for the intended path to a future bulk cutover for processes that want one.

Architecture Decision Records

  • ADR-001 — a register decision takes effect independently of case-framework closure.
  • ADR-002 — the write-through translator carries no business rules.
  • ADR-003 — ownership is taken per-case for now; bulk migration is a planned, separate capability.
  • sync-not-implemented.md.

10-minute click-through

  1. WerkvoorraadGET /api/worklist (or the placeholder page at /): 17 cases from two databases in one list. Filter ?origin=Legacy / ?origin=Owned to see which is which.
  2. A-1001 (GET /api/worklist/legacy/1001) — all three write paths visible in one actions block; the seams block names where each section's data comes from.
  3. Gegevens wijzigen with a bad payloadPUT /api/worklist/legacy/1001/details with a blank surname, missing house number, and malformed postcode returns three field-level errors, one per input.
  4. Beoordeling on a legacy case — actions.recordAssessment.mode == "redirect"; following it lands on /legacy/aanvraag/1001/beoordeling, outside the new portal.
  5. A-1002 — take ownership. POST /api/worklist/legacy/1002/take-ownership201. Re-fetch the same case by its new id: the seams block now reads owned throughout, the redirect and write-through actions are gone, replaced by owned-mode actions. This is the argument the whole demo is making — same screen, same two actions, only the authority changed.
  6. /legacy — row 1002 now renders greyed out with a beheerd in nieuw portaal link back into the new portal.
  7. A-1005POST /api/worklist/legacy/1005/take-ownership422, naming Bsn.ElevenProof. Three more distinct adoption failures exist at 1003 (contact), 1006 (motivation), 1007 (partial address) — one failure looks like a bug, four look like a policy.
  8. REG-2026-0002POST /api/worklist/owned/00000000-0000-0000-0000-000000000002/assessment succeeds and reports closurePending: true; the case-framework's own closure-request is genuinely conflicted (an open task), and the decision stands regardless.

Step 5 is the argument; everything before it is setup, everything after is evidence that the boundaries hold.

S
Description
No description provided
Readme
265 KiB
Languages
C# 69.8%
TypeScript 16.9%
HTML 8.3%
Shell 3.3%
Dockerfile 1.2%
Other 0.5%