Files
atomic-design-poc/docs/project/backlog/WP-69-intake-scholing-threshold-enforcement.md
T
ehoandClaude Opus 5 472a49f19f docs(backend): correct IntakePolicy's false re-validation claim (WP-68 F5)
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>
2026-08-05 15:42:21 +02:00

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 — the intakes and applications/{id}/submit endpoints

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's ValidIntake.aanvullendeScholing/punten) — this is a wire change, so it touches contracts/, the wizard's submit payload, and npm run gen:api.
  • Whether to add the rule to SubmissionRules (alongside RejectZeroUren) or give IntakePolicy its own RejectMissingScholing(uren, scholing), matching the class that already owns the threshold.
  • Reading the answer out of the wizard's Draft JSON 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.