The doc-comment claimed "the backend re-validates on submit as the authority" — it doesn't. Neither SubmitApplicationRequest nor IntakeRequest carries a scholing answer at all, so there's nothing to re-validate; both submit paths only apply SubmissionRules.RejectZeroUren. A crafted POST can bypass the scholing requirement entirely. States the gap and points to WP-69 (opened, not yet planned) for the enforcement, which needs a wire change. Also strengthens the F2 concurrency test to assert the persisted status matches whichever request actually won the race, and updates WP-68's own Decisions/ acceptance-criteria text to reflect two implementation choices that improved on the original write-up once real constraints surfaced: ProcessingWindow stays on ApplicationStore (StatusAt is already in the same file), and AanvraagStatusTag is not given a Concept member (would have broken AanvraagStatusTag_covers_the_published_lifecycle) — AanvraagStatus.Tag is nullable instead, null exactly for Concept. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2.5 KiB
WP-69 — Enforce the scholing threshold server-side
Status: todo Phase: 12 — DDD hardening
Why
WP-68 (F5) found that IntakePolicy's doc-comment claimed "the backend re-validates on
submit as the authority" — it doesn't. GET /intake/policy only echoes ScholingThreshold;
neither SubmitApplicationRequest (DiplomaHerkomst, Uren, Documents) nor IntakeRequest
(Uren) carries a scholing answer at all, so there's nothing for the server to re-validate.
Both submit paths apply only SubmissionRules.RejectZeroUren. A crafted POST — bypassing the
wizard entirely — can skip the scholing requirement (scholingGevolgd/punten in
intake.machine.ts) even though it's presented as mandatory in the UI. ADR-0001's canonical
"config value" example (the FE applies the threshold for instant feedback, the backend
re-validates as authority) is unenforced for the one rule it was written to illustrate.
Read first
backend/src/BigRegister.Api/Domain/Intake/IntakePolicy.cs(the corrected doc-comment, WP-68)- ADR-0001 — BFF-lite + decision DTOs §"config value"
apps/ssp/src/app/herregistratie/domain/intake.machine.ts(lageUren,scholingGevolgd,punten— the wizard's existing FE-side rule and its answers)backend/src/BigRegister.Api/Contracts/Dtos.cs(SubmitApplicationRequest,IntakeRequest,DocumentRefDto)backend/src/BigRegister.Api/Program.cs— theintakesandapplications/{id}/submitendpoints
Decisions
Not yet made — this is a placeholder WP opened by WP-68, not a ready-to-implement one. Needs
a planner pass before work starts. Open questions to resolve then:
- The request DTOs need a scholing answer field (likely mirroring
intake.machine.ts'sValidIntake.aanvullendeScholing/punten) — this is a wire change, so it touchescontracts/, the wizard's submit payload, andnpm run gen:api. - Whether to add the rule to
SubmissionRules(alongsideRejectZeroUren) or giveIntakePolicyits ownRejectMissingScholing(uren, scholing), matching the class that already owns the threshold. - Reading the answer out of the wizard's
DraftJSON was rejected in WP-68 — the backend's documented posture is that the draft is opaque (AppDbContext's header comment) — so the answer must arrive as an explicit request field, not be extracted from the opaque snapshot.
Out of scope (for now)
Implementation — this WP exists to track the gap; do not implement without a Decisions block.