Add ASP.NET Core backend hosting business rules; FE consumes via typed client

Move the authoritative business rules off the frontend into a real backend,
realising the BFF-lite + decision-DTO design (ADR-0001) that until now lived
only in static mock JSON.

Backend (backend/):
- ASP.NET Core (.NET 10) minimal API, contract-first, Swagger UI at /swagger.
- DDD Domain/ rules layer: profession derivation + applicable policy questions
  (DiplomaRules), herregistratie eligibility + reason (HerregistratieRule),
  scholing threshold (IntakePolicy), submit rejections + reference generation
  (SubmissionRules). In-memory seeded data, ProblemDetails (RFC 7807) errors.
- 27 xUnit tests: rule units + endpoint integration incl. BRP no-address and
  DUO not-found fallbacks and 422 submit paths.

Frontend (only infrastructure/ + contracts/ change, as the architecture promised):
- NSwag-generated typed client (api-client.ts), routed through Angular HttpClient
  via a small fetch adapter so the ?scenario= interceptor still applies.
- GET adapters use resource({ loader: client.x }); submit commands call the client
  and map ProblemDetails -> err. The hardcoded uren==0 / manual-diploma rules are
  deleted (now server-side). Domain, stores, UI and format validators unchanged.
- Deleted the now-dead public/mock/*.json.

Tooling/docs:
- npm start proxies /api -> backend; npm run gen:api regenerates the client;
  docker compose up runs both (bind mounts use :z for SELinux/Fedora).
- backend/README.md walkthrough: adding a policy question is a one-file backend
  change, no FE change, no client regen. Updated CLAUDE.md + ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
eho
2026-06-26 20:05:53 +02:00
co-authored by Claude Opus 4.8
parent 4e9af05cc1
commit cf570a8132
62 changed files with 2618 additions and 394 deletions
+54 -46
View File
@@ -18,61 +18,69 @@ import { BigProfileStore } from '@registratie/application/big-profile.store';
],
template: `
<app-page-shell heading="Mijn BIG-registratie">
@if (store.pendingHerregistratie()) {
<app-alert type="info">Uw herregistratie-aanvraag is in behandeling.</app-alert>
}
<div class="app-stack">
@if (store.pendingHerregistratie()) {
<app-alert type="info">Uw herregistratie-aanvraag is in behandeling.</app-alert>
}
<!-- ONE state for two combined services (BIG-register + BRP). -->
<app-async [data]="store.profile()">
<ng-template appAsyncLoaded let-p>
<app-registration-summary [reg]="$any(p).registration" />
<div class="rhc-card rhc-card--default" style="margin-top:1rem">
<app-heading [level]="2">Persoonsgegevens (BRP)</app-heading>
<dl class="rhc-data-summary rhc-data-summary--row">
<app-data-row key="Straat" [value]="$any(p).person.adres.straat" />
<app-data-row key="Postcode" [value]="$any(p).person.adres.postcode" />
<app-data-row key="Woonplaats" [value]="$any(p).person.adres.woonplaats" />
</dl>
</div>
</ng-template>
<ng-template appAsyncLoading>
<app-skeleton height="2.5rem" [count]="6" />
</ng-template>
</app-async>
<div style="margin-top:2rem">
<app-heading [level]="2">Specialismen en aantekeningen</app-heading>
<app-async [data]="store.aantekeningen()">
<ng-template appAsyncLoaded let-r>
<app-registration-table [rows]="$any(r)" />
<!-- ONE state for two combined services (BIG-register + BRP). -->
<app-async [data]="store.profile()">
<ng-template appAsyncLoaded let-p>
<app-registration-summary [reg]="$any(p).registration" />
<div class="rhc-card rhc-card--default app-section">
<app-heading [level]="2">Persoonsgegevens (BRP)</app-heading>
<dl class="rhc-data-summary rhc-data-summary--row">
<app-data-row key="Straat" [value]="$any(p).person.adres.straat" />
<app-data-row key="Postcode" [value]="$any(p).person.adres.postcode" />
<app-data-row key="Woonplaats" [value]="$any(p).person.adres.woonplaats" />
</dl>
</div>
</ng-template>
<ng-template appAsyncLoading>
<app-skeleton height="2.5rem" [count]="3" />
</ng-template>
<ng-template appAsyncEmpty>
<p class="rhc-paragraph">U heeft nog geen specialismen of aantekeningen.</p>
<app-skeleton height="2.5rem" [count]="6" />
</ng-template>
</app-async>
</div>
<p style="margin-top:2rem">
<app-link to="/registreren">Inschrijven in het BIG-register (registratiewizard) →</app-link>
</p>
<p>
<app-link to="/registratie">Gegevens bekijken of een wijziging doorgeven →</app-link>
</p>
<p>
<app-link to="/herregistratie">Herregistratie aanvragen →</app-link>
</p>
<p>
<app-link to="/intake">Herregistratie-intake (vragenlijst met vertakkingen) →</app-link>
</p>
<p>
<app-link to="/concepts">Functionele patronen (impossible states) →</app-link>
</p>
<section>
<app-heading [level]="2">Specialismen en aantekeningen</app-heading>
<app-async [data]="store.aantekeningen()">
<ng-template appAsyncLoaded let-r>
<app-registration-table [rows]="$any(r)" />
</ng-template>
<ng-template appAsyncLoading>
<app-skeleton height="2.5rem" [count]="3" />
</ng-template>
<ng-template appAsyncEmpty>
<p class="rhc-paragraph">U heeft nog geen specialismen of aantekeningen.</p>
</ng-template>
</app-async>
</section>
<section>
<app-heading [level]="2">Wat wilt u doen?</app-heading>
<ul class="app-card-grid app-section">
@for (a of acties; track a.to) {
<li class="rhc-card rhc-card--default">
<app-heading [level]="3">{{ a.titel }}</app-heading>
<p class="rhc-paragraph">{{ a.tekst }}</p>
<app-link [to]="a.to">{{ a.actie }} →</app-link>
</li>
}
</ul>
</section>
</div>
</app-page-shell>
`,
})
export class DashboardPage {
protected store = inject(BigProfileStore);
/** Primary actions, rendered as an accessible card grid. */
protected readonly acties = [
{ to: '/registreren', titel: 'Inschrijven', tekst: 'Schrijf u in in het BIG-register via de registratiewizard.', actie: 'Start inschrijving' },
{ to: '/registratie', titel: 'Mijn gegevens', tekst: 'Bekijk uw gegevens of geef een wijziging door.', actie: 'Bekijk gegevens' },
{ to: '/herregistratie', titel: 'Herregistratie', tekst: 'Vraag uw herregistratie aan.', actie: 'Vraag aan' },
{ to: '/intake', titel: 'Herregistratie-intake', tekst: 'Vragenlijst met vertakkingen.', actie: 'Start intake' },
{ to: '/concepts', titel: 'Functionele patronen', tekst: 'Onmogelijke toestanden onmogelijk maken.', actie: 'Bekijk patronen' },
];
}