feat(backend): enforce the scholing threshold server-side (WP-69)
ADR-0001's own canonical "config value" example was unenforced: GET
/intake/policy echoed ScholingThreshold, but no request DTO carried a
scholing answer, so the server had nothing to re-validate. A crafted
POST could skip a requirement the wizard presents as mandatory.
IntakePolicy.RejectIncompleteScholing is the authority — three-valued
completeness (below threshold an answer is required; "nee" is legal and
still submits; punten only belong to a followed scholing), living in the
class that owns the constant so scripts/check-seam.sh keeps guarding the
FE/BE literal pair. Both submit paths call it; a violation 400s with
ProblemDetails and leaves the aanvraag a Concept. Gated on
Type == "intake" (the endpoint's switch lumps herregistratie with
intake, which has no scholing question), and guarded by `reject is null`
so a zero-uren submission is still decided on its merits.
Also fixes a live FE bug in the same rule: validateStep required punten
whenever scholingGevolgd was 'ja' regardless of lageUren, while the
template renders those fields only when lageUren — so answering 'ja'
then raising uren either blocked the user on an invisible field or
emitted aanvullendeScholing: undefined alongside punten. punten now
derives from aanvullendeScholing, so that combination is unrepresentable
in ValidIntake.
Note: EndpointTests' Worked_hours_submission_succeeds was itself
asserting the vulnerable payload ({ uren: 40 }, no answer) and needed a
complete answer added; the zero-hours rows are the ordering regression
net and are unmodified.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,8 +20,8 @@ tested where._
|
||||
|
||||
Every bullet below is a real test name from the suite — an `it()` title (frontend) or a test
|
||||
method name (backend), read as a sentence. Nothing here is hand-written prose: this page
|
||||
**is** the suite, reshaped for a business reader. 401 frontend behaviours across
|
||||
8 contexts; 206 backend behaviours across 33 test
|
||||
**is** the suite, reshaped for a business reader. 404 frontend behaviours across
|
||||
8 contexts; 219 backend behaviours across 35 test
|
||||
classes.
|
||||
|
||||
## Frontend (by context)
|
||||
@@ -312,6 +312,7 @@ classes.
|
||||
- low uren requires the scholing question, and punten only once scholing is followed
|
||||
- buitenland gewerkt requires land and hours abroad before advancing
|
||||
- gaNaarStap corrects an earlier answer without losing later ones
|
||||
- raising uren above the threshold after answering scholing drops both fields (WP-69 §6)
|
||||
- SetPolicy (server-owned threshold) can turn an already-answered uren into one that now requires scholing
|
||||
|
||||
#### intake hasProgress
|
||||
@@ -349,6 +350,8 @@ classes.
|
||||
- reaches Submitting ONLY with valid answers
|
||||
- punten is required only when aanvullende scholing was gevolgd
|
||||
- low hours requires the scholing answer before submit
|
||||
- does not require punten for a hidden question (WP-69 §6)
|
||||
- drops punten when raising uren hides the question (WP-69 §6)
|
||||
- resolve maps Submitting to Submitted on a successful submit
|
||||
- resolve maps Submitting to Failed on a failed submit
|
||||
|
||||
@@ -957,6 +960,25 @@ classes.
|
||||
- Different idempotency keys are independent submissions
|
||||
- A rejected submission replays the same rejection not a retry
|
||||
|
||||
### IntakeRuleTests
|
||||
|
||||
- Below threshold with no answer is incomplete
|
||||
- At the threshold no answer is required
|
||||
- Niet gevolgd is a complete answer below threshold
|
||||
- Gevolgd without punten is incomplete
|
||||
- Gevolgd with zero punten is valid
|
||||
- Gevolgd with negative punten is refused
|
||||
|
||||
### IntakeSubmissionTests
|
||||
|
||||
- Below threshold without an answer is rejected and stays a concept
|
||||
- Below threshold with an answer succeeds
|
||||
- Above threshold needs no answer
|
||||
- Punten without gevolgd is rejected
|
||||
- Herregistratie is unaffected by the intake only gate
|
||||
- Zero uren is still afgewezen not a 400
|
||||
- Legacy intakes endpoint enforces it too
|
||||
|
||||
### LetterHtmlTests
|
||||
|
||||
- Render matches the golden file
|
||||
|
||||
@@ -483,6 +483,12 @@ export class ApiClient {
|
||||
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as ReferentieResponse;
|
||||
return result200;
|
||||
});
|
||||
} else if (status === 400) {
|
||||
return response.text().then((_responseText) => {
|
||||
let result400: any = null;
|
||||
result400 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as ProblemDetails;
|
||||
return throwException("Bad Request", status, _responseText, _headers, result400);
|
||||
});
|
||||
} else if (status === 422) {
|
||||
return response.text().then((_responseText) => {
|
||||
let result422: any = null;
|
||||
@@ -2210,6 +2216,8 @@ export interface IntakePolicyDto {
|
||||
|
||||
export interface IntakeRequest {
|
||||
uren?: number;
|
||||
aanvullendeScholing?: boolean | undefined;
|
||||
scholingPunten?: number | undefined;
|
||||
}
|
||||
|
||||
export interface LetterBlockDto {
|
||||
@@ -2419,6 +2427,8 @@ export interface SubmitApplicationRequest {
|
||||
diplomaHerkomst?: string | undefined;
|
||||
uren?: number | undefined;
|
||||
documents?: DocumentRefDto[] | undefined;
|
||||
aanvullendeScholing?: boolean | undefined;
|
||||
scholingPunten?: number | undefined;
|
||||
}
|
||||
|
||||
export interface SubmitApplicationResponse {
|
||||
|
||||
Reference in New Issue
Block a user