Appearance
Spec: Triage Form Booking Link Routing (VA / Field / Complex / Manual Review)
Status: Implemented Date: 2026-07-23 Branch context: UserStory777DesktopAppointments
2026-09-23 (UserStory1750): routing moved out of FormsPortal. It now runs in WebAPI as the first step of the queued
TriageFinalizeSubmissiontask (WebAPI/Services/TriageBookingRoutingService.cs), after syncing the job's Prime allocations (IPrimeAllocationService.GetAllocationsByJob), and is guarded byethFormTriage.BookingRoutedAt(migration 0019), and the rules now follow the V9 process diagram (§2).2026-10-21 (UserStory1947): routing runs inline on submit again, so the customer is taken straight to their booking page. The service moved to
CommonPrimeAPI/Services/TriageBookingRoutingService.csand is shared by FormsPortal and the finalize task (§8).
1. Overview
Extends the original Virtual Assessment (VA) booking trigger (see VirtualAssessmentBooking-Spec.md) so the FormsPortal triage form's answers route every submission to one of five outcomes (TriageBookingOutcome, stored in ethFormTriage.BookingOutcome), automatically, on submit:
- Desktop VA booking link (30 min desktop estimator video call)
- Field Quotation booking link
- Complex Quotation booking link
- Case manager review — nothing sent to the insured; the case manager alert says a specialist report and Desktop VA are required
- Manual booking review — no automatic booking link; the case manager alert lists the claim's affected units/tenants instead
Previously, only the VA path was automatic; Field/Complex booking links could only be created manually via AdminPortal's "Create a new booking link?" dialog (BookingCreate.razor). This spec makes all three self-service.
2. Routing logic
Per the "Triage Form High Level V9" process diagram. Evaluated in TriageBookingRoutingService.DecideAsync (CommonPrimeAPI); first match wins:
1. Multi-unit claim (IsMultiDwellingClaim) -> Manual booking review
2. Commercial, 2-3 rooms / 4+ rooms / extensive damage -> Complex Quotation
Commercial, 1 room (or no room count, e.g. fence only) -> Field Quotation
3. Domestic, fence only -> Desktop VA
4. Domestic, 4+ rooms / extensive damage -> Complex Quotation
5. Domestic, wet area or 2-3 rooms -> Field Quotation
6. Domestic, 1 room, not a wet area:
Specialist Report allocation on file -> Case manager review
otherwise -> Desktop VAChoices the diagram leaves open:
- Commercial fence only has no room count, so it is treated as 1 room (Field Quotation).
- Domestic wet area with 4+ rooms meets both the Field and the Complex condition; rule 4 runs first, so the larger job (Complex) wins.
- Domestic fence only is a Desktop VA whether or not a Specialist Report allocation is on file — the diagram's specialist report condition applies only to the 1-room branch.
The Specialist Report check (spPrimeJobHasSpecialistReportAllocation: type Specialist, Roof, Plumbing, Electrical or Flooring Report, status not "Cancelled") reads primeAllocations, so the job's allocations are synced from Prime first. Only rule 6 syncs, since no other outcome depends on it. On submit the sync gets one attempt, cut off after 5 seconds. In the finalize task a failed sync is retried in place after 10 and then 30 seconds. If it still fails, rule 6 throws rather than guess, and the finalize task is retried.
Each booking link also tags the Prime job: "Desktop Video Assessment", "Field Quotation" or "Complex Quotation". The tag is added by the finalize task (TriageFinalizeQueueService.TagBookingTypeAsync) from the recorded outcome, not when the link is created. The resulting job status is set by TriageStatusRouting.StatusFor: Estimator Appointment Required when a link was sent, Insured Triage Complete - awaiting review for case manager review, Multi Unit Booking Required for a multi-unit claim. The status is only changed while the job (read live from Prime) is at Job Registered, Initial Contact Made - Awaiting Further Info, Desktop Quote Required, Estimator Appointment Required or Insured Triage Complete - Awaiting Review — the same list that guards the move to Estimator Booked (BookingJobStatus). Any other status is left alone and the skip is logged; the rest of the finalisation carries on.
Compare via the named constants in TriageBookingRouting (CommonDataAccess\Models\Forms\TriageFormModel.cs), not inline strings.
2.1 Out of scope: the Prime "Triage" site form
The diagram also shows the claims team completing Prime's own "Triage" site form (reference triage) moving the job to Estimator Appointment Required. That is drawn for completeness and is implemented in Prime, not here — no bookings or other actions are taken by this system. The change-custom-form-status webhook only syncs the job's site forms and requests a fresh AI job summary (PrimeWebhook-Sync-Spec.md). Its questions differ from the customer triage form, so none of the routing above applies.
3. New triage question: multiple dwellings / strata
Two new dedicated MudStepper steps, inserted right after "Emergency help" and before "The damage":
- "Multiple dwellings" (
TriageMultiDwelling.razor) — just the Yes/No question:"Does this claim involve damage to multiple dwellings or units within a strata complex?" (Yes/No)
- "Tenants" (
TriageTenants.razor) — a table of affected units, one row per unit: Unit number, First name, Last name, Phone number, Email address (TriageTenantContact/Model.Tenants, add/remove rows, pre-seeded with one empty row). Each field is required; phone and email are format-validated both inline (as the customer types) and again at the step-completion gate, via a sharedTriageContactValidationhelper (digits/spaces phone mask + 8–15 digit check; basicx@y.zemail shape check).
Stepper flow control (Triage.razor's OnStepperInteraction, steps are fixed-index 0–10: Welcome, Emergency help, Multiple dwellings, Tenants, The damage, Damage details, Photos, Authorisation, Support for you, Contact, Review & sign):
- Answering No on "Multiple dwellings" skips the "Tenants" step entirely — the handler cancels the default single-step advance and jumps
activeIndexstraight to "The damage" (index 4). - Answering Yes proceeds normally into "Tenants". Once its unit rows validate, the claim is complete — the handler jumps
activeIndexstraight to "Review & sign" (index 10), skipping "The damage" through "Contact" entirely, since none of those field-specific questions apply to a multi-dwelling/strata claim. - The programmatic
activeIndexjump reuses the same mechanism already used by the Review & Sign step's per-section "Edit" links (GoToStep) — just setting the boundActiveIndex, cancelling the framework's own step transition viaargs.Cancel = true. - The Review & Sign step's "Edit" links and damage summary already account for both paths (edit jumps to "Tenants" instead of "The damage" when
IsMultiDwelling == "Yes"; the Photos/Authorisation/Support/Contact edit rows are hidden entirely for that path since they were never asked).
4. Room count option changes
RoomCount radio options in TriageDamage.razor changed from "1 room" / "2 or more rooms" / "Extensive damage throughout" to:
"1 room"(unchanged)"2 or 3 rooms"(was"2 or more rooms")"4 or more rooms"(new)"Extensive damage throughout"(unchanged, kept as a distinct option — routes the same as"4 or more rooms", i.e. Complex Quotation)
5. Data model
Additive-only change to the existing schema version — no version bump per the model's own convention (TriageFormDataV20260704.Version stays "20260704"; older rows without these fields deserialise with empty defaults, same pattern as the earlier SiteConfirmed addition):
TriageFormDataV20260704.IsMultiDwelling(string, "Yes"/"No")TriageFormDataV20260704.Tenants(List<TriageTenantContact>)- New
TriageTenantContactclass (CommonDataAccess\Models\Forms\TriageFormModel.cs):UnitNumber,FirstName,LastName,PhoneNumber,EmailAddress
TriageFormDataFormatter.ToLabelValueLines (used by the AI Job Summary prompt) includes the multi-dwelling answer and per-unit tenant lines when present.
No SQL changes were required — ethFormTriage.FormData is schema-free JSON.
6. Field Quotation / Complex Quotation auto-creation
Reuses IBookingData.Create(jobID, appointmentTypeID, SkipNotifyContact: false, ...) — the same call BookingCreate.razor's manual dialog makes — from TriageBookingRoutingService.CreateFieldBookingAsync. SkipNotifyContact is always false: there's no admin in this automatic path to decide notify/don't-notify, so the customer is always sent the booking link by SMS, gated by the existing BookingSMSEnabled site setting (unchanged behaviour from the manual flow).
Prime appointment type IDs (same values as BookingCreate.razor):
- Field Quotation:
43736768-F316-4492-AC9E-196EB294D5BB - Complex Quotation:
08162014-77DC-4D18-98B8-BE01F06DDC5D
7. Manual booking review notification
A multi-dwelling/strata claim gets no separate notification. The single case manager alert (TriageFinalizeQueueService.AlertCaseManagerAsync — one email per submission, content varying with the form) says no booking link was sent and lists the submitted units/tenants (unit, name, phone, email). The tenants are also created as Prime contacts on the job (TriageContactSyncService).
2026-10-07: this used to be a separate "Manual Booking Review Required" email to the addresses in the
ManualBookingReviewEmailssite option (AdminPortal → Settings → Bookings). The V9 diagram has no such email, so it and the setting were removed and the tenants table moved into the case manager alert. The old site-option row is no longer read.
7.1 Everything after that email is manual, on purpose
The multi-dwelling path deliberately ends here. Once the case manager has the email, the rest — contacting the units, deciding what gets booked, and moving the job on to Estimator Booked — is done by hand in Prime.
This is a decision, not a gap. A strata claim's unit-by-unit coordination isn't something the routing rules in §2 can make a sensible call on, so there is deliberately no automatic booking link, no self-service page, and no follow-up automation on this branch. Reviewers comparing the code against the process diagram tend to read the manual legs as missing work — they aren't.
7.2 The "Multi Unit Booking Required" status
TriageFinalizeQueueService patches a multi-unit claim's job to a dedicated "Multi Unit Booking Required" status (mirroring "Job Registered") instead of "Insured Triage Complete - awaiting Review". The status now exists in Prime; its GUID (41833334-0135-463E-BBFB-547F8B0410A9) is held in TriageStatusRouting.MultiUnitBookingRequiredStatusId and is patched unconditionally alongside the single-unit status.
8. Error handling
Routing first runs inline on submit (Triage.razor's RouteBookingAsync), after the submission has been saved and stamped, while the customer sees "Preparing your booking…". It is only the decision and the booking INSERT. Only rule 6's Prime allocation sync can be slow, and it is cut off after 5 seconds. On success FormsPortal stamps BookingRoutedAt and opens the booking page (/booking/va/{code} or /booking/estimator/{code}; the /staff/ routes when staff submitted). The booking-link SMS still goes out, in case the customer closes the page without booking. Any failure is logged, and the customer gets the thank-you page.
The queued TriageFinalizeSubmission task is queued after that stamp, so it skips routing when FormsPortal did it. Otherwise it routes the booking itself, so a failure on submit never loses the booking.
If the booking can't be created in the finalize task, BookingRoutedAt stays unset. The rest of the finalisation (PDF, attachments, status, contacts, alerts) still runs, and each of those steps is stamped. The task then throws, so it is retried for the booking alone. That retry is due in 15 minutes rather than the usual 36 hours (spEthTaskIncrementAttempt @RetryMinutes), because the customer is waiting on the link. The n8n schedule picks it up within 3 minutes after that. There is no attempt cap, so a booking that can never be routed keeps retrying every 15 minutes. Filter tags and the "process now" nudges are best-effort: a failure there is logged, not thrown, because a retry would create a second booking.
Each task is claimed (spEthTaskClaimNext, ethTask.ClaimedAt) before it is processed, so overlapping runs never work the same submission. Runs overlap because each one takes minutes (every photo is a slow Prime upload) and the n8n schedule, the submit webhook and the n8n lock expiring each start a run. Without the claim, each run uploaded the PDF summary again (AB#1849). A claim older than 60 minutes is treated as a dead run and handed out again.

