Appearance
Spec: Virtual Assessment (VA) Booking
Status: Implemented (UserStory777DesktopAppointments, with later changes noted inline) Date: 2026-07-09 (original spec) Updated 2026-09-30: merged the UserStory777 branch summaries.
This document describes VA booking as built, verified against the code on main and the live SQL definitions. Where the original 2026-07-09 plan differed from what shipped, the shipped behaviour is described and the change is noted. Field-estimator booking, OptimoRoute and the shared booking layer are covered in OptimoRoute-Appointments-Bookings-Logic.md (§4.2 and §6.2 cover VA). The triage routing rules that decide whether a job gets a VA link are in TriageBookingRouting-Spec.md.
1. Overview
When a triage submission indicates a low-complexity domestic claim, the insured is sent a booking link (SMS + email) to self-schedule a 30-minute Virtual Assessment (VA) — a video call with a desktop estimator instead of an in-person visit. The customer picks a timed slot; availability is computed from enabled desktop estimators' Prime calendars and their own working hours. When the customer confirms, a Prime appointment is created asynchronously and both the customer and the estimator are notified.
Triage submitted (FormsPortal)
│ inline on submit (or the queued TriageFinalizeSubmission task if that fails)
▼
TriageBookingRoutingService decides DesktopVA
│ spEthVABookingCreate → ethVABooking row (sharing code)
│ → customer taken to /booking/va/{code}
│ + VAProcessEvents webhook nudge (best-effort)
│ + Prime filter tag "Desktop Video Assessment" (finalize task)
▼
OnCreate (VABookingService) ──► Prime note with link, customer SMS + email invite
│
▼
Customer opens /booking/va/{code} (FormsPortal, polls every 5–25 s)
│ spEthVABookingGetAvailability → 30-min slots, estimator pre-assigned
▼
Customer confirms slot + callback phone + notes
│ spEthVABookingSetDate (OnBook pending) + VAProcessEvents webhook
▼
OnBook (VABookingService) ──► Prime appointment (1 h), mirror re-synced,
│ job status → Estimator Booked (guarded),
│ Prime note, customer SMS + email,
│ estimator email (+ SMS if mobile set)
▼
Customer may Change (re-runs OnBook) or Cancel (OnCancel → Prime appointment
cancelled, note, customer SMS + email)The design deliberately decouples every customer-facing step from the Prime API with an event-outbox pattern, because the Prime API is fragile. No Prime API call happens inline in a customer request: portals write a row and fire a webhook, and WebAPI drains the queue.
2. Parameters (as built)
| Parameter | Value |
|---|---|
| Trigger | Decided by TriageBookingRoutingService (UserStory1750; inline on submit since UserStory1947). Outcome DesktopVA for: Domestic + fence-only damage; or Domestic + RoomCount == "1 room" + not a wet area + no Specialist Report allocation on the job. Constants in TriageBookingRouting (CommonDataAccess/Models/Forms/TriageFormModel.cs). Full rule table in TriageBookingRouting-Spec.md. Staff can also create a VA link by hand (AdminPortal BookingCreate, type "Desktop VA") |
| Customer slot length | 30 minutes (ethVABooking.FromDate/ToDate) |
| Prime appointment length | 1 hour from the slot start — extra prep/wrap-up time for the estimator. The booking row keeps the real 30 min |
| Prime appointment type | primeAppointmentType.id = 6fca7477-f5bc-4ebb-bd28-5dc365685541 (originally labelled "Virtual Assessment", renamed in Prime to "Desktop Video Assessment - Ethos led"). VABookingService and the availability proc both use the ID, never the label, so a rename can't break booking |
| Buffer | None (changed from the original 30 min each side). A straight overlap check against all non-cancelled Prime appointments for the estimator. The 1-hour Prime appointment provides the estimator's slack |
| Customer slot grid | Every half hour, 9:00 AM – 4:30 PM customer-local (15 slots, last 4:00–4:30 PM) |
| Estimator hours | Each slot must also fall inside that estimator's own WorkingHoursStart/WorkingHoursEnd (Melbourne wall-clock, single daily window, default 09:00–16:30) and must not span Melbourne midnight |
| Lead time | From the next customer-local day, plus a 1-hour minimum-notice floor |
| Days offered | The first 10 business days in the next 30 calendar days. Weekends (customer-local) and VIC public holidays (ethCalendar, UserStory1619) are excluded. The holiday is tested again on the slot's Melbourne date |
| Timezones | All storage and maths in UTC. Customer timezone (IANA, e.g. Australia/Perth) resolved by ITimeZoneHelper.GetTimeZoneId(state, suburb, postcode) at create and stored on the row. Local time only at display edges |
| Prime API time quirk | POST appointments expects Melbourne local time; GETs and primeAppointments are UTC. PrimeAppointmentService.CreateAppointment converts (callers pass UTC) |
| Customer notifications | SMS and email at invite (OnCreate), confirmation (OnBook) and cancellation (OnCancel). Email from the Prime contact; if none, SMS only — never fails the event |
| Messaging switches | BookingSMSEnabled = '0' silences customer SMS and email. BookingMobileOverride / BookingEmailOverride redirect customer messages to a test number/address (the email override wins even when the contact has no email). Shared with field booking — there is no VA-only switch |
| Estimator notification | Email on OnBook always (when an email is on file); SMS as well if ethDesktopEstimator.NotificationMobile is set. Not gated by BookingSMSEnabled |
| Video call link | Out of scope. The customer is told "your estimator will contact you on the phone number you provided us with"; the estimator email says they must arrange the call |
| Storage | Separate ethVABooking table — ethBooking and the field flow are untouched |
| Estimator visibility | The customer never sees the estimator's name |
| Sharing code | 16-char code generated in spEthVABookingCreate. VA codes do not expire (no expiry column; AdminPortal treats them as never-expiring) |
3. Where it lives
| Area | Files |
|---|---|
| Customer page | FormsPortal/Components/Pages/VirtualAssessment.razor (/booking/va, /booking/va/{sharingcodeParam}, NoHeaderLayout) |
| Page states | FormsPortal/Components/Forms/VABooking/ — VAHeader, VADetailsCard, VAUnbooked, VABooked, VACancelled, VANotFound |
| Dialogs | FormsPortal/Components/Dialogs/ — VATime (confirm), VACancel (reason), VAShowMoreDates, VAAboutCall |
| Trigger | CommonPrimeAPI/Services/TriageBookingRoutingService.cs, called inline from FormsPortal Triage.razor on submit, or from WebAPI TriageFinalizeQueueService when that didn't route it |
| Event processor | WebAPI/Services/VABookingService.cs, WebAPI/Controllers/VABookingController.cs |
| Data access | CommonDataAccess/Data/Bookings/VABookingData.cs + Interfaces/IVABookingData.cs; Data/Prime/DesktopEstimatorData.cs + Interfaces/IDesktopEstimatorData.cs; IPrimeData.GetUserMobile |
| Models | Models/Booking/VABookingModel, VABookingCreateResultModel, VABookingEventModel (shared row shape for the three GetOn* procs), VABookingListModel, VAAvailabilityModel; Models/Prime/DesktopEstimatorModel, DesktopEstimatorScheduledAppointmentModel |
| Staff UI | AdminPortal DesktopEstimators.razor (+ Forms/DesktopEstimatorInsert, DesktopEstimatorEdit), Bookings.razor, Dialogs/BookingCreate.razor, BookingAvailability.razor, BookingAvailabilityCalendar.razor, Dialogs/DesktopEstimatorScheduledAppointments.razor |
| Webhook | CommonWebhooks IWebhookHandler.VAProcessEvents() → WebhookSettings:VAProcessEvents |
| SQL in repo | DbMigrations/Scripts/Programmable/Procedures/spEthVABookingGetAvailability.sql, spEthVABookingGetDesktopEstimatorAvailabilityDetailsAll.sql. The other VA objects predate DbMigrations and are in the baseline (live definitions in sql-ontology) |
DI: IVABookingData and IDesktopEstimatorData are registered in FormsPortal, AdminPortal and WebAPI; VABookingService is scoped in WebAPI.
4. Database objects
dbo.ethDesktopEstimator
One row per Prime user in the VA rotation.
| Column | Type | Notes |
|---|---|---|
| PrimeUserId | uniqueidentifier PK | primeUsers.id by convention |
| Enabled | bit | in/out of the rotation |
| WorkingHoursStart / WorkingHoursEnd | time | single daily window (not per weekday), Melbourne wall-clock; default 09:00–16:30 |
| NotificationMobile | nvarchar NULL | if set, the estimator also gets an SMS on OnBook |
| CreatedAt / ModifiedAt / ModifiedBy | audit |
Only Enabled = 1 estimators whose primeUsers.status is active (UPPER(status) = 'ACTIVE') are offered.
Procs: spEthDesktopEstimatorList (joins primeUsers for name/email/status), spEthDesktopEstimatorUpsert, spEthVABookingGetEstimatorAppointmentsForDate (an estimator's non-cancelled Prime appointments of any type for a Melbourne date, or a range with the optional @ToDate — UserStory1007), spPrimeGetUserMobile (primeUsers.contactId → primeContacts.mobileNumber, used to default the notification mobile).
The table was first built as ethDesktopSupervisor and renamed to match the codebase's "Estimator" terminology. It is unrelated to dbo.ethSupervisor (UserStory1040), which is the list of WIP supervisors.
dbo.ethVABooking
| Column | Type | Notes |
|---|---|---|
| ID | int identity PK | |
| SharingCode | nvarchar | generated in spEthVABookingCreate |
| PrimeJobID | uniqueidentifier | |
| PrimeJobNumber / PrimeJobURL | nvarchar NULL | copied from primeJobs at create |
| PrimeContactID | uniqueidentifier | the insured |
| TriageFormID | int NULL | ethFormTriage.ID; NULL for staff-created links |
| TimeZoneId | nvarchar | customer IANA timezone |
| DesktopEstimatorUserId | uniqueidentifier NULL | assigned at confirm, from the availability row |
| FromDate / ToDate | datetime NULL | picked 30-min slot, UTC |
| OffsetMins | int NULL | customer UTC offset at the slot (JS sign convention) |
| Notes | nvarchar NULL | customer notes from the confirm dialog |
| CallbackPhone | nvarchar NULL | the number the estimator should call, confirmed at booking |
| PrimeAppointmentID | uniqueidentifier NULL | set once the Prime appointment exists |
| PrimeJobStatusUpdatedAt | datetime2 NULL | guards the one-time "Estimator Booked" patch |
| CancelReasonCode / CancelledAt | 1 none of the times suit, 2 no longer needed, 3 not the right person, 4 other | |
OnCreate/OnBook/OnCancel ProcessedAt | datetime2 NULL | event-outbox flags; NULL = pending |
OnCreate/OnBook/OnCancel Attempts | int | failure counter |
OnCreate/OnBook/OnCancel Failed | bit | set with ProcessedAt at the 5th failure, so the row stops retrying |
| InviteViewedAtUtc / SlotPickedAtUtc | datetime2 NULL | customer interaction timestamps (not stamped for staff) |
| FirstBookedAtUtc / FirstBookedBy | datetime2 / nvarchar(400) NULL | first booking only; FirstBookedBy = staff UPN, NULL = the insured |
| CreatedAt / ModifiedAt | datetime2 |
There is no CreatedBy/CreatedSource on VA bookings (field bookings have them). Staff book on the customer's behalf at /booking/va/staff/{code} (EntraID sign-in), which AdminPortal's booking link dialog opens - see TriageReporting-Spec.md.
Stored procedures (VA family)
| Proc | Purpose |
|---|---|
spEthVABookingCreate | Insert row + generate sharing code; returns ID and code |
spEthVABookingGetByCode | Load the booking for the customer page |
spEthVABookingGetAvailability(@VABookingID) | The availability engine, §5 |
spEthVABookingSetDate | Slot (UTC), offset, estimator, notes, callback phone; clears OnBookProcessedAt (OnBook pending) |
spEthVABookingCancel | Sets CancelReasonCode/CancelledAt (slot kept for the record); OnCancel pending |
spEthVABookingClear | Resets a slot to unbooked; OnCancel pending if it had a Prime appointment. No caller today |
spEthVABookingSetAppointmentID | Stores the Prime appointment ID |
spEthVABookingSetInteractionTimestamp | InviteViewed / SlotPicked |
spEthVABookingGetOnCreate/OnBook/OnCancel | Pending rows for each stage (excluding Failed). GetOnCancel also requires CancelledAt IS NOT NULL - a new row has OnCancelProcessedAt NULL, so without that every new booking got a cancellation message. GetOnBook also returns job address, contact, estimator name/email/notification mobile and the raw triage FormData/SchemaVersion |
spEthVABookingSetOnCreate/OnBook/OnCancelProcessedAt(@Success) | Success stamps ProcessedAt; failure increments Attempts and at 5 stamps ProcessedAt + Failed = 1 |
spEthVABookingSetPrimeJobStatusUpdatedAt | Stamps the status-patch guard |
spEthVABookingSMSOnCreate/OnBook/OnCancel | Queue customer SMS (spMakesafeQueueSMSMessage) to the Prime contact's mobile; honour BookingSMSEnabled and BookingMobileOverride |
spEthVABookingSMSOnBookEstimator | Queue the estimator SMS to NotificationMobile (Melbourne time). Ignores BookingSMSEnabled, but does honour BookingMobileOverride |
spEthVABookingGetList / spEthVABookingGetCounts | AdminPortal Bookings grid and tab counts |
spEthVABookingGetDesktopEstimatorAvailabilityDetailsAll(@PastDays, @FutureDays) | Desktop half of the AdminPortal Booking Availability pivot (UserStory1288/1584); excludes weekends and VIC holidays (UserStory1619) |
5. Availability algorithm (spEthVABookingGetAvailability)
Returns rows (Date, FromTimeUtc, ToTimeUtc, PrimeUserId), ordered by time. Returns nothing for a cancelled booking or one with no TimeZoneId.
Timezone mapping.
TimeZoneIdis IANA (what .NET wants) but SQL Server'sAT TIME ZONEneeds Windows names, so the proc mapsAustralia/*zones to their Windows equivalents at the top. Estimator maths usesAUS Eastern Standard Time(Melbourne, DST-aware).Busy data, fetched once. For enabled, active desktop estimators, over a window of now −2 to +16 days, four temp tables are filled up front:
#EstimatorBusy— non-cancelledprimeAppointments(any type) byprimaryUserId;#EstimatorPendingVA— otherethVABookingrows booked to the estimator withPrimeAppointmentID IS NULLand not cancelled (race guard for picked-but-not-yet-synced bookings);#EstimatorFieldBusyDay— Melbourne dates on which the estimator has a non-cancelled fieldethBooking(primeUserID) or anoptimoRouteOrderscheduled to them (ScheduledEstimatorID/optimoRouteDay). The dual-role case: someone who is both a field and a desktop estimator is out for the whole day;#EstimatorVACountByDay— non-cancelled VA appointments per estimator per Melbourne day, for load balancing.
The first version ran correlated subqueries per slot per estimator (~150+ scans per page load, with no index on
primaryUserId); the temp tables replaced that with the same result.Days. The next 30 customer-local days from tomorrow, filtered to weekdays that are not VIC public holidays,
TOP (10).Slots. Customer-local 09:00 + 30 min × 0..14, converted to UTC per day (DST-safe), then to Melbourne wall-clock.
Pick an estimator per slot (
OUTER APPLY … TOP (1)), requiring:- slot inside that estimator's
WorkingHoursStart/Endand not crossing Melbourne midnight; - no overlap with
#EstimatorBusyor#EstimatorPendingVA(no buffer); - no row in
#EstimatorFieldBusyDayfor the slot's Melbourne date; - the slot's Melbourne date is not a VIC public holiday;
ordered by fewest VA appointments that day, then
PrimeUserId.- slot inside that estimator's
Keep slots with an estimator whose start is at least 60 minutes from now.
Because primeAppointments is a synced mirror, OnBook calls GetandSaveAppointment for the new appointment and then GetAndSaveAppointmentsByPrimaryUserId(futureOnly: true) for the estimator, so the slot (and anything booked directly in Prime) shows up straight away. The pending-VA clause covers the gap between pick and sync. A simultaneous pick of the same slot by two customers is accepted as a v1 risk.
6. Components (as built)
Trigger — TriageBookingRoutingService (CommonPrimeAPI)
Runs inline in FormsPortal Triage.razor after MarkSharingCodeUsed (UserStory1947), so the customer is taken straight to /booking/va/{code}. It decides the outcome (syncing the job's Prime allocations only for the one-room case, cut off after 5 seconds) and for DesktopVA:
- resolves the timezone from the job summary (State/Suburb/Postcode);
IVABookingData.Create(jobId, formModel.PrimeContactID ?? jobSummary.ContactId, tz, triageFormId);- FormsPortal stamps
BookingRoutedAt, then in the background firesVAProcessEvents()(best-effort; the n8n schedule covers it) so the SMS invite goes out even if the customer closes the page without booking.
The Prime filter tag Desktop Video Assessment is added by the queued TriageFinalizeSubmission task (best-effort). If routing fails or times out on submit, the customer gets the thank-you page and that task routes the booking instead, with the full allocation-sync retries (UserStory1750, migration 0019). A create failure throws, so ethFormTriage.BookingRoutedAt stays unset and the finalize task retries. The recorded BookingOutcome drives the job status and the case-manager alert.
Staff-created links — AdminPortal BookingCreate
Type "Desktop VA" calls IVABookingData.Create(jobId, contactId, tz) (no triage form). The link is always sent — there is no "notify the insured" option for VA, and the dialog says so. It does not fire the webhook; the 3-minute schedule picks it up.
Customer page — FormsPortal /booking/va/{code}
- Anonymous. The page loads the row directly with
IVABookingData.GetByCode— the VA code is its own credential, not anethFormSharingCode. An unknown code showsVANotFound. - State:
CancelledAtset →VACancelled;FromDatenull or "change booking" →VAUnbooked; otherwiseVABooked. - Records
InviteViewedthe first time an unbooked page is opened. - While unbooked, polls availability every 5–25 s (jittered).
- Timezone warning when the browser offset (
BrowserTimezoneService) differs from the customer timezone. VAUnbooked: slots grouped by customer-local day, two days at a time; the first is tagged SOONEST. "Show more days" goes throughVAShowMoreDates, which nudges the customer to keep the soonest days. "How does this work?" opensVAAboutCall(video call and camera permissions). With no slots, the page asks the customer to phone 1300 238 467.- Picking a slot records
SlotPickedand opensVATime: a required callback phone (pre-filled fromCallbackPhoneor the contact mobile; must be a valid AU number — 10 digits starting 0, or +61 and 9 digits) and optional notes. Confirm →SetDate(ID, fromUtc, toUtc, offsetMins, estimatorId, notes, callbackPhone)→VAProcessEvents(). VABookedoffers Change booking (back to the slot picker; confirming callsSetDateagain) and Cancel booking (VACancelreason →Cancel(ID, reasonCode)→VAProcessEvents()). A cancelled booking cannot be rebooked online; the page gives the phone number.- Styling reuses the
est-*classes inbooking.css, plus.est-slot-groupand.est-times.
Event processor — WebAPI VABookingService
Endpoints (Basic Auth, each behind its own ControllerSemphoreService lock; a concurrent call returns 409): POST api/vabooking/event/oncreate, event/onbook, event/oncancel.
- OnCreate — Prime internal note "Virtual Assessment booking link sent: https://forms.ethosbuilding.com.au/booking/va/{code}" → customer SMS (
spEthVABookingSMSOnCreate) + email invite → mark processed. Every customer SMS and email (OnCreate, OnBook, OnCancel) is also recorded on the job as a Prime "Customer Contact" note - format, insurer sync and test-override handling inOptimoRoute-Appointments-Bookings-Logic.md§6.1. - OnBook — per row, stop the batch if the Prime rate-limit budget is at or below
Limit3(rows stay pending). Then:- if no
PrimeAppointmentID: create the appointment —New, type Virtual Assessment,primaryUserId= the assigned estimator, start = slot start, end = start + 1 h; instructions = job number, triage summary (property type, room count, what happened — only for triage schemaTriageFormDataV20260704) and customer name + callback phone; notes = customer notes. Store the ID, re-sync the appointment and the estimator's future calendar; - patch the job to Estimator Booked once (
PrimeJobStatusUpdatedAtguard; only from statusesBookingJobStatus.CanPatchToEstimatorBookedallows) — same status as field bookings; - Prime note with the time in Melbourne (and customer-local if different), triage summary and customer notes;
- customer SMS + email confirmation (customer-local time, change/cancel link), each recorded as a Prime "Customer Contact" note;
- estimator SMS (if
NotificationMobile) and email: job + address, customer + phone, time in Melbourne and customer-local, triage summary, notes, Prime job link, and "you'll need to arrange the call yourself" - each recorded as a Prime Internal note (UserStory1949,PrimeMessageNotes-Spec.md); - mark processed.
- if no
- OnCancel — if there is a Prime appointment (and rate-limit budget), re-read it for a current version and set it
Cancelled; Prime note with the reason; customer SMS + email; mark processed. - Failures — the row stays pending and
Attemptsincrements. On the 5th failure the proc marks it processed +Failed, and the service emailsVABooking:OpsAlertEmail(WebAPI appsettings). Every step is logged tooptimoRouteLogwith moduleVABookingService.
Scheduling — n8n VA Booking WebAPI Process Events
Schedule + webhook hybrid (the pattern of OptimoRoute WebAPI Create and Plan Appointments): a 3-minute schedule as the fallback, plus a Webhook trigger (path 382a7b11-a360-4e25-b904-c7b270dd169c) that VAProcessEvents() calls with the n8nWebhookKeyJul2026 credential. It holds its own DB task lock (spEthTaskLockIsLocked/Insert/Clear 'n8nVABookingProcessEvents'), separate from the field booking flow. The export is in the n8n-backup repo (workflows/VA Booking WebAPI Process Events.json).
AdminPortal
- Desktop Estimators (
/desktopestimators, also embedded as a tab in Settings): grid of name, email, Prime status, working hours, notification mobile, enabled, with a "show disabled" switch. Row click → edit dialog (hours, mobile, enabled). Add → pick fromIPrimeData.GetUsers()(active, not already in the rotation); hours default to 09:00–16:30 and the mobile auto-fills from the user's Prime contact (GetUserMobile), still editable. - Bookings (
/bookings): VA rows are merged into the field grid (IsDesktopEstimator = true, laptop icon) with shared tab counts. The customer link opens/booking/va/{code}. There is no Advanced booking and no staff cancel for VA rows. - Booking Availability and Booking Availability Calendar: desktop estimators appear beside field estimators (
GetDesktopEstimatorAvailabilityDetailsAll); a desktop cell opensDesktopEstimatorScheduledAppointments, and the calendar loads their Prime appointments directly (UserStory1007).spPrimeGetEstimatorIDFromNameresolves desktop-only estimators too, preferring the field row for a dual-role estimator. The nav menu lists desktop estimators' calendars. - Booking KPI alerts support an estimator type of Desktop, which has no state and no travel (see
BookingKPIAlerts-Spec.md).
7. Notifications summary
| Event | Customer | Estimator |
|---|---|---|
| OnCreate | SMS + email: why a VA, booking link, 1300 number | — |
| OnBook | SMS + email: confirmed local time, change/cancel link, "your estimator will contact you on the phone number you provided us with" | Email: job/address, customer + callback phone, time (Melbourne + customer-local), triage summary, notes, Prime link, "you arrange the call". SMS too if NotificationMobile is set |
| OnCancel | SMS + email acknowledgement | — |
Emails go through IEmailMessageData.InsertHtmlMessage from noreply@ethosbuilding.com.au. Customer SMS go to the Prime contact's mobile, not CallbackPhone.
8. Design decisions
- Event outbox over inline calls. Every state change only writes the row and flips a stage to pending, so the customer's request is a fast DB write and Prime flakiness becomes a retry for the background processor.
- Schedule + webhook, not schedule alone. Field booking OnCreate/OnBook only polls; VA adds an immediate nudge because a customer who has just booked a call time should hear back promptly, but nothing depends on it.
- Bounded retries. Five attempts per stage, then an ops alert and stop, rather than looping forever on something that will not fix itself. The alert email carries the exception plus job number (linked to Prime), customer name/phone/email, address, slot (Melbourne time), estimator and Prime appointment ID, so staff can find the booking without a DB lookup.
- Separate table, separate "estimator". "Estimator" means two things: the field estimator (
BookingEstimatorModel,ethBookingEstimatorSkill) and the desktop VA estimator (DesktopEstimatorModel,ethDesktopEstimator). They do not collide, and one person can be both (§5 step 2).
9. Configuration and external dependencies
- Prime appointment type
6fca7477-f5bc-4ebb-bd28-5dc365685541(VA) must exist and keep its ID (it does; its label may change). WebhookSettings:VAProcessEvents— set in FormsPortal and AdminPortal appsettings. WebAPI also calls it from the triage routing; it is not in WebAPI'sappsettings.json, so it must come from the app service settings or that nudge fails (logged, schedule fallback).VABooking:OpsAlertEmail— WebAPI appsettings.- Site options
BookingSMSEnabled,BookingMobileOverride,BookingEmailOverride(shared with field booking). - n8n flow
VA Booking WebAPI Process Eventsimported and active.
10. Known gaps and open items
- Rescheduling ("Change booking" ->
SetDate) re-queues OnBook withPrimeAppointmentIDstill set. Since UserStory1802 OnBook moves that appointment to the new slot and estimator (UpdatePrimeAppointmentOnBook) and re-syncs the mirror. Still open: until OnBook runs (up to one 3-minute tick) the new slot is not protected in availability (the pending-VA guard only covers rows without an appointment), and only the new estimator is emailed - the old one is not told the call moved off their calendar. - No full end-to-end pass on Test (triage → invite → book → Prime appointment → estimator notify → cancel) was recorded when the feature shipped.
- The customer SMS copy was flagged as not signed off at build time.
- Worth confirming the n8n webhook's basic-auth credential matches
n8nWebhookKeyJul2026. - Staff cannot cancel or clear a VA booking from AdminPortal (
spEthVABookingClearhas no caller).
11. Out of scope
- Video call link generation or delivery (the estimator arranges it).
- The customer's own state public holidays (only VIC, the estimators' state, is excluded).
- Hard locking against simultaneous slot picks.
- Showing the estimator's identity to the customer.
- OptimoRoute involvement.

