feat(WP-67): merge behandelportal into this repo as a monorepo
Restructures into apps/ssp + apps/behandelportal (two Angular projects) plus libs/shared + libs/beheer (cross-app libraries), replacing WP-61's separate sibling repo. That split had already produced real drift: a hand-vendored copy of the backend's OpenAPI doc, a shared/ui+layout tree forked and silently diverging (7 files), and beheer + the styles.scss token bridge duplicated byte-for-byte across both repos. - git mv the SSP's src/app/* into apps/ssp/; fold shared/, beheer/, environments/, the Storybook docs/*.mdx, and styles.scss into libs/shared + libs/beheer (all confirmed identical between the two repos before merging). auth stays deliberately duplicated per ADR-0002 (actor-specific, expected to diverge) - amended there. - One generated API client (libs/shared), no more vendored swagger.json. - .dependency-cruiser split into a base factory + one config per app, and Storybook into .storybook-ssp/.storybook-behandelportal - both forced by the @auth/* alias resolving to different directories per app. - SiteHeaderComponent/ShellComponent gained HEADER_NAV_ITEMS/ HEADER_ADMIN_LINKS/DEBUG_PANEL injection tokens so each app supplies its own nav/admin-links/dev-panel instead of one being hardcoded. - CLAUDE.md, ARCHITECTURE.md, dependencies.md, and ADR-0002 updated; WP-67 backlog entry documents the full decision trail. npm run ci green (lint, dep:check x2, 360 tests across ssp/ behandelportal/shared/beheer, both localized builds, backend tests, snippet + api-client drift); both dev servers, both Storybook instances, and docker compose verified working. The old sibling repo (/home/eho/repos/behandelportal) is left untouched, not deleted. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { BriefViewDto } from '@shared/infrastructure/api-client';
|
||||
import {
|
||||
parseBrief,
|
||||
parseBriefView,
|
||||
parseNode,
|
||||
parseOrgTemplate,
|
||||
parseStatus,
|
||||
} from './brief.adapter';
|
||||
|
||||
const view: BriefViewDto = {
|
||||
brief: {
|
||||
briefId: 'b1',
|
||||
beroep: 'arts',
|
||||
templateId: 't1',
|
||||
drafterId: 'demo-drafter',
|
||||
status: { tag: 'submitted', submittedBy: 'demo-drafter', submittedAt: '2026-07-01' },
|
||||
placeholders: [
|
||||
{ key: 'naam', label: 'Naam', autoResolvable: true },
|
||||
{ key: 'code', label: 'Code', autoResolvable: true, fillable: false },
|
||||
],
|
||||
sections: [
|
||||
{
|
||||
sectionKey: 'aanhef',
|
||||
title: 'Aanhef',
|
||||
required: true,
|
||||
blocks: [
|
||||
{
|
||||
type: 'passage',
|
||||
blockId: 'local-1',
|
||||
content: { paragraphs: [{ nodes: [{ type: 'placeholder', key: 'naam' }] }] },
|
||||
sourcePassageId: 'p1',
|
||||
sourceVersion: 2,
|
||||
edited: true,
|
||||
},
|
||||
{
|
||||
type: 'freeText',
|
||||
blockId: 'local-2',
|
||||
content: { paragraphs: [{ nodes: [{ type: 'text', text: 'hoi', marks: ['bold'] }] }] },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
availablePassages: [
|
||||
{
|
||||
passageId: 'p1',
|
||||
scope: 'global',
|
||||
sectionKey: 'aanhef',
|
||||
label: 'Aanhef',
|
||||
content: { paragraphs: [{ nodes: [] }] },
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
passageId: 'p-neg-scholing',
|
||||
scope: 'global',
|
||||
sectionKey: 'kern',
|
||||
label: 'Onvoldoende scholing',
|
||||
content: { paragraphs: [{ nodes: [{ type: 'text', text: 'x' }] }] },
|
||||
version: 1,
|
||||
besluit: 'negatief',
|
||||
reason: 'onvoldoende_scholing',
|
||||
},
|
||||
],
|
||||
decisions: {
|
||||
canEdit: false,
|
||||
canApprove: true,
|
||||
canReject: true,
|
||||
canSend: false,
|
||||
canRevealBigNummer: false,
|
||||
},
|
||||
orgTemplate: {
|
||||
subOrgId: 'cibg-registers',
|
||||
orgName: 'CIBG — Registers',
|
||||
returnAddress: 'BIG-register\nPostbus 00000\n2500 AA Den Haag',
|
||||
footerContact: 'www.bigregister.nl\ninfo@voorbeeld.example',
|
||||
footerLegal: 'KvK 00000000',
|
||||
signatureName: 'A. de Vries',
|
||||
signatureRole: 'Hoofd Registratie',
|
||||
signatureClosing: 'Met vriendelijke groet,',
|
||||
margins: { topMm: 25, rightMm: 25, bottomMm: 25, leftMm: 25 },
|
||||
version: 1,
|
||||
},
|
||||
caseContext: {
|
||||
zorgverlenerNaam: 'Dr. A. (Anna) de Vries',
|
||||
bigNummer: '19012345601',
|
||||
beroep: 'arts',
|
||||
aanvraagReferentie: 'HER-2026-000842',
|
||||
},
|
||||
};
|
||||
|
||||
describe('brief.adapter parse boundary', () => {
|
||||
it('parses a well-formed view into the domain unions', () => {
|
||||
const r = parseBriefView(view);
|
||||
expect(r.ok).toBe(true);
|
||||
if (!r.ok) return;
|
||||
expect(r.value.brief.status).toEqual({
|
||||
tag: 'submitted',
|
||||
submittedBy: 'demo-drafter',
|
||||
submittedAt: '2026-07-01',
|
||||
});
|
||||
const [passage, free] = r.value.brief.sections[0].blocks;
|
||||
expect(passage.type === 'passage' && passage.edited).toBe(true);
|
||||
expect(free.type).toBe('freeText');
|
||||
expect(r.value.brief.placeholders[1]).toEqual({
|
||||
key: 'code',
|
||||
label: 'Code',
|
||||
autoResolvable: true,
|
||||
fillable: false,
|
||||
});
|
||||
expect(r.value.decisions).toEqual({
|
||||
canEdit: false,
|
||||
canApprove: true,
|
||||
canReject: true,
|
||||
canSend: false,
|
||||
canRevealBigNummer: false,
|
||||
});
|
||||
// Guided-drafting tags survive the boundary; the untagged passage has neither.
|
||||
expect(r.value.availablePassages[0].besluit).toBeUndefined();
|
||||
expect(r.value.availablePassages[1]).toMatchObject({
|
||||
besluit: 'negatief',
|
||||
reason: 'onvoldoende_scholing',
|
||||
});
|
||||
expect(r.value.caseContext).toEqual({
|
||||
zorgverlenerNaam: 'Dr. A. (Anna) de Vries',
|
||||
bigNummer: '19012345601',
|
||||
beroep: 'arts',
|
||||
aanvraagReferentie: 'HER-2026-000842',
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects a view whose case context is missing or malformed', () => {
|
||||
expect(parseBriefView({ ...view, caseContext: undefined }).ok).toBe(false);
|
||||
expect(
|
||||
parseBriefView({
|
||||
...view,
|
||||
caseContext: { ...view.caseContext!, bigNummer: undefined as never },
|
||||
}).ok,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('parses the org template and drops a null logoDocumentId', () => {
|
||||
const r = parseOrgTemplate(view.orgTemplate);
|
||||
expect(r.ok).toBe(true);
|
||||
if (!r.ok) return;
|
||||
expect(r.value.orgName).toBe('CIBG — Registers');
|
||||
expect(r.value.margins).toEqual({ topMm: 25, rightMm: 25, bottomMm: 25, leftMm: 25 });
|
||||
expect('logoDocumentId' in r.value).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects a view whose org template is missing or malformed', () => {
|
||||
expect(parseBriefView({ ...view, orgTemplate: undefined }).ok).toBe(false);
|
||||
expect(parseOrgTemplate({ ...view.orgTemplate, signatureName: undefined }).ok).toBe(false);
|
||||
expect(
|
||||
parseOrgTemplate({
|
||||
...view.orgTemplate,
|
||||
margins: { topMm: 25, rightMm: 25, bottomMm: 25 },
|
||||
}).ok,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects a view whose decisions are missing or malformed', () => {
|
||||
expect(parseBriefView({ ...view, decisions: undefined as never }).ok).toBe(false);
|
||||
expect(
|
||||
parseBriefView({ ...view, decisions: { ...view.decisions, canSend: 'yes' as never } }).ok,
|
||||
).toBe(false);
|
||||
// The PII-reveal flag (PRD-0002 §5c) is required at the boundary too.
|
||||
expect(
|
||||
parseBriefView({
|
||||
...view,
|
||||
decisions: { ...view.decisions, canRevealBigNummer: undefined as never },
|
||||
}).ok,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('narrows node variants and rejects unknown ones', () => {
|
||||
expect(parseNode({ type: 'text', text: 'x' })).toEqual({
|
||||
ok: true,
|
||||
value: { type: 'text', text: 'x' },
|
||||
});
|
||||
expect(parseNode({ type: 'placeholder', key: 'k' })).toEqual({
|
||||
ok: true,
|
||||
value: { type: 'placeholder', key: 'k' },
|
||||
});
|
||||
expect(parseNode({ type: 'lineBreak' })).toEqual({ ok: true, value: { type: 'lineBreak' } });
|
||||
expect(parseNode({ type: 'text' }).ok).toBe(false); // missing text
|
||||
expect(parseNode({ type: 'bogus' } as never).ok).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects a status DTO missing its required fields', () => {
|
||||
expect(parseStatus({ tag: 'submitted' }).ok).toBe(false); // no submittedBy/At
|
||||
expect(parseStatus({ tag: 'rejected', rejectedBy: 'x', rejectedAt: 't' }).ok).toBe(false); // no comments
|
||||
expect(parseStatus({ tag: 'draft' }).ok).toBe(true);
|
||||
});
|
||||
|
||||
it('reads section.locked (default false) and paragraph.list', () => {
|
||||
const r = parseBrief({
|
||||
...view.brief,
|
||||
sections: [
|
||||
{
|
||||
sectionKey: 'aanhef',
|
||||
title: 'Aanhef',
|
||||
required: true,
|
||||
locked: true,
|
||||
blocks: [
|
||||
{
|
||||
type: 'freeText',
|
||||
blockId: 'b1',
|
||||
content: {
|
||||
paragraphs: [
|
||||
{ nodes: [{ type: 'text', text: 'een' }], list: 'bullet' },
|
||||
{ nodes: [{ type: 'text', text: 'twee' }], list: 'number' },
|
||||
{ nodes: [{ type: 'text', text: 'plat' }] },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{ sectionKey: 'kern', title: 'Kern', required: true, blocks: [] }, // no `locked` → false
|
||||
],
|
||||
});
|
||||
expect(r.ok).toBe(true);
|
||||
if (!r.ok) return;
|
||||
const [aanhef, kern] = r.value.sections;
|
||||
expect(aanhef.locked).toBe(true);
|
||||
expect(kern.locked).toBe(false);
|
||||
expect(aanhef.blocks[0].content.paragraphs.map((p) => p.list)).toEqual([
|
||||
'bullet',
|
||||
'number',
|
||||
undefined,
|
||||
]);
|
||||
});
|
||||
|
||||
it('rejects a library passage with an unknown scope', () => {
|
||||
const r = parseBriefView({
|
||||
...view,
|
||||
availablePassages: [{ ...view.availablePassages![0], scope: 'bogus' as never }],
|
||||
});
|
||||
expect(r.ok).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects a passage block missing provenance', () => {
|
||||
const r = parseBrief({
|
||||
...view.brief,
|
||||
sections: [
|
||||
{
|
||||
sectionKey: 's',
|
||||
title: 'S',
|
||||
required: false,
|
||||
blocks: [{ type: 'passage', blockId: 'b', content: { paragraphs: [] } }],
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(r.ok).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user