# RD-06 — Fix the unrecoverable submit failure in the two single-step forms Status: done Source: PLAN.md 1a (the two bugs) ## Why **This is a bug fix, not a refactor.** Two forms have an unrecoverable dead end reachable from any failed submit. Both machines already support recovery; only the UI affordance is missing. Verified mechanics, per component: **`besluit-form.component.ts` — the fields are wiped.** - The template branches on `Submitted` only (line 34); `Failed` falls into the `@else` at line 36, which renders the editable form. - `editing = whenTag(state(), 'Editing')` (line 112) is `undefined` in `Failed`, so `besluit()` and `toelichting()` (lines 116-117) both return `''`. **The user's decision disappears from the screen.** - The submit button's only disable condition is `Submitting` (line 80), so in `Failed` it is **enabled**. - Clicking dispatches `Submit`, and `besluit.machine.ts:75` is `if (s.tag !== 'Editing') return s` — **a no-op**. **`change-request-form.component.ts` — the fields are frozen.** - Same `@if Submitted / @else form` shape (lines 55, 68), same always-enabled submit button (line 118), same no-op `Submit` (`change-request.machine.ts:62`). - Different in one way: `telefoon()` (lines 158-163) **does** read `Failed.data`, with the comment "so the user sees what they sent". So the value stays on screen — but `SetField` is `Editing`-only (`change-request.machine.ts:60`), so **typing does nothing**. Either way the only escape is a page reload. **The machines are already correct.** `Failed` carries `data: Valid` (`besluit.machine.ts:34`, `change-request.machine.ts:32`), and `Retry` maps `Failed → Submitting` with that preserved data (`besluit.machine.ts:80`, `change-request.machine.ts:67`). Neither UI ever dispatches it. ## Read first - `libs/shared/src/application/store.ts` — the effect map RD-05 added, and its `Seed` rule - `apps/behandelportal/src/app/behandeling/ui/besluit-form/besluit-form.component.ts` (140 lines) - `apps/behandelportal/src/app/behandeling/domain/besluit.machine.ts` (92 lines) - `apps/ssp/src/app/registratie/ui/change-request-form/change-request-form.component.ts` - `apps/ssp/src/app/registratie/domain/change-request.machine.ts` - `libs/shared/src/layout/wizard-shell/wizard-shell.component.ts:134` — the retry button and the `$localize` id to reuse - Both `*.stories.ts` for the two components ## Decisions (pre-made, don't relitigate) 1. **Give `Failed` its own template branch.** Do not let it fall through to the editable form. One change fixes both symptoms: nothing renders wiped fields, and no dead submit button exists. Shape: ``` @if (state().tag === 'Submitted') { … } @else if (state().tag === 'Failed') { error + what was sent + Retry } @else { the form } ``` 2. **Show what was sent, read from `Failed.data`.** `change-request-form.telefoon()` already does exactly this and says why in its comment. `besluit-form` copies that pattern rather than inventing one. The user must be able to see what they are retrying. 3. **The Retry button dispatches `{ tag: 'Retry' }`.** No machine change is needed — both reducers already handle it, and RD-05's effect map fires on `Failed → Submitting` because that is a tag transition. `onRetry` needs no effect call of its own. 4. **Reuse the `$localize` id `@@wizard.opnieuwProberen`** with byte-identical source text `Opnieuw proberen`. Verified present with an English target in **both** `apps/ssp/src/locale/messages.en.xlf:2429` and `apps/behandelportal/src/locale/messages.en.xlf:2373` (`Try again`). **No new xlf target is needed.** A different source text under the same id fails extraction, so do not reword it. 5. **Migrate both components to RD-05's effect map.** Delete `runIfSubmitting` from both; register the body as `{ Submitting: (s, store) => … }` on `createStore`. The narrowed state arrives as argument one, so the `const s = this.state(); if (s.tag !== 'Submitting') return;` preamble goes away. `onSubmit` becomes a single `dispatch`. 6. **Do not add a "go back and edit after a failure" path.** Neither machine has a `Failed → Editing` message, and adding one is scope creep for a bug fix. Retry recovers the dead end, which is what this ticket is for. Record the edit-after-failure gap as a follow-up in the Out of scope section. 7. **Do not change the `Reset` message or the `Seed` contract.** The stories mount states via `Seed`, and RD-05 exempts `Seed` from firing effects precisely so they do not perform real HTTP. ## Files - `apps/behandelportal/src/app/behandeling/ui/besluit-form/besluit-form.component.ts` - `apps/behandelportal/src/app/behandeling/ui/besluit-form/besluit-form.stories.ts` - `apps/ssp/src/app/registratie/ui/change-request-form/change-request-form.component.ts` - `apps/ssp/src/app/registratie/ui/change-request-form/change-request-form.stories.ts` No machine file changes. No xlf changes. ## Steps 1. In `besluit-form`, add the `Failed` branch per decisions 1-3, reading the besluit and toelichting from `Failed.data`. 2. Replace `runIfSubmitting` with an effect-map entry per decision 5; reduce `onSubmit` to one dispatch. 3. Repeat both steps for `change-request-form`. 4. Add a `Failed` story to each component's `*.stories.ts` so the new branch has a rendered, axe-checked state. Seed it directly, per decision 7. 5. Update this ticket's `Status:` to `done` and the README's RD-06 row to `done`. 6. Commit all of it together. ## Acceptance criteria ```bash npm run ci # exits 0 npm run ci --full # exits 0 — required: this ticket adds stories ``` Then prove the dead end is gone. There is no automated coverage for this, so verify by seeding the `Failed` state in Storybook (`npm run storybook` and `npm run storybook:behandelportal`) and checking all three, for **both** components: 1. The submitted values are visible — not blank. 2. No enabled control dispatches `Submit`. 3. The Retry button is present, and clicking it leaves `Failed` (it enters `Submitting`). Prove the method is gone from both forms rather than renamed. Baseline today is **5 files** (3 wizards + these 2 forms); note the registratie wizard spells it `runIfIndienen`, so both names must be matched: ```bash grep -rl "runIfSubmitting\|runIfIndienen" apps/ssp apps/behandelportal | sort # MUST be exactly these 3 (the wizards, migrated later by RD-08): # apps/ssp/.../herregistratie-wizard/herregistratie-wizard.component.ts # apps/ssp/.../intake-wizard/intake-wizard.component.ts # apps/ssp/.../registratie-wizard/registratie-wizard.component.ts ``` ## Verification `npm run ci --full`. `--full` is mandatory here: this ticket adds stories, and only `build-storybook` plus the axe run exercise them. ## Out of scope - The 3 wizards. That is RD-08, after RD-07 adds `Primary`. - **Editing after a failure.** Both machines can only `Retry` the same data or `Reset` to empty. A `Failed → Editing` transition that maps `data` back to a `draft` would be a genuine UX improvement and needs a new message plus a reducer spec. Recorded here as a follow-up; not part of this fix. - `WizardStatus`/`WizardPhase`. That is RD-10. ## Risks - **The Storybook trap.** Both components mount `Submitting` via `Seed` in stories that use a real `provideHttpClient()` with **no request mocking** (`besluit-form.stories.ts:33`, `change-request-form.stories.ts:34`). RD-05's `Seed` exemption is what stops those firing real network calls. If a story flips to `Failed` on load, or `storybook-a11y` goes red, the exemption is not working — fix that, do not delete the story. - **Do not reword the retry label.** Same id, same source text, or extraction fails (decision 4). - **`behaviour-spec.mdx` drift** if you add or rename any spec. Run `npm run gen:behaviour-spec` in the same commit if you do. - **`besluit-form` has no `*.spec.ts`.** Do not add a component TestBed spec for this — the house tests UI through Storybook (CLAUDE.md decision 5). The machines already have specs, and this ticket changes no machine.