import { Injectable, inject } from '@angular/core'; import { Result, ok, err } from '@shared/kernel/fp'; import { ApiClient, AanvraagStatusDto, ApplicationSummaryDto, ApplicationDetailDto, DraftSyncRequest, SubmitApplicationRequest, SubmitApplicationResponse, } from '@shared/infrastructure/api-client'; import { Aanvraag, AanvraagDetail, AanvraagStatus, AanvraagType, } from '@registratie/domain/aanvraag'; /** * Infrastructure adapter for the backend-owned Aanvraag aggregate — the only place * its HTTP lives (ADR-0001 anti-corruption boundary). The list is a resource; the * mutations (create/sync/cancel/submit) are thin commands the ApplicationsStore * orchestrates optimistically. The untrusted response is validated + mapped to * domain by the hand-written parse* boundary below. */ @Injectable({ providedIn: 'root' }) export class ApplicationsAdapter { private client = inject(ApiClient); /** The dashboard's application list (raw DTOs; the store parses at the boundary). */ list(): Promise { return this.client.applicationsAll(); } /** Admin: every case across all owners (WP-36; `cases:manage`). Parsed at the boundary. */ listAll(): Promise { return this.client.casesAll(); } /** Admin: delete ANY case (any owner, submitted or not — WP-36). */ deleteAny(id: string): Promise { return this.client.cases(id); } detail(id: string): Promise { return this.client.applicationsGET(id); } /** Create a Concept for a wizard type; resolves to the new aanvraag id. */ create(type: AanvraagType): Promise { return this.client.applicationsPOST({ type }).then((d) => d.id ?? ''); } /** Draft sync per step (idempotent). Keep it debounced at the call site — it is chatty. */ syncDraft(id: string, body: DraftSyncRequest): Promise { return this.client.applicationsPUT(id, body); } /** Cancel a Concept (cascades to its unlinked documents server-side). */ cancel(id: string): Promise { return this.client.applicationsDELETE(id); } submit(id: string, body: SubmitApplicationRequest): Promise { return this.client.submit(id, body); } } const AANVRAAG_TYPES: readonly string[] = ['registratie', 'herregistratie', 'intake']; /** Trust-boundary parse of the status union — the tag drives which fields must exist. */ export function parseAanvraagStatus( s: AanvraagStatusDto | undefined, ): Result { if (!s || typeof s.tag !== 'string') return err('aanvraag: missing status'); switch (s.tag) { case 'Concept': if (typeof s.stepIndex !== 'number' || typeof s.stepCount !== 'number') return err('aanvraag: bad Concept status'); return ok({ tag: 'Concept', stepIndex: s.stepIndex, stepCount: s.stepCount }); case 'Ingediend': if (typeof s.referentie !== 'string') return err('aanvraag: bad Ingediend status'); return ok({ tag: 'Ingediend', referentie: s.referentie }); case 'InBehandeling': if (typeof s.referentie !== 'string' || typeof s.manual !== 'boolean') return err('aanvraag: bad InBehandeling status'); return ok({ tag: 'InBehandeling', referentie: s.referentie, manual: s.manual }); case 'MeerInfoGevraagd': if (typeof s.referentie !== 'string' || typeof s.reden !== 'string') return err('aanvraag: bad MeerInfoGevraagd status'); return ok({ tag: 'MeerInfoGevraagd', referentie: s.referentie, reden: s.reden }); case 'Goedgekeurd': if (typeof s.referentie !== 'string') return err('aanvraag: bad Goedgekeurd status'); return ok({ tag: 'Goedgekeurd', referentie: s.referentie }); case 'Afgewezen': if (typeof s.referentie !== 'string' || typeof s.reden !== 'string') return err('aanvraag: bad Afgewezen status'); return ok({ tag: 'Afgewezen', referentie: s.referentie, reden: s.reden }); default: return err(`aanvraag: unknown status tag ${s.tag}`); } } function parseCommon(dto: ApplicationSummaryDto): Result { if (typeof dto.id !== 'string') return err('aanvraag: missing id'); if (typeof dto.type !== 'string' || !AANVRAAG_TYPES.includes(dto.type)) return err(`aanvraag: bad type ${dto.type}`); if (typeof dto.createdAt !== 'string' || typeof dto.updatedAt !== 'string') return err('aanvraag: missing timestamps'); const status = parseAanvraagStatus(dto.status); if (!status.ok) return status; return ok({ id: dto.id, type: dto.type as AanvraagType, status: status.value, documentIds: dto.documentIds ?? [], createdAt: dto.createdAt, updatedAt: dto.updatedAt, submittedAt: dto.submittedAt, owner: dto.owner, // only present on the admin cross-owner list (WP-36) }); } export function parseApplicationSummary(json: unknown): Result { if (typeof json !== 'object' || json === null) return err('aanvraag: not an object'); return parseCommon(json as ApplicationSummaryDto); } export function parseApplications(json: unknown): Result { if (!Array.isArray(json)) return err('aanvragen: not an array'); const out: Aanvraag[] = []; for (const item of json) { const parsed = parseApplicationSummary(item); if (!parsed.ok) return parsed; out.push(parsed.value); } return ok(out); } export function parseApplicationDetail(json: unknown): Result { if (typeof json !== 'object' || json === null) return err('aanvraag: not an object'); const base = parseCommon(json as ApplicationDetailDto); if (!base.ok) return base; return ok({ ...base.value, draft: (json as ApplicationDetailDto).draft ?? null }); }