Commit Graph
9 Commits
Author SHA1 Message Date
ehoandClaude Sonnet 5 5d73ca21f6 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>
2026-08-18 22:42:14 +02:00
ehoandClaude Sonnet 5 3652ff8d3f docs(test): make Given/When/Then the default BDD structure (WP-71)
bdd.mdx previously banned "Given/When/Then ceremony" outright, which
directly contradicted WP-70's own acceptance tests (Acceptance/
BesluitLifecycleTests.cs already used // Given/When/Then comments) and
the backend's organically-evolved PascalCase_snake_sentence convention,
which the doc gave zero guidance for. Reverses that rule: every test is
now structured Given -> When -> Then, with a genuinely empty phase
omitted rather than faked; present-tense declarative naming and the
one-behaviour-per-test rule are unchanged. ADR-0006 gets a cross-reference
so both documents agree everywhere, not just in acceptance tests.

Also closes out the doc's other named-but-unenforced rules found by the
audit: fixes the 5 files asserting rendered $localize copy instead of
the underlying tag/message-id (the compliant pattern already existed in
werkvoorraad-item-view.spec.ts), splits the multi-behaviour titles the
doc itself calls a smell (";", "and", "/"), and fixes bdd.mdx's own false
citation of registratie-wizard.machine.spec.ts as "one transition per
test" by actually splitting that test into one-transition-per-test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 20:25:30 +02:00
ehoandClaude Sonnet 5 28c0a250e7 test(backend): split RuleTests.cs by aggregate, refresh DDD doc (WP-71)
RuleTests.cs held five aggregates' rules as nested classes in one file,
misaligned with Domain/<Aggregate>/ and with the Acceptance/Builders/
folder convention WP-70 started. Split into Domain/<Aggregate>RuleTests.cs
(pure move — same names, same bodies, same count) plus a new
ApplicationRuleTests.cs (the enum invariant moved out of the
WebApplicationFactory-booting ApplicationTests.cs, since it's a pure
Enum.GetNames check with no business needing a web host) and
OrgTemplateRuleTests.cs (RejectDraft had no direct unit test before,
only endpoint coverage).

libs/shared/docs/layers.mdx still taught the pre-WP-67 shape (six
contexts, no apps/libs split, enforcement via ESLint) — updated to the
real monorepo structure and to dependency-cruiser as the actual
enforcement mechanism. Adds specs for registration.policy.ts's
isStatusConsistent (untested; its backend mirror is) and both apps'
auth/domain/session.ts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 20:25:05 +02:00
ehoandClaude Sonnet 5 b937e55ad3 test: close illegal-state escape hatches in spec type-safety (WP-71)
ESLint blanket-exempted every *.spec.ts from the any ban, and no gate
type-checked spec files at all (ng test is transpile-only), so a wrong
cast in a test could never fail the build. 76 `as any` + 12 `as
Extract<>` state-narrowing casts in the three biggest wizard specs read
one variant's fields off a whole-union value: if the reducer returned
the wrong variant, the assertion silently read undefined instead of
failing.

expectTag(state, tag) (libs/shared/src/testing/expect-tag.ts) asserts
and narrows in one call, replacing every one of those casts. Removes
the spec-file any exemption, adds `npm run typecheck` (tsc --noEmit
over each project's tsconfig.spec.json) to CI, and forbids production
code from importing libs/shared/src/testing via dependency-cruiser.
Backend: AanvraagBuilder now models ZaakUrl (closing the last
post-Build() mutation) and guards AtStep; null-forgiving `!` on
endpoint assertions replaced with Assert.NotNull so a null DTO fails by
name, not NullReferenceException.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 20:24:53 +02:00
ehoandClaude Sonnet 5 a73a1c6f1e test(frontend): replay real messages instead of hand-built state literals (WP-70)
Every machine spec redefined its own throwaway fixture helper (editing1/2/3,
editingWith), hardcoding fields like errors: {} that assert against shapes
the reducer may never actually produce. given(reduce, initial)(...msgs)
(libs/shared/src/testing/machine.ts) replaces them by replaying real Msgs
through the real reduce, so a fixture is provably reachable. Adds the same
idiom for value objects (unwrapOk) and RemoteData (loading/success/failure),
plus intake.acceptance.spec.ts as a worked full-journey example.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 15:31:06 +02:00
ehoandClaude Sonnet 5 af8a011819 feat(behandelportal): WP-65b beoordeling besluit (decision write)
CI / changes (pull_request) Successful in 17s
CI / lint (pull_request) Failing after 56s
CI / frontend (pull_request) Successful in 2m36s
CI / storybook-a11y (pull_request) Failing after 3m19s
CI / backend (pull_request) Failing after 1m55s
CI / api-client-drift (pull_request) Canceled after 0s
CI / e2e (pull_request) Canceled after 40s
CI / semgrep (pull_request) Canceled after 24s
Adds POST /beoordeling/{id}/besluit: a Besluit enum (Goedkeuren/Afwijzen/
MeerInfoOpvragen) backed by new Aanvraag.BesluitStatus/BesluitToelichting
columns, gated by the same BeoordelingRules.CanDecide the read side's
canBesluiten flag already uses (409 on an illegal transition, 400 on a
missing required toelichting). Mappers.ToStatusDto gains the "a recorded
decision wins" branch. FE: besluit.machine.ts + besluit-form organism
(same form idiom as change-request-form), wired into the beoordeling page
behind the server's canBesluiten flag.

Completes WP-65 (65a + 65b) — verified end-to-end against a running
backend (werkvoorraad -> beoordeling -> besluit -> status reflected back).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 09:46:20 +02:00
ehoandClaude Sonnet 5 4133b30e5d feat(behandelportal): WP-65a beoordeling detail (read) + fix unreachable medewerker login
CI / changes (pull_request) Successful in 17s
CI / lint (pull_request) Failing after 54s
CI / frontend (pull_request) Successful in 2m38s
CI / storybook-a11y (pull_request) Failing after 3m28s
CI / backend (pull_request) Successful in 2m1s
CI / semgrep (pull_request) Successful in 1m9s
CI / e2e (pull_request) Successful in 2m55s
CI / api-client-drift (pull_request) Successful in 2m1s
New GET /beoordeling/{id} shows one aanvraag's status, linked documents, and a
canBesluiten decision flag, gated by the same CanBeoordelen capability as the
werkvoorraad list. Reads through IZaakSource.ListCases rather than a new seam
method (WP-66 needs one anyway for the real write); owner BSN is masked.

Fixes a real gap found while wiring this up: the behandelportal's login was still
WP-61's copied citizen/BSN DigiD flow, so nothing ever sent X-Medewerker and the
werkvoorraad screen (WP-64) always denied in a real browser. A dev-only
medewerkerInterceptor (mirrors the existing ?role= stand-in as ?rollen=) fixes that.

WP-65's own Risks note authorized splitting read from write across sessions given
its size; this is the read half. The decision-recording mutation is next (65b).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 09:01:09 +02:00
ehoandClaude Sonnet 5 fe69caee63 feat(behandelportal): WP-64 werkvoorraad (queue) screen
CI / changes (pull_request) Successful in 15s
CI / lint (pull_request) Successful in 57s
CI / frontend (pull_request) Successful in 2m36s
CI / storybook-a11y (pull_request) Failing after 3m14s
CI / backend (pull_request) Successful in 2m1s
CI / semgrep (pull_request) Successful in 1m10s
CI / e2e (pull_request) Successful in 3m3s
CI / api-client-drift (pull_request) Successful in 2m1s
New GET /werkvoorraad endpoint lists aanvragen still open (Ingediend/InBehandeling),
gated by the medewerker capability (CanBeoordelen) rather than the admin role — reuses
the existing ApplicationSummaryDto, no new DTO. GET /me now surfaces aanvraag:beoordelen
for a behandelaar so the FE can gate with the same AccessStore/capabilityGuard idiom
every other page uses.

FE: a behandeling domain type deliberately narrower than ssp's full AanvraagStatus
union (only the two open tags — illegal states unrepresentable), composed into a
werkvoorraad-list organism from existing shared/ui molecules. Replaces WP-61's
scaffold placeholder as the app's real landing page.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-02 22:02:35 +02:00
ehoandClaude Sonnet 5 e7156c5132 feat(WP-67): merge behandelportal into this repo as a monorepo
Restructures into apps/ssp + apps/behandelportal (two Angular projects)
plus libs/shared + libs/beheer (cross-app libraries), replacing WP-61's
separate sibling repo. That split had already produced real drift: a
hand-vendored copy of the backend's OpenAPI doc, a shared/ui+layout tree
forked and silently diverging (7 files), and beheer + the styles.scss
token bridge duplicated byte-for-byte across both repos.

- git mv the SSP's src/app/* into apps/ssp/; fold shared/, beheer/,
  environments/, the Storybook docs/*.mdx, and styles.scss into
  libs/shared + libs/beheer (all confirmed identical between the two
  repos before merging). auth stays deliberately duplicated per
  ADR-0002 (actor-specific, expected to diverge) - amended there.
- One generated API client (libs/shared), no more vendored swagger.json.
- .dependency-cruiser split into a base factory + one config per app,
  and Storybook into .storybook-ssp/.storybook-behandelportal - both
  forced by the @auth/* alias resolving to different directories per app.
- SiteHeaderComponent/ShellComponent gained HEADER_NAV_ITEMS/
  HEADER_ADMIN_LINKS/DEBUG_PANEL injection tokens so each app supplies
  its own nav/admin-links/dev-panel instead of one being hardcoded.
- CLAUDE.md, ARCHITECTURE.md, dependencies.md, and ADR-0002 updated;
  WP-67 backlog entry documents the full decision trail.

npm run ci green (lint, dep:check x2, 360 tests across ssp/
behandelportal/shared/beheer, both localized builds, backend tests,
snippet + api-client drift); both dev servers, both Storybook
instances, and docker compose verified working.

The old sibling repo (/home/eho/repos/behandelportal) is left
untouched, not deleted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-02 21:01:57 +02:00