Skip to content

Migrate Delete Route journey to GOV.UK Questions - #3698

Merged
gunndabad merged 1 commit into
mainfrom
claude/govuk-questions-delete-route-6c48f5
Aug 6, 2026
Merged

Migrate Delete Route journey to GOV.UK Questions#3698
gunndabad merged 1 commit into
mainfrom
claude/govuk-questions-delete-route-6c48f5

Conversation

@gunndabad

Copy link
Copy Markdown
Member

Context

Continues the move off WebCommon.FormFlow onto GovUk.Questions.AspNetCore (DFE issue #369).
This one is Delete Route to professional status.

Delete Route is the simplest of the three route journeys — a linear walk of Index → Reason →
Check answers — so it follows Add Route (#3685) rather than the hub-and-spoke path model Edit
Route (#3691) needed. Nothing here is novel; it's worth reading mainly for the two things I
deleted rather than ported.

Based on main, not on #3691. The only file the two branches share is
EditRoute/PermissionsTests, which covers both journeys — see below.

Changes proposed in this pull request

DeleteRouteJourneyCoordinator holds what the pages shared — the page caption, the person
id, and CancelAsync (delete the uploaded evidence file, drop the instance, return to the
record). DeleteRouteState loses IRegisterJourney, its Initialized flag and its
IsComplete computed property, and is now plain data.

Index is [StartsJourney] and advances to Reason with SetAsFirstStep, so Reason is step 0
— its back link is null and falls back to the person's qualifications — and the redirect page
doesn't stay in the path.

The check answers completeness guard is gone. Unlike Edit Route, where the detail page is a
hub and the user can go straight to check answers at any point, here the page is reachable only
by posting the reason page, which path validation already enforces. Same reasoning as Add Route,
and the opposite conclusion to Edit Route — worth being explicit about, since the three journeys
now differ on this.

Cancel is a form field, not a handler. Both /cancel handler routes are gone, and with them
the {handler?} segments from the two page routes: the library validates the request URL against
the journey path, so a distinct path or query is an invalid step and redirects forever. The
buttons now POST to the same URL with name="Cancel" value="true".

Change links use the library's returnUrl in place of the fromCheckAnswers flag, so
DeleteRouteLinkGenerator drops qualificationId from its arguments — scoping route values come
from InstanceId.RouteValues — and CheckAnswers loads its reference data in
OnPageHandlerExecutionAsync rather than a separate OnGetAsync.

CheckRouteToProfessionalStatusExistsFilterFactory moves from per-page attributes into the
folder's Conventions
, matching Edit Route.

Guidance to review

I did not take #3691's IOrderedFilter/-200 change to
CheckRouteToProfessionalStatusExistsFilter.
That change exists so Edit Route's coordinator can
seed its state from the route before the journey filter runs. Delete Route's state is just the
reason, so GetStartingState() needs nothing from the request, and the filter running at its
default order is fine. Leaving the shared file alone keeps these two branches from touching it —
whichever lands second, that file merges cleanly.

EditRoute/PermissionsTests will conflict with #3691. It's a single file covering both
journeys' pages, and both branches change how the delete journey is seeded in it. The conflict is
confined to one switch expression; I've kept the change as small as it can be. Whichever merges
second takes the other's Edit Route arm and keeps its own Delete Route arm.

Test content. The two page test classes move onto a new DeleteRouteTestBase that seeds the
path via JourneyHelper; cancel tests POST Cancel=true instead of following a formaction; and
journey-deletion assertions read IJourneyStateStorage rather than JourneyInstance.Completed,
since the library has no notion of completing an instance. I added back link coverage for both
pages, which nothing asserted before.

Verified locally:

Suite Result
SupportUi.Tests 3880 passed, 3 skipped
SupportUi.Tests, RoutesToProfessionalStatus 597 passed, 2 skipped
SupportUi.EndToEndTests, RoutesToProfessionalStatus 26 passed

Two flakes worth naming, neither in Delete Route. One E2E run failed
EditRouteToProfessionalStatusTests.EditEachField_Cya_ShowsEditedContent on a training provider
name — it passes in isolation and on re-run, and looks like the usual committed-test-data
interaction. One full SupportUi.Tests run reported a single failure whose name I didn't capture
before two subsequent clean full runs.

No emitted events change, so docs/process-type-events.md is untouched.

Checklist

  • Attach to Trello card
  • Rebased master
  • Cleaned commit history
  • Tested by running locally

Continues the move off WebCommon.FormFlow onto GovUk.Questions.AspNetCore
(DFE issue #369).

Delete Route is a plain linear walk — Index redirects to Reason, which
advances to CheckAnswers — so it follows Add Route rather than the
hub-and-spoke path model Edit Route needed.

- DeleteRouteJourneyCoordinator owns the page caption, the person id and
  CancelAsync; DeleteRouteState is plain data.
- Index is [StartsJourney] and advances to Reason with SetAsFirstStep, so
  Reason is step 0 and the redirect page stays out of the path.
- The check answers completeness guard is dropped: the page is only
  reachable by posting the reason page, which path validation already
  enforces.
- Cancel becomes a form field rather than a handler — a distinct path
  would be an invalid step and redirect forever.
- Change links use the library's returnUrl in place of fromCheckAnswers,
  and the existence filter moves into the folder's Conventions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@gunndabad
gunndabad merged commit 8d091dd into main Aug 6, 2026
9 checks passed
@gunndabad
gunndabad deleted the claude/govuk-questions-delete-route-6c48f5 branch August 6, 2026 08:35
gunndabad added a commit that referenced this pull request Aug 6, 2026
### Context

Continues the move off `WebCommon.FormFlow` onto
`GovUk.Questions.AspNetCore` (DFE issue #369).
This one is Edit Route to professional status. Delete Route (#3698)
landed while this was in
review, so with this one merged no route journey is left on FormFlow —
only Edit Induction.

Edit Route is hub and spoke rather than a linear walk, so the path model
is the part worth reading
closely. It's a different answer to the same question #3685 dealt with,
for a different journey shape.

### Changes proposed in this pull request

**The detail page is a hub, and its questions are reachable from it.**

The detail page offers the questions that apply to the route, and each
one returns there. The library
only pushes a step on a post, so a change link — a GET straight to a
question — would be rejected as
an invalid step and bounced back, a redirect loop.

`EditRouteJourneyCoordinator` therefore overrides `StepIsValid` so the
hub's questions are reachable,
as `SignInJourneyCoordinator` does for `/oauth2/authorize`. It has to
override `GetCurrentStep` too:
`ValidateJourneyFilter` asks for the current step *before* validating
it, and the base implementation
only knows about steps in the path, so overriding validation alone
changes nothing. That was the
single biggest cause of test failures while building this.

Only the questions the route and its status actually ask are accepted —
a question that doesn't
apply isn't reachable at all. Because a question isn't a step in the
path it can't push the next one,
so navigation between questions is by plain redirect; only the detail
page advances the path, to
check answers.

**Check answers keeps its completeness guard**, unlike Add Route. The
hub lets the user go straight
there at any point, so the guard is what asks for the reason for the
change and for anything the
route needs that its answers don't cover. In Add Route the linear walk
made the same guard
unreachable and it was removed; here it's load bearing.

**Completing a route.** Moving a route to 'holds' asks when it was first
held and whether it carries
an induction exemption, buffered in `EditStatusState` until the last of
them is answered, so
abandoning half way leaves the route as it was. That sequence is now
owned by the coordinator rather
than spread across the status, holds from and induction exemption pages.

**`EditRoutePage` replaces the shared `AddRoutePage`**, with its own
requirement mapping, so this
journey no longer reaches into the Add Route coordinator. There's no
question for the route type
itself — you can't change it when editing — so the two enums were never
the same set.

**`EditRouteCommonPageModel` is gone**, with the shared behaviour on the
coordinator, and
`EditRouteState` is plain data. Pages validate with FluentValidation
through `ThrowIfInvalidAsync`,
and the date inputs carry their error message prefix on the tag helper.

### Guidance to review

Three things I'd particularly like a second opinion on:

1. **`CheckRouteToProfessionalStatusExistsFilterFactory` now implements
`IOrderedFilter` at -200**,
exactly as `CheckPersonExistsFilterFactory` already did. Without it the
filter ran *after* the
journey filter, so the coordinator couldn't seed its state from the
route. It's a shared file —
Delete Route applies the same factory from its `Conventions` — though
running an existence check
earlier should be harmless. #3698 deliberately left the file alone so it
would merge cleanly,
   which it did.

2. **The applicable pages are cached on the journey state**
(`AvailablePages`), recomputed when the
journey starts and whenever the status changes. Step validation runs
synchronously in the journey
filter and can't await the route's reference data, and I didn't want to
introduce sync-over-async
there. The alternative is an applicability check inside each page's
handler, where async is
   available, at the cost of a line per page.

3. **`_RouteDetail.cshtml` stays a partial here**, unlike Add Route
where it was inlined, because
both the detail page and check answers render it. Detail's "Continue"
changed from a link to a
   submit button so that it can advance the path.

Two changes to test content are worth reading rather than skimming. The
old `EditRouteStateBuilder`
is gone in favour of object initialisers plus an `EditRouteTestBase`;
while converting it, note that
`WithStatus` had been setting `CurrentStatus` as well, which matters
because that pair is what
decides whether the journey is completing a route. And the theory rows
that exercised a question the
route doesn't ask now assert that the page isn't reachable, rather than
asserting how its heading
reads.

A few tests picked a route and status at random without requiring that
the page under test applies,
which flakes between 200 and 302 now that paths are validated. I
constrained the ones that failed;
there may be others that currently pass by luck.

**Rebased over #3698.** The one conflict was
`EditRoute/PermissionsTests`, exactly where #3698 said
it would be and resolved the way it proposed: this branch's Edit Route
arm, `main`'s Delete Route
arm. The class now derives from `EditRouteTestBase` for the former and
keeps #3698's private
`CreateDeleteRouteJourneyInstanceAsync` for the latter — it can only
inherit one of the two test
bases. Both journeys are on the library now, so the comment about the
other one still being on
FormFlow is gone.

Verified locally:

| Suite | Result |
| --- | --- |
| SupportUi.Tests | 3865 passed, 3 skipped |
| SupportUi.EndToEndTests | 142 passed |
| SupportUi.EndToEndTests, `RoutesToProfessionalStatus` | 26 passed |

One of three full SupportUi.Tests runs after the rebase reported a
single failure; the other two
were clean and I didn't capture its name, so I can't say more than that
it wasn't reproducible.

No emitted events change, so `docs/process-type-events.md` is untouched.

### Checklist

-   [ ] Attach to Trello card
-   [x] Rebased master
-   [x] Cleaned commit history
-   [x] Tested by running locally

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
gunndabad added a commit that referenced this pull request Aug 6, 2026
### Context

Continues the move off `WebCommon.FormFlow` onto
`GovUk.Questions.AspNetCore` (DFE issue #369).
This is Edit Induction — the last SupportUi *journey* on FormFlow.

It doesn't let us delete FormFlow yet. After this lands, what's left is
Edit Route (#3691) and
Delete Route (#3698), both migrated but still open; the shared plumbing
(`GlobalUsings`,
`LinkGeneratorExtensions.GetRequiredPathByPage`,
`HttpContextCurrentUserIdProvider`, `AddFormFlow`,
`TestBase`/`HostFixture`/`UiTestCommon`, and
`WebCommon.Tests/FormFlow`); and three files under
`AuthorizeAccess/Infrastructure/FormFlow` that are **already dead** —
nothing references them. The
deletion is a follow-up branch once the two route PRs merge.

### Changes proposed in this pull request

**The four entry points keep their URLs.** The induction page links
straight into Status,
Exemption reasons, Start date and Completed date.
`ValidateJourneyFilter` only consults
`StartsJourney` when there's no instance, and
`TryCreateNewInstanceAsync` seeds the new path with
the requested URL as step 0 — so each of those pages can be both a way
in and an ordinary step
just by carrying `[StartsJourney]`. Nothing about the URLs changes, and
**the question the user
came in by is simply the path's first step**.

That makes a lot of the journey's bookkeeping redundant:

- **`JourneyStartPage`** and the **`Initialized`/`EnsureInitialized`**
pair — the start page is
read from the path and the seeding moves into `GetStartingStateAsync`.
Check answers decides
which answers offer a Change link from the path rather than a state
field.
- **`JourneyFromCheckAnswersPage`** — replaced by the library's
`returnUrl`. It couldn't have
survived as a query parameter anyway: anything other than
`_jid`/`returnUrl` is part of the
  StepId, so it would fail path validation.
- **`CommonJourneyPage`** and **`InductionJourneyPage`** — the shared
behaviour is on
  `EditInductionJourneyCoordinator`; pages are plain `PageModel`s again.
- **The four redirect-to-start-page guards.** Path validation covers a
status that stops asking a
question. The guards remain only for the case path validation can't see:
a request naming a page
directly as the journey's first step. One of those redirected to the
page it was on — a latent
  loop — and now goes to the induction page.

**`OnInvalidStep` returns the user to the question they came in by.**
The default is the last step,
which here is check answers, and landing there with a question
unanswered just bounces them out
again.

**Going back to check answers doesn't push a step.** `PushStep`
truncates everything after the
current step, so advancing from an early question straight to check
answers dropped the questions
its other Change links point at. When the target is already in the path
a plain redirect leaves the
path intact. This is the same trap #3645 hit on Edit Details.

**The status question is the exception to `returnUrl`.** Every other
question sends the user back
to check answers when they came from there; the status walks them
forward through the questions
that follow it even when they re-pick the same status, because those
answers depend on it. That's
today's behaviour, pinned by
`EditInductionStatus_CYA_ChangeStatus_ContinueThroughJourneyToCYA`.

### Guidance to review

**One back link changes.** Enter a start date from check answers that
falls after the completed
date, and you're asked for the completed date again — its Back now goes
to check answers rather
than back to the start date.
`JourneyFromCheckAnswersPage.CheckAnswersToStartDate` encoded "back
here, continue there", and one `returnUrl` can't express both. Happy to
reinstate it with an
explicit flag on the state if you'd rather keep it exactly as it was.

**Page captions are now uniform** — `@Model.PageCaption`, "Edit
induction details - {name}". Three
pages said "Enter induction details" and `ExemptionReasons.cshtml` said
"Enter induction
**deetails**". Fixing the typo meant touching the copy, so I normalised
all six rather than leave
them inconsistent.

**Tests.** They move onto `EditInductionTestBase`, which seeds the path
the real journey would have
built — which questions are in it depends on the start page and the
status, so a few theory rows
that described journeys that can't exist (a journey "starting at the
start date" for a status that
never asks for one) had their start page corrected. Per house style
`EditInductionStateBuilder` and
`CommonPageTests` are gone: state is built with object initialisers, the
cross-page tests are folded
into the page classes, and the access checks are in their own
`PermissionsTests`.

Three tests are new and worth reading:
`Post_FromCheckAnswers_LeavesTheOtherQuestionsReachable`
(the path-truncation regression, which only the browser tests caught
first time round),

`Post_Status_FromCheckAnswers_WhenNewStatusChangesTheQuestionsAsked_WalksForwardToTheNextQuestion`,
and
`Post_StartDate_FromCheckAnswers_AfterCompletedDate_AsksForCompletedDateWithBackLinkToStartDate`.

Verified locally:

| Suite | Result |
| --- | --- |
| SupportUi.Tests | 3876 passed, 3 skipped |
| SupportUi.Tests, `EditInduction` | 396 passed |
| SupportUi.EndToEndTests, `Induction` | 13 passed |

Note for anyone else running these locally: intermittent
`ChangeHistoryTests` /
`ChangeLogProfessionalStatusEventsTests` failures during this work
turned out to be the usual
committed-data accumulation across repeated runs against one container,
not this change —
`just remove-tests-schema-cache` clears it.

No emitted events change, so `docs/process-type-events.md` is untouched.

### Checklist

-   [ ] Attach to Trello card
-   [x] Rebased master
-   [x] Cleaned commit history
-   [x] Tested by running locally

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants