Skip to content

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) ​

ParameterValue
TriggerDecided 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 length30 minutes (ethVABooking.FromDate/ToDate)
Prime appointment length1 hour from the slot start — extra prep/wrap-up time for the estimator. The booking row keeps the real 30 min
Prime appointment typeprimeAppointmentType.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
BufferNone (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 gridEvery half hour, 9:00 AM – 4:30 PM customer-local (15 slots, last 4:00–4:30 PM)
Estimator hoursEach 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 timeFrom the next customer-local day, plus a 1-hour minimum-notice floor
Days offeredThe 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
TimezonesAll 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 quirkPOST appointments expects Melbourne local time; GETs and primeAppointments are UTC. PrimeAppointmentService.CreateAppointment converts (callers pass UTC)
Customer notificationsSMS and email at invite (OnCreate), confirmation (OnBook) and cancellation (OnCancel). Email from the Prime contact; if none, SMS only — never fails the event
Messaging switchesBookingSMSEnabled = '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 notificationEmail on OnBook always (when an email is on file); SMS as well if ethDesktopEstimator.NotificationMobile is set. Not gated by BookingSMSEnabled
Video call linkOut 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
StorageSeparate ethVABooking table — ethBooking and the field flow are untouched
Estimator visibilityThe customer never sees the estimator's name
Sharing code16-char code generated in spEthVABookingCreate. VA codes do not expire (no expiry column; AdminPortal treats them as never-expiring)

3. Where it lives ​

AreaFiles
Customer pageFormsPortal/Components/Pages/VirtualAssessment.razor (/booking/va, /booking/va/{sharingcodeParam}, NoHeaderLayout)
Page statesFormsPortal/Components/Forms/VABooking/ — VAHeader, VADetailsCard, VAUnbooked, VABooked, VACancelled, VANotFound
DialogsFormsPortal/Components/Dialogs/ — VATime (confirm), VACancel (reason), VAShowMoreDates, VAAboutCall
TriggerCommonPrimeAPI/Services/TriageBookingRoutingService.cs, called inline from FormsPortal Triage.razor on submit, or from WebAPI TriageFinalizeQueueService when that didn't route it
Event processorWebAPI/Services/VABookingService.cs, WebAPI/Controllers/VABookingController.cs
Data accessCommonDataAccess/Data/Bookings/VABookingData.cs + Interfaces/IVABookingData.cs; Data/Prime/DesktopEstimatorData.cs + Interfaces/IDesktopEstimatorData.cs; IPrimeData.GetUserMobile
ModelsModels/Booking/VABookingModel, VABookingCreateResultModel, VABookingEventModel (shared row shape for the three GetOn* procs), VABookingListModel, VAAvailabilityModel; Models/Prime/DesktopEstimatorModel, DesktopEstimatorScheduledAppointmentModel
Staff UIAdminPortal DesktopEstimators.razor (+ Forms/DesktopEstimatorInsert, DesktopEstimatorEdit), Bookings.razor, Dialogs/BookingCreate.razor, BookingAvailability.razor, BookingAvailabilityCalendar.razor, Dialogs/DesktopEstimatorScheduledAppointments.razor
WebhookCommonWebhooks IWebhookHandler.VAProcessEvents() → WebhookSettings:VAProcessEvents
SQL in repoDbMigrations/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.

ColumnTypeNotes
PrimeUserIduniqueidentifier PKprimeUsers.id by convention
Enabledbitin/out of the rotation
WorkingHoursStart / WorkingHoursEndtimesingle daily window (not per weekday), Melbourne wall-clock; default 09:00–16:30
NotificationMobilenvarchar NULLif set, the estimator also gets an SMS on OnBook
CreatedAt / ModifiedAt / ModifiedByaudit

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 ​

ColumnTypeNotes
IDint identity PK
SharingCodenvarchargenerated in spEthVABookingCreate
PrimeJobIDuniqueidentifier
PrimeJobNumber / PrimeJobURLnvarchar NULLcopied from primeJobs at create
PrimeContactIDuniqueidentifierthe insured
TriageFormIDint NULLethFormTriage.ID; NULL for staff-created links
TimeZoneIdnvarcharcustomer IANA timezone
DesktopEstimatorUserIduniqueidentifier NULLassigned at confirm, from the availability row
FromDate / ToDatedatetime NULLpicked 30-min slot, UTC
OffsetMinsint NULLcustomer UTC offset at the slot (JS sign convention)
Notesnvarchar NULLcustomer notes from the confirm dialog
CallbackPhonenvarchar NULLthe number the estimator should call, confirmed at booking
PrimeAppointmentIDuniqueidentifier NULLset once the Prime appointment exists
PrimeJobStatusUpdatedAtdatetime2 NULLguards the one-time "Estimator Booked" patch
CancelReasonCode / CancelledAt1 none of the times suit, 2 no longer needed, 3 not the right person, 4 other
OnCreate/OnBook/OnCancel ProcessedAtdatetime2 NULLevent-outbox flags; NULL = pending
OnCreate/OnBook/OnCancel Attemptsintfailure counter
OnCreate/OnBook/OnCancel Failedbitset with ProcessedAt at the 5th failure, so the row stops retrying
InviteViewedAtUtc / SlotPickedAtUtcdatetime2 NULLcustomer interaction timestamps (not stamped for staff)
FirstBookedAtUtc / FirstBookedBydatetime2 / nvarchar(400) NULLfirst booking only; FirstBookedBy = staff UPN, NULL = the insured
CreatedAt / ModifiedAtdatetime2

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) ​

ProcPurpose
spEthVABookingCreateInsert row + generate sharing code; returns ID and code
spEthVABookingGetByCodeLoad the booking for the customer page
spEthVABookingGetAvailability(@VABookingID)The availability engine, §5
spEthVABookingSetDateSlot (UTC), offset, estimator, notes, callback phone; clears OnBookProcessedAt (OnBook pending)
spEthVABookingCancelSets CancelReasonCode/CancelledAt (slot kept for the record); OnCancel pending
spEthVABookingClearResets a slot to unbooked; OnCancel pending if it had a Prime appointment. No caller today
spEthVABookingSetAppointmentIDStores the Prime appointment ID
spEthVABookingSetInteractionTimestampInviteViewed / SlotPicked
spEthVABookingGetOnCreate/OnBook/OnCancelPending 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
spEthVABookingSetPrimeJobStatusUpdatedAtStamps the status-patch guard
spEthVABookingSMSOnCreate/OnBook/OnCancelQueue customer SMS (spMakesafeQueueSMSMessage) to the Prime contact's mobile; honour BookingSMSEnabled and BookingMobileOverride
spEthVABookingSMSOnBookEstimatorQueue the estimator SMS to NotificationMobile (Melbourne time). Ignores BookingSMSEnabled, but does honour BookingMobileOverride
spEthVABookingGetList / spEthVABookingGetCountsAdminPortal 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.

  1. Timezone mapping. TimeZoneId is IANA (what .NET wants) but SQL Server's AT TIME ZONE needs Windows names, so the proc maps Australia/* zones to their Windows equivalents at the top. Estimator maths uses AUS Eastern Standard Time (Melbourne, DST-aware).

  2. 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-cancelled primeAppointments (any type) by primaryUserId;
    • #EstimatorPendingVA — other ethVABooking rows booked to the estimator with PrimeAppointmentID IS NULL and not cancelled (race guard for picked-but-not-yet-synced bookings);
    • #EstimatorFieldBusyDay — Melbourne dates on which the estimator has a non-cancelled field ethBooking (primeUserID) or an optimoRouteOrder scheduled 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.

  3. Days. The next 30 customer-local days from tomorrow, filtered to weekdays that are not VIC public holidays, TOP (10).

  4. Slots. Customer-local 09:00 + 30 min × 0..14, converted to UTC per day (DST-safe), then to Melbourne wall-clock.

  5. Pick an estimator per slot (OUTER APPLY … TOP (1)), requiring:

    • slot inside that estimator's WorkingHoursStart/End and not crossing Melbourne midnight;
    • no overlap with #EstimatorBusy or #EstimatorPendingVA (no buffer);
    • no row in #EstimatorFieldBusyDay for 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.

  6. 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:

  1. resolves the timezone from the job summary (State/Suburb/Postcode);
  2. IVABookingData.Create(jobId, formModel.PrimeContactID ?? jobSummary.ContactId, tz, triageFormId);
  3. FormsPortal stamps BookingRoutedAt, then in the background fires VAProcessEvents() (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.

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 an ethFormSharingCode. An unknown code shows VANotFound.
  • State: CancelledAt set → VACancelled; FromDate null or "change booking" → VAUnbooked; otherwise VABooked.
  • Records InviteViewed the 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 through VAShowMoreDates, which nudges the customer to keep the soonest days. "How does this work?" opens VAAboutCall (video call and camera permissions). With no slots, the page asks the customer to phone 1300 238 467.
  • Picking a slot records SlotPicked and opens VATime: a required callback phone (pre-filled from CallbackPhone or 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().
  • VABooked offers Change booking (back to the slot picker; confirming calls SetDate again) and Cancel booking (VACancel reason → Cancel(ID, reasonCode) → VAProcessEvents()). A cancelled booking cannot be rebooked online; the page gives the phone number.
  • Styling reuses the est-* classes in booking.css, plus .est-slot-group and .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 in OptimoRoute-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:
    1. 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 schema TriageFormDataV20260704) and customer name + callback phone; notes = customer notes. Store the ID, re-sync the appointment and the estimator's future calendar;
    2. patch the job to Estimator Booked once (PrimeJobStatusUpdatedAt guard; only from statuses BookingJobStatus.CanPatchToEstimatorBooked allows) — same status as field bookings;
    3. Prime note with the time in Melbourne (and customer-local if different), triage summary and customer notes;
    4. customer SMS + email confirmation (customer-local time, change/cancel link), each recorded as a Prime "Customer Contact" note;
    5. 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);
    6. mark processed.
  • 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 Attempts increments. On the 5th failure the proc marks it processed + Failed, and the service emails VABooking:OpsAlertEmail (WebAPI appsettings). Every step is logged to optimoRouteLog with module VABookingService.

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 from IPrimeData.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 opens DesktopEstimatorScheduledAppointments, and the calendar loads their Prime appointments directly (UserStory1007). spPrimeGetEstimatorIDFromName resolves 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 ​

EventCustomerEstimator
OnCreateSMS + email: why a VA, booking link, 1300 number—
OnBookSMS + 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
OnCancelSMS + 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's appsettings.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 Events imported and active.

10. Known gaps and open items ​

  • Rescheduling ("Change booking" -> SetDate) re-queues OnBook with PrimeAppointmentID still 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 (spEthVABookingClear has 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.