Restructure into DDD bounded contexts + functional state management

Reorganise from atomic-design-only folders into bounded contexts
(auth / registratie / herregistratie) over a shared kernel, each split into
domain / application / infrastructure / ui layers. Dependencies point inward;
the domain layer is framework-free. Path aliases (@shared/@auth/@registratie/
@herregistratie) make import direction explicit.

State management (Elm-style, native TS, no new deps):
- shared/application/store.ts — createStore(init, update): pure reducer + signal
- shared/application/remote-data.ts — add map/map2/map3/andThen combinators so
  several services fold into one RemoteData; <app-async> gains an [rd] input
- registratie/application/big-profile.store.ts — root singleton combining the
  BIG-register and BRP services via map2 into one state; holds the optimistic
  herregistratie flag shared with the dashboard
- herregistratie: machine gains a WizardMsg union + pure reduce; submit is a
  command that calls infra and dispatches the result, with optimistic update +
  rollback against the shared store
- auth: SessionStore + DigiD adapter + functional route guard; login establishes
  the session, protected routes use canActivate

Rich domain: registration.policy.ts (statusColor/label, herregistratie
eligibility, invariants); BigNummer/Postcode/Uren value objects with smart
constructors. status-badge is now domain-free (colour/label inputs).

Specs for the reducer, RemoteData combinators, and eligibility policy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 07:20:13 +02:00
parent 6bd6e854c7
commit 2114514ad7
74 changed files with 841 additions and 347 deletions

View File

@@ -0,0 +1,59 @@
import { Component, output, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { FormFieldComponent } from '@shared/ui/form-field/form-field.component';
import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
import { ButtonComponent } from '@shared/ui/button/button.component';
import { HeadingComponent } from '@shared/ui/heading/heading.component';
import { Postcode, parsePostcode } from '@registratie/domain/value-objects/postcode';
/** A submitted change request carries a *parsed* postcode (branded Postcode),
not a raw string — downstream code can't receive an unvalidated one. */
export interface ChangeRequest {
street: string;
zip: Postcode;
city: string;
}
/** Organism: change-request (adreswijziging) form. Reuses the same form-field
molecule + text-input/button atoms as the login form. Field errors come
straight from the parser's Result — no parallel "is it valid" flag to drift. */
@Component({
selector: 'app-change-request-form',
imports: [FormsModule, FormFieldComponent, TextInputComponent, ButtonComponent, HeadingComponent],
template: `
<app-heading [level]="2">Adreswijziging doorgeven</app-heading>
<form (ngSubmit)="onSubmit()" style="max-width:28rem">
<app-form-field label="Straat en huisnummer" fieldId="street" [error]="streetError()">
<app-text-input inputId="street" [(ngModel)]="street" name="street" [invalid]="!!streetError()" />
</app-form-field>
<app-form-field label="Postcode" fieldId="zip" [error]="zipError()">
<app-text-input inputId="zip" [(ngModel)]="zip" name="zip" placeholder="1234 AB" [invalid]="!!zipError()" />
</app-form-field>
<app-form-field label="Woonplaats" fieldId="city">
<app-text-input inputId="city" [(ngModel)]="city" name="city" />
</app-form-field>
<div style="margin-top:1rem">
<app-button type="submit" variant="primary">Wijziging indienen</app-button>
</div>
</form>
`,
})
export class ChangeRequestFormComponent {
street = '';
zip = '';
city = '';
streetError = signal('');
zipError = signal('');
submitted = output<ChangeRequest>();
onSubmit() {
const street = this.street.trim();
this.streetError.set(street ? '' : 'Vul straat en huisnummer in.');
const postcode = parsePostcode(this.zip);
this.zipError.set(postcode.ok ? '' : postcode.error);
if (!street || !postcode.ok) return;
this.submitted.emit({ street, zip: postcode.value, city: this.city.trim() });
}
}