One-time prettier --write so the new format:check CI gate starts green. .prettierignore excludes generated (api-client.ts, documentation.json), vendored (public/cibg-huisstijl), and backend (dotnet format owns it). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
83 lines
2.9 KiB
TypeScript
83 lines
2.9 KiB
TypeScript
import { Injectable, inject, resource } from '@angular/core';
|
|
import { Result, ok, err } from '@shared/kernel/fp';
|
|
import {
|
|
DuoLookupDto,
|
|
DuoDiplomaDto,
|
|
PolicyQuestionDto,
|
|
ManualDiplomaPolicyDto,
|
|
} from '@registratie/contracts/duo-diplomas.dto';
|
|
import { ApiClient } from '@shared/infrastructure/api-client';
|
|
|
|
/**
|
|
* Infrastructure adapter for the DUO diploma lookup, reached only through our own
|
|
* ("BFF-lite") endpoint — the anti-corruption boundary. The response carries the
|
|
* user's diplomas (each with its server-computed beroep + policy questions) and
|
|
* the manual-entry fallback policy. The frontend renders; it does not derive.
|
|
* Data comes from the .NET backend (`GET /api/duo/diplomas`) via the typed client.
|
|
*/
|
|
@Injectable({ providedIn: 'root' })
|
|
export class DuoAdapter {
|
|
private client = inject(ApiClient);
|
|
|
|
diplomasResource() {
|
|
return resource({ loader: () => this.client.diplomas() });
|
|
}
|
|
}
|
|
|
|
function parseQuestions(json: unknown): PolicyQuestionDto[] | null {
|
|
if (!Array.isArray(json)) return null;
|
|
const out: PolicyQuestionDto[] = [];
|
|
for (const q of json) {
|
|
if (typeof q !== 'object' || q === null) return null;
|
|
const p = q as Partial<PolicyQuestionDto>;
|
|
if (
|
|
typeof p.id !== 'string' ||
|
|
typeof p.vraag !== 'string' ||
|
|
(p.type !== 'ja-nee' && p.type !== 'tekst')
|
|
)
|
|
return null;
|
|
out.push({ id: p.id, vraag: p.vraag, type: p.type });
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** Trust-boundary parse: validate the untrusted response shape (diplomas, each
|
|
with derived beroep + policy questions, plus the manual-entry fallback). */
|
|
export function parseDuoLookup(json: unknown): Result<string, DuoLookupDto> {
|
|
if (typeof json !== 'object' || json === null) return err('duo-lookup: not an object');
|
|
const dto = json as Partial<DuoLookupDto>;
|
|
if (!Array.isArray(dto.diplomas)) return err('duo-lookup: missing diplomas');
|
|
|
|
const diplomas: DuoDiplomaDto[] = [];
|
|
for (const item of dto.diplomas) {
|
|
if (typeof item !== 'object' || item === null) return err('duo-lookup: invalid diploma');
|
|
const d = item as Partial<DuoDiplomaDto>;
|
|
const vragen = parseQuestions(d.policyQuestions);
|
|
if (
|
|
typeof d.id !== 'string' ||
|
|
typeof d.naam !== 'string' ||
|
|
typeof d.beroep !== 'string' ||
|
|
vragen === null
|
|
) {
|
|
return err('duo-lookup: missing/invalid diploma fields');
|
|
}
|
|
diplomas.push({
|
|
id: d.id,
|
|
naam: d.naam,
|
|
instelling: d.instelling ?? '',
|
|
jaar: typeof d.jaar === 'number' ? d.jaar : 0,
|
|
beroep: d.beroep,
|
|
policyQuestions: vragen,
|
|
});
|
|
}
|
|
|
|
const hm = dto.handmatig;
|
|
const hmVragen = hm ? parseQuestions(hm.policyQuestions) : null;
|
|
if (!hm || !Array.isArray(hm.beroepen) || hmVragen === null) {
|
|
return err('duo-lookup: missing/invalid handmatig fallback');
|
|
}
|
|
const handmatig: ManualDiplomaPolicyDto = { beroepen: hm.beroepen, policyQuestions: hmVragen };
|
|
|
|
return ok({ diplomas, handmatig });
|
|
}
|