test: close the remaining FE/BE seams (WP-75)
Three seams WP-71 documented but left unguarded.
Deletes the FE's isHerregistratieEligible and isStatusConsistent — both
uncalled, the first dead by its own doc-comment. Their tests used fixtures
completely disjoint from the backend's (the backend even had an exact-window
boundary case the FE lacked), so the two sides could diverge indefinitely
without failing anything. CLAUDE.md's policy of keeping server-owned rules
as FE "reference impls" is what kept them alive, so it is amended: the FE may
mirror a server-supplied value for instant feedback, never reimplement the
algorithm. registration.policy.ts keeps its three live exports.
check-seam.sh now also guards the Besluit tag list — the C# enum and the TS
BESLUIT_TAGS array are identical ordered name lists with nothing linking
them, and Enum.TryParse fails at request time rather than build time. Anchored
on the full declaration so it avoids the "greps all matches" trap WP-69 hit.
The phone-format divergence turned out to be real, not latent as recorded:
the backend returned 422 for +31612345678 and (06) 12345678, both of which
the FE's own parseTelefoonnummer accepts. A grep check would have compared
the shared ^0\d{9}$ regex and reported all clear — the difference was in
stripping. RejectPhoneChange now strips what the FE strips, pinned by a
contract test.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Registration, RegistrationStatus } from './registration';
|
||||
import { isHerregistratieEligible, isStatusConsistent, statusColor } from './registration.policy';
|
||||
import { Registration } from './registration';
|
||||
import { herregistratieDeadline, statusColor, statusLabel } from './registration.policy';
|
||||
|
||||
const reg = (status: Registration['status']): Registration => ({
|
||||
bigNummer: '19012345601',
|
||||
@@ -12,25 +12,10 @@ const reg = (status: Registration['status']): Registration => ({
|
||||
});
|
||||
|
||||
describe('registration.policy', () => {
|
||||
it('only an active registration within the window is eligible', () => {
|
||||
const active = reg({ tag: 'Geregistreerd', herregistratieDatum: '2027-01-01' });
|
||||
expect(isHerregistratieEligible(active, new Date('2026-06-01'))).toBe(true); // within 12 months
|
||||
expect(isHerregistratieEligible(active, new Date('2020-01-01'))).toBe(false); // too early
|
||||
});
|
||||
|
||||
it('struck-off / suspended registrations are never eligible', () => {
|
||||
expect(
|
||||
isHerregistratieEligible(
|
||||
reg({ tag: 'Doorgehaald', doorgehaaldOp: '2024-05-01', reden: 'x' }),
|
||||
new Date('2027-01-01'),
|
||||
),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isHerregistratieEligible(
|
||||
reg({ tag: 'Geschorst', geschorstTot: '2026-12-31', reden: 'x' }),
|
||||
new Date('2027-01-01'),
|
||||
),
|
||||
).toBe(false);
|
||||
it('statusLabel echoes the tag', () => {
|
||||
expect(statusLabel('Geregistreerd')).toBe('Geregistreerd');
|
||||
expect(statusLabel('Doorgehaald')).toBe('Doorgehaald');
|
||||
expect(statusLabel('Geschorst')).toBe('Geschorst');
|
||||
});
|
||||
|
||||
it('statusColor is total over the union', () => {
|
||||
@@ -39,25 +24,15 @@ describe('registration.policy', () => {
|
||||
expect(statusColor('Geschorst')).toContain('oranje');
|
||||
});
|
||||
|
||||
it('a well-formed status is always consistent', () => {
|
||||
it('herregistratieDeadline is only set for an active registration', () => {
|
||||
expect(
|
||||
isStatusConsistent(reg({ tag: 'Geregistreerd', herregistratieDatum: '2027-01-01' }).status),
|
||||
).toBe(true);
|
||||
herregistratieDeadline(reg({ tag: 'Geregistreerd', herregistratieDatum: '2027-01-01' })),
|
||||
).toEqual(new Date('2027-01-01'));
|
||||
expect(
|
||||
isStatusConsistent(
|
||||
reg({ tag: 'Doorgehaald', doorgehaaldOp: '2024-05-01', reden: 'x' }).status,
|
||||
),
|
||||
).toBe(true);
|
||||
herregistratieDeadline(reg({ tag: 'Doorgehaald', doorgehaaldOp: '2024-05-01', reden: 'x' })),
|
||||
).toBeNull();
|
||||
expect(
|
||||
isStatusConsistent(reg({ tag: 'Geschorst', geschorstTot: '2026-12-31', reden: 'x' }).status),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('a Geregistreerd status without its herregistratieDatum is inconsistent', () => {
|
||||
// The union itself makes this unrepresentable through normal construction (every
|
||||
// Geregistreerd literal must carry a herregistratieDatum) — only reachable here by
|
||||
// bypassing the type system, the way malformed runtime/serialized data could.
|
||||
const malformed = { tag: 'Geregistreerd' } as unknown as RegistrationStatus;
|
||||
expect(isStatusConsistent(malformed)).toBe(false);
|
||||
herregistratieDeadline(reg({ tag: 'Geschorst', geschorstTot: '2026-12-31', reden: 'x' })),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { assertNever } from '@shared/kernel/fp';
|
||||
import { Registration, RegistrationStatus, StatusTag } from './registration';
|
||||
import { Registration, StatusTag } from './registration';
|
||||
|
||||
/**
|
||||
* Domain logic for a registration — pure functions, NO Angular. This is where
|
||||
@@ -32,27 +32,3 @@ export function statusColor(tag: StatusTag): string {
|
||||
export function herregistratieDeadline(reg: Registration): Date | null {
|
||||
return reg.status.tag === 'Geregistreerd' ? new Date(reg.status.herregistratieDatum) : null;
|
||||
}
|
||||
|
||||
/** A registration may apply for herregistratie only while active and within the
|
||||
window before its deadline. A struck-off or suspended registration may not.
|
||||
SERVER-OWNED RULE: this now runs on the backend (BFF), which ships the result
|
||||
as `decisions.eligibleForHerregistratie` in the dashboard view. Kept here as
|
||||
the reference implementation + unit test; the frontend no longer calls it. */
|
||||
export function isHerregistratieEligible(
|
||||
reg: Registration,
|
||||
today: Date,
|
||||
windowMonths = 12,
|
||||
): boolean {
|
||||
const deadline = herregistratieDeadline(reg);
|
||||
if (!deadline) return false;
|
||||
const windowStart = new Date(deadline);
|
||||
windowStart.setMonth(windowStart.getMonth() - windowMonths);
|
||||
return today >= windowStart;
|
||||
}
|
||||
|
||||
/** Invariant check used in tests/demos: a non-active status must not carry a
|
||||
herregistratie date. The union already enforces this structurally; this is
|
||||
the runtime statement of the same rule. */
|
||||
export function isStatusConsistent(status: RegistrationStatus): boolean {
|
||||
return status.tag === 'Geregistreerd' ? typeof status.herregistratieDatum === 'string' : true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user