import { Injectable, inject, isDevMode } from '@angular/core'; import { Result, ok, err } from '@shared/kernel/fp'; import { runResult, runSubmit } from '@shared/application/submit'; import { currentRole } from '@shared/infrastructure/role'; import { problemDetail } from '@shared/infrastructure/api-error'; import { environment } from '@shared/environments/environment'; import { ApiClient, OrgTemplateAdminViewDto, OrgTemplateDto, OrgTemplateVersionDto, PublishOrgTemplateResponse, SubOrgSummaryDto, } from '@shared/infrastructure/api-client'; import { OrgTemplate, OrgTemplateAdminView, OrgTemplateVersion, PublishResult, SubOrgSummary, } from '@brief/domain/org-template'; import { parseOrgTemplate } from '@brief/infrastructure/brief.adapter'; /** * The only place admin org-template HTTP lives (ADR-0001 boundary). CRUD/publish/ * rollback go through the generated client (X-Role added by `roleInterceptor`); * `parse*` narrows the untrusted wire shape. The proefbrief is `text/html` and * `ExcludeFromDescription`'d — a hand-written fetch, same seam as `letter-preview.adapter`. * `X-Role` there is a dev-only identity stand-in (`role.ts`) and is sent only under * `isDevMode()`, mirroring `roleInterceptor`'s own dev-only registration — a production * build never sends it from this hand-written call either (BIO-012). */ const FAILED = $localize`:@@orgTemplate.action.failed:De actie is niet gelukt. Probeer het later opnieuw.`; export const PROEFBRIEF_FAILED = $localize`:@@orgTemplate.proefbrief.failed:De proefbrief kon niet worden geopend.`; @Injectable({ providedIn: 'root' }) export class OrgTemplateAdapter { private client = inject(ApiClient); async list(): Promise> { const r = await runResult(() => this.client.orgTemplates(), FAILED); if (!r.ok) return r; const out: SubOrgSummary[] = []; for (const s of r.value ?? []) { const parsed = parseSubOrg(s); if (!parsed.ok) return parsed; out.push(parsed.value); } return ok(out); } async load(subOrgId: string): Promise> { const r = await runResult(() => this.client.orgTemplateGET(subOrgId), FAILED); return r.ok ? parseAdminView(r.value) : r; } async save(subOrgId: string, draft: OrgTemplate): Promise> { const r = await runSubmit( () => this.client.orgTemplatePUT(subOrgId, { draft: toDto(draft) }), FAILED, ); return r.ok ? parseAdminView(r.value) : r; } async publish(subOrgId: string): Promise> { const r = await runSubmit(() => this.client.orgTemplatePublish(subOrgId), FAILED); return r.ok ? parsePublish(r.value) : r; } async rollback(subOrgId: string, version: number): Promise> { const r = await runSubmit(() => this.client.orgTemplateRollback(subOrgId, version), FAILED); return r.ok ? parseAdminView(r.value) : r; } /** Proefbrief: the unpublished draft rendered over a fixture letter, opened as a Blob. */ async proefbrief(subOrgId: string): Promise> { let res: Response; try { res = await fetch( `${environment.apiBaseUrl}/api/v1/admin/org-template/${encodeURIComponent(subOrgId)}/preview`, { headers: isDevMode() ? { 'X-Role': currentRole() } : {} }, ); } catch { return err(PROEFBRIEF_FAILED); } if (!res.ok) return err(await proefbriefErrorMessage(res)); return ok(await res.blob()); } } /** Trust boundary (TE-002): maps a non-OK proefbrief response to a message. Exported so a spec can call it directly instead of stubbing `globalThis.fetch`. */ export async function proefbriefErrorMessage(res: Response): Promise { try { return problemDetail(await res.json(), PROEFBRIEF_FAILED); } catch { return PROEFBRIEF_FAILED; } } // --- parse: wire → domain, validating at the boundary --- function parseSubOrg(dto: SubOrgSummaryDto): Result { if (typeof dto.subOrgId !== 'string' || typeof dto.orgName !== 'string') return err('sub-org: bad shape'); return ok({ subOrgId: dto.subOrgId, orgName: dto.orgName, publishedVersion: dto.publishedVersion ?? 0, }); } function parseVersion(dto: OrgTemplateVersionDto): Result { if (typeof dto.version !== 'number' || typeof dto.publishedAt !== 'string') return err('version: bad shape'); const template = parseOrgTemplate(dto.template); if (!template.ok) return template; return ok({ version: dto.version, publishedAt: dto.publishedAt, template: template.value }); } export function parseOrgTemplateAdminView( dto: OrgTemplateAdminViewDto, ): Result { const draft = parseOrgTemplate(dto.draft); if (!draft.ok) return draft; if (typeof dto.publishedVersion !== 'number' || typeof dto.unsentBriefs !== 'number') return err('admin-view: bad shape'); const history: OrgTemplateVersion[] = []; for (const v of dto.history ?? []) { const parsed = parseVersion(v); if (!parsed.ok) return parsed; history.push(parsed.value); } return ok({ draft: draft.value, publishedVersion: dto.publishedVersion, history, unsentBriefs: dto.unsentBriefs, }); } const parseAdminView = parseOrgTemplateAdminView; function parsePublish(dto: PublishOrgTemplateResponse): Result { if (typeof dto.version !== 'number' || typeof dto.affectedUnsentBriefs !== 'number') return err('publish: bad shape'); return ok({ version: dto.version, affectedUnsentBriefs: dto.affectedUnsentBriefs }); } // --- toDto: domain → wire (for save) --- function toDto(t: OrgTemplate): OrgTemplateDto { return { subOrgId: t.subOrgId, orgName: t.orgName, returnAddress: t.returnAddress, ...(t.logoDocumentId != null ? { logoDocumentId: t.logoDocumentId } : {}), footerContact: t.footerContact, footerLegal: t.footerLegal, signatureName: t.signatureName, signatureRole: t.signatureRole, signatureClosing: t.signatureClosing, margins: { ...t.margins }, version: t.version, }; }