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>
49 lines
2.5 KiB
Markdown
49 lines
2.5 KiB
Markdown
# 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](../../reference/architecture/0001-bff-lite-decision-dtos.md)
|
|
§"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.
|