import { Injectable, inject, resource } from '@angular/core'; import { Result, ok, err } from '@shared/kernel/fp'; import { ApiClient, DashboardViewDto, RegistrationDto, RegistrationStatusDto, PersonDto, } from '@shared/infrastructure/api-client'; import { Registration, RegistrationStatus, HerregistratieDecisions, } from '@registratie/domain/registration'; import { Person } from '@registratie/domain/person'; import { BigProfile } from '@registratie/domain/big-profile'; /** * The parsed, frontend-side view: the wire DTO mapped onto our own domain model. * Lives HERE, not in a `contracts/*.dto.ts` file, because it references domain * types — that split is the decoupling seam (CLAUDE.md §1, ADR-0001). */ export interface DashboardView { profile: BigProfile; decisions: HerregistratieDecisions; } /** * Infrastructure adapter for the screen-shaped ("BFF-lite") dashboard endpoint. * ONE call returns registration + person + server-computed decisions. The data * comes from the .NET backend (`GET /api/dashboard-view`) via the generated typed * client; the decisions (e.g. herregistratie eligibility) are computed there. */ @Injectable({ providedIn: 'root' }) export class DashboardViewAdapter { private client = inject(ApiClient); // The value is still untrusted JSON — parseDashboardView validates it at the // boundary and maps DTO → domain before the app uses it. dashboardViewResource() { return resource({ loader: () => this.client.dashboardView() }); } } /** Trust-boundary parse of the status union — the generated `RegistrationStatusDto` flattens all three variants into one object with every field optional (NSwag can't express a discriminated union), so the tag drives which fields must actually be present. Mirrors `parseAanvraagStatus` in `aanvragen.adapter.ts`. */ export function parseRegistrationStatus( s: RegistrationStatusDto | undefined, ): Result { if (!s || typeof s.tag !== 'string') return err('registration: missing status'); switch (s.tag) { case 'Geregistreerd': if (typeof s.herregistratieDatum !== 'string') return err('registration: bad Geregistreerd status'); return ok({ tag: 'Geregistreerd', herregistratieDatum: s.herregistratieDatum }); case 'Geschorst': if (typeof s.geschorstTot !== 'string' || typeof s.reden !== 'string') return err('registration: bad Geschorst status'); return ok({ tag: 'Geschorst', geschorstTot: s.geschorstTot, reden: s.reden }); case 'Doorgehaald': if (typeof s.doorgehaaldOp !== 'string' || typeof s.reden !== 'string') return err('registration: bad Doorgehaald status'); return ok({ tag: 'Doorgehaald', doorgehaaldOp: s.doorgehaaldOp, reden: s.reden }); default: return err(`registration: unknown status tag ${s.tag}`); } } function parseRegistration(dto: RegistrationDto | undefined): Result { if ( !dto || typeof dto.bigNummer !== 'string' || typeof dto.naam !== 'string' || typeof dto.beroep !== 'string' || typeof dto.registratiedatum !== 'string' || typeof dto.geboortedatum !== 'string' ) { return err('dashboard-view: missing/invalid registration'); } const status = parseRegistrationStatus(dto.status); if (!status.ok) return status; return ok({ bigNummer: dto.bigNummer, naam: dto.naam, beroep: dto.beroep, registratiedatum: dto.registratiedatum, geboortedatum: dto.geboortedatum, status: status.value, }); } function parsePerson(dto: PersonDto | undefined): Result { const a = dto?.adres; if ( !dto || typeof dto.naam !== 'string' || typeof dto.geboortedatum !== 'string' || !a || typeof a.straat !== 'string' || typeof a.postcode !== 'string' || typeof a.woonplaats !== 'string' ) { return err('dashboard-view: missing/invalid person'); } return ok({ naam: dto.naam, geboortedatum: dto.geboortedatum, adres: { straat: a.straat, postcode: a.postcode, woonplaats: a.woonplaats }, }); } /** * Trust-boundary parse: validate the untrusted response shape and map the DTO * onto our own domain model. Hand-written on purpose — no Zod for a single * contract. ponytail: reach for a schema lib once the contract count grows. */ export function parseDashboardView(json: unknown): Result { if (typeof json !== 'object' || json === null) return err('dashboard-view: not an object'); const dto = json as Partial; const registration = parseRegistration(dto.registration); if (!registration.ok) return registration; const person = parsePerson(dto.person); if (!person.ok) return person; const d = dto.decisions; if (!d || typeof d.eligibleForHerregistratie !== 'boolean') { return err('dashboard-view: missing/invalid decisions'); } return ok({ profile: { registration: registration.value, person: person.value }, decisions: { eligibleForHerregistratie: d.eligibleForHerregistratie, herregistratieReason: d.herregistratieReason, }, }); }