Appearance
UserStory1637 — OptimoRoute Route Dispatch
Status: BUILT. Steps 1-4 complete and verified against dev and the OptimoRoute sandbox. Remaining work is deployment and tidy-up, not implementation — see §9. Branch: UserStory1637OptimoWebAPISendRoutes
Moved to
docs/specs/on 2026-09-30 (wasdocs/UserStory1637/). The n8n export and email previews that sat beside it were removed. The poll-flow export (OptimoRoute Dispatch - Process (API)) has no copy under that name inC:\GitHub\n8n-backup, which holds the OptimoRoute Dispatch - Enqueue / Process Queue / Queue Tasks flows. The operational behaviour is summarised inOptimoRoute-Appointments-Bookings-Logic.md§8.7–§8.8.
1. What this is
Automate what is currently a manual step: a human presses "send routes" in the OptimoRoute web UI. Two new OptimoRoute APIs make it automatable.
POST /v1/send_routes— https://optimoroute.com/api/#send-routesGET /v1/get_dispatch_status— https://optimoroute.com/api/#get-dispatch-status
The existing code already knows this is manual today — see the comment at OptimoRoutePlanningService.cs:175: "I believe this may occur if the Route Notifications have been sent to insured and/or to the drivers."
The requirement, as given
At Cutoff Send Routes at the cutoff (e.g. 4PM) including notifications. Create a notification email to drivers as the API doesn't cover this.
NB: at 4PM you send routes for the next business day.
- 4PM Tuesday 10th → send Wednesday 11th
- 4PM Friday 18th → send Monday 21st
- 4PM Friday 18th, Monday 21st is a public holiday → send Tuesday 22nd
Before next cutoff Check Dispatch for changes on the live date. e.g. 4:30PM Tuesday 10th → Tue 10th 4PM to Wed 11th 4PM is the live window. If there are changes within this window we resend the routes and notification for Wed 11th.
Implement as a WebAPI so this can be called from an n8n flow.
2. The key simplification
The live-window rule is already implemented. dbo.fnOptimoRoutePlanningCutoffUtc returns "today if before cutoff, else the next business day" — which is the live date:
| Now (Melbourne) | Function returns | Live date | Correct? |
|---|---|---|---|
| Tue 10th 09:00 | Tue 10th | Tue 10th | yes — still inside the window opened Mon 4PM |
| Tue 10th 16:00 | Wed 11th | Wed 11th | yes — cutoff send |
| Tue 10th 16:30 | Wed 11th | Wed 11th | yes — the resend example |
| Fri 18th 16:00 | Mon 21st | Mon 21st | yes |
| Fri 18th 16:00, Mon 21st holiday | Tue 22nd | Tue 22nd | yes |
So there is no separate "live window" concept to build. One function resolves the date; whether it is a cutoff send or a resend is just "has this date been sent yet?".
The cutoff time itself is already configuration as of the commit below — ethBookingConfig / RoutePlanningCutoffTime, default 16:00, edited at AdminPortal → Settings → Booking.
3. Decisions already made
Answered by Andrew on 2026-09-16.
| Question | Decision |
|---|---|
Which calendar picks the target date? send_routes takes ONE account-wide date, but ethCalendar is per-state | Business day in ANY operating state. So a VIC-only public holiday still dispatches, because QLD is working. VIC drivers have no orders that day, so the send is harmless to them. |
| Trigger model | n8n polls every ~5 min; the API decides. Not a 4PM cron — the cutoff now lives in config and a cron would silently desync when someone edits it. Polling is idempotent and self-healing: a missed poll sends late rather than never. |
| Customer notifications on resend | Always send notifications (sendNotifications: true on every call). The risk this carried — re-notifying unchanged customers — was resolved on 2026-09-17: OptimoRoute only notifies what changed. See §6. |
| Driver emails on resend | Cutoff emails everyone with a route; a resend emails only drivers whose own day changed. OptimoRoute reports changes against the DAY with no driver breakdown, so this is answered by hashing the route as emailed — see §5.4. |
| What triggers a resend | Any change (totalCnt > 0), with a minimum gap between sends (configurable, default 15 min) so a trickle of edits collapses into one send. |
4. Verified against the sandbox
The dev/sandbox API key is in the WebAPI user-secrets store (a1d40001-9f3c-4d21-8b10-000000000008) under OptimoRoute:ApiKey. Andrew confirmed it is the sandbox and safe to test against. It was pasted into a chat transcript on 2026-09-16 — rotate it.
Probes run on 2026-09-16. These are empirical, not from the docs:
Never call
get_dispatch_statuswithout a date. The no-date call returned2026-09-22— a Tuesday six days out, unrelated to any cutoff. It returns whatever was last sent, not "the date we care about". Always pass our computed date.liveis a reliable per-date boolean.trueonly on2026-09-22,falseon every other date probed. This gives a free cross-check: if we believe we sent date D but OptimoRoute says D is not live, somebody sent another date from the OptimoRoute UI and silently un-lived ours. Treat that as an alert condition.success: falsearrives on an HTTP 200. Confirmed by callingsend_routeson a date with zero changes:HTTP 200 {"success":false,"code":"ERR_NOTHING_TO_SEND","message":"There is nothing new to send for this date."}Branch on
success, never on the status code. This codebase has already been bitten by exactly this — the orphaned-orders incident documented inOptimoRouteController.OrphanedOrderswasdelete_orderreturning 200 withsuccess: falsewhile the code clearedoptimoRouteIDanyway.ERR_NOTHING_TO_SENDis the normal resend outcome, not a failure. Record it as a no-op.Routes and notifications have independent states. On
2026-09-22, routes weresentwhile notifications werenot_sent. They can diverge, andnotifications.stateis the only way to verify the customer side actually went. Record both.Querying arbitrary dates is free and error-free — Christmas Day returns a clean
not_sent, no error. The poll can check freely without guarding.The sandbox cannot validate notifications. Notifications had never been sent on that account. Since resolved by asking OptimoRoute directly rather than by testing — see §6.
optimoRouteOrder.ScheduledAtis stored LOCAL, not UTC (found 2026-09-17, the hard way). Written byspOptimoRouteUpsertStopstraight fromget_routes, which reports the driver's own clock. Across 60 days of stops the stored hour runs 07:00–15:00 clustered on 08:00, and 109 of 110 stops share a date withoptimoRouteDay; stored UTC would put an Australian working day at 21:00–05:00. An early version ofspOptimoRouteDispatchGetDriverRoutesconverted it and pushed every stop ten hours forward — an 08:30 job would have been emailed to the driver as 18:30. The neighbouring table is the opposite:OptimoRouteDispatchSendAttemptUTC/ConfirmedSentUTCgenuinely are UTC and are converted. Check which table you are reading before adding a conversion.
Observed response shape:
json
{"date":"2026-09-22","live":true,
"routes":{"state":"sent","sentAtUtc":"2026-09-16T04:39:57","scheduledForUtc":null,
"changes":{"totalCnt":0,"addedCnt":0,"modifiedCnt":0,"removedCnt":0}},
"notifications":{"state":"not_sent","sentAtUtc":null,"scheduledForUtc":null},
"success":true}5. What to build
5.1 SQL
dbo.fnOptimoRouteDispatchDate(@NowUtc) — new. Same shape as fnOptimoRoutePlanningCutoffUtc but with the any-state business-day rule: the next date that is a business day in at least one operating state. Document it as the sibling of the existing function so the two cannot silently diverge. Reads the cutoff from ethBookingConfig via dbo.fnEthBookingConfigValue (inline TVF — see the warning in that function's header).
dbo.OptimoRouteDispatch — new table, one row per send attempt. optimoRoutePlan is keyed by PlanDate and cannot hold resend history.
| Column | Notes |
|---|---|
ID | identity PK |
PlanDate | date dispatched |
SendAttemptUTC | when we called send_routes |
Trigger | Cutoff / Change / Manual |
SendNotifications | bit, what we asked for |
Success | from the response body, not the HTTP code |
ResponseCode, ResponseMessage | e.g. ERR_NOTHING_TO_SEND |
ChangesTotal/Added/Modified/Removed | the counts that justified the send |
RoutesState, NotificationsState | last known from get_dispatch_status |
ConfirmedSentUTC | set when routes.state == "sent" is later observed |
DriverEmailsQueued | int |
CreatedUTC |
Index on (PlanDate, SendAttemptUTC). Queries needed: has this date been successfully sent? and when was the last successful send? (for the throttle).
New migration starts at 0013 — 0011 (cutoff config) and 0012 (email send failure tracking) are both taken. Add a matching CREATE OR ALTER file under Scripts/Programmable/Functions/.
5.2 Service — IOptimoRouteDispatchService in CommonOptimoRoute
Single entry point ProcessDispatch():
- Bail if
DispatchEnabled = 0. - Resolve the dispatch date (§5.1).
- Refresh our own route data for that date first, so the driver email matches what was sent. (
GetPlanningRoutesRefreshQueuealready does this work.) - Not sent yet and now ≥ cutoff →
send_routes,sendNotifications: true. TriggerCutoff. - Already sent →
get_dispatch_status(date, includeRouteChanges=true). IftotalCnt > 0and the last successful send is older thanDispatchMinResendGapMins→ resend. TriggerChange. - Guard: plan status must be
PlanningCompletedorRoutesCompleted. A date still inPlanningat cutoff is logged and skipped, and alerts after N consecutive skips. - Record the attempt in
OptimoRouteDispatch; queue driver emails. - Confirm delivery on the next poll, not inline. The docs are explicit that
success: trueonly means "will transmit shortly" —routes.state == "sent"is the real confirmation.
Error handling:
| Code | Handling |
|---|---|
ERR_NOTHING_TO_SEND | no-op, record, not an error |
ERR_SYNC | routes and notifications must go together — log; likely caused by an earlier routes-only send for that date |
ERR_ADP_SCHEDULED_NOTIFS | needs a human in the OptimoRoute UI. Fire _smsMessageData.QueueSMSException (existing pattern) and stop retrying rather than looping |
5.3 Endpoints
In WebAPI/Controllers/OptimoRouteController.cs, following the existing ControllerSemphoreService lock pattern used by every other action there.
POST ProcessDispatch— the n8n pollPOST SendRoutes?date=&force=true— manual recoveryGET DispatchStatus?date=— read-only, for diagnostics / AdminPortal
5.4 Driver email
Explicitly in scope per the requirement ("the API doesn't cover this").
Recipients: optimoRouteDriverDay for that PlanDate → primeUsers.email (via EstimatorID). Delivery: queue with IEmailMessageData.InsertHtmlMessage → ethEmailMessage → the existing n8n "Email Queue - Send" flow. No new send mechanism.
Adopt PDP's failure stance: PDPService.SendSummaryEmailAsync deliberately swallows email errors so a queueing problem cannot fail the submission. Same here — the routes have already gone to OptimoRoute by the time we email, so a failed email must not fail the dispatch.
Styling — follow the PDP form emails
Pattern: an embedded-resource chrome template with { @Token } placeholders, plus a generated middle section. See CommonDataAccess/Data/Email/EmailTemplates/PDPEmail.html, FormsData.SetPDPFormEmailBodyAsync (the loader) and PDPEmailSummaryBuilder (the sections).
| Element | Spec |
|---|---|
| Top accent bar | four blocks: #07272D / #344E41 / #6D8242 / #67823A |
| Card | 600px, white on #f0f0f0, border-radius:4px, soft shadow, responsive-table class collapses on mobile |
| Header band | #EBECE7, 26px/700 #07272D title + 14px #4b5b52 subtitle |
| Section labels | 10px, 700, uppercase, letter-spacing:0.1em, #67823A |
| Data tables | 1px solid #e8e8e8, zebra #f9f9f9, label cell 12px/600 #555, value 13px #111 |
| Note callout | #f5f7f0 with 3px solid #67823A left border |
| Footer | #07272D band, 11px #aaaaaa, then a #67823A bottom bar |
Where PDP does not transfer: PDP is a form summary, so every row is label/value (PDPEmailSummaryBuilder.RawRow). A driver route email is a stop list. Forcing that into label/value mashes "08:00 — J015190 — 42 Smith St" into one cell and wraps badly on a phone.
So: reuse the chrome verbatim; keep the label/value table for the summary block (date, stop count, first/last stop, total distance, total drive time); add a columnar row helper for the stops table — same borders, zebra, type scale and colours, three columns instead of two.
To do:
CommonDataAccess/Data/Email/EmailTemplates/DriverRouteEmail.html, tokens{ @DriverName },{ @RouteDate },{ @StopCount },{ @TotalDistanceKms },{ @TotalDriveMins },{ @Sections }.- Add
<EmbeddedResource Include="Data\Email\EmailTemplates\DriverRouteEmail.html" />toCommonDataAccess.csproj— templates are listed individually there, not globbed. - Put the loader in
CommonOptimoRoute, notFormsData. The PDP loader sits in a data-access class despite doing no data access; do not extend that. - A resend must look different from the cutoff send or drivers will ignore the second email. Use the header subtitle and a note callout — "Updated route — replaces the one sent at 4:02 PM" — rather than inventing a colour outside the palette.
5.5 Config keys
All in the same Settings → Booking section, so there is one place to look. Add each to BookingConfigDescriptions and BookingConfigTypes in AdminPortal/Components/Pages/Settings.razor.
| Key | Default | Purpose |
|---|---|---|
RoutePlanningCutoffTime | 16:00 | already exists |
DispatchEnabled | 0 | kill switch — ship disabled |
DispatchOperatingStates | VIC,QLD,NSW | which states count for the any-state business-day rule |
DispatchMinResendGapMins | 15 | resend throttle |
DispatchDriverEmailEnabled | 1 |
6. Risks and open questions
The kill switch is not optional, and one switch is not enough. Every other endpoint in OptimoRouteController is internally reversible. This one contacts real customers and pushes to real driver phones, and a bad resend loop cannot be undone.
DispatchEnabled lives in ethBookingConfig — in the database — and the dev database is restored from prod. So the moment dispatch is enabled in prod, every subsequent dev refresh copies DispatchEnabled = 1 into dev along with the data, silently arming a non-production environment. If that environment is ever pointed at the production OptimoRoute key, it dispatches real routes.
Sending therefore requires both:
| Switch | Where | Purpose |
|---|---|---|
OptimoRoute__DispatchAllowed | Azure App Setting, prod WebAPI only | per-environment; does not travel with a database restore |
DispatchEnabled | ethBookingConfig, Settings > Booking | operational on/off an operator can reach in seconds |
Anything other than a literal true parses as false — a missing key, a typo, a colon instead of the double underscore. The failure mode of that flag has to be "does not send". See docs/secretmigration/AzureAppSettings.md.
RESOLVED 2026-09-17 — does a repeat send re-notify unchanged customers?
No. OptimoRoute support confirmed sendNotifications "does what the UX portal does": it notifies what has changed, not everyone again.
Our own evidence agrees, and was collected before the answer arrived — re-sending 2026-09-24 with zero changes was refused with ERR_NOTHING_TO_SEND rather than re-transmitting, so OptimoRoute tracks what it has already sent and moves deltas only.
So sendNotifications: true on every send is correct, and the config flag this spec originally called for is unnecessary. The reasoning is recorded at the call site in OptimoRouteDispatchService.SendAndRecord, because the tempting "optimisation" — setting it false on resends to avoid spam — inverts the failure: customers whose appointment actually moved after the cutoff would never be told, which is the one outcome this feature exists to prevent.
Open — ask OptimoRoute support:
- "At most one date can have live routes at any time" — so sending Wednesday's routes at 4PM Tuesday makes Wednesday live while Tuesday's drivers may still be finishing jobs. What does the OptimoRoute mobile app do to an in-progress day when the live date moves?
Open — ask Andrew:
- Is
VIC,QLD,NSWthe right operating-state list? - The resend requirement says "we resend the routes and notification" — singular "notification". Does that mean the OptimoRoute customer notifications, the driver email, or both? Currently assumed both. "Drivers get re-emailed every time anything changes" is a materially different experience from "drivers get one email at cutoff".
- On a resend, email all drivers or only those whose day changed?
get_dispatch_statusgives counts only, no driver breakdown, so changed-only means diffing our ownoptimoRouteDriverDaysnapshot. (This was a Claude suggestion, not a requirement.) - What from-address for the driver email? PDP uses
noreply@ethosbuilding.com.au.
6a. How to test this (learned the hard way, 2026-09-17)
The sandbox cannot plan anything yet, and orders are not the reason. start_planning there returns ERR_OPT_RESOURCES_EXCEEDED: the account has one driver ("Sandbox Driver One") with no capacity on most dates. The API has no endpoint to create or list drivers or set their work schedules — that is web-UI-only administration. Until someone configures 2–3 drivers with schedules in the sandbox OptimoRoute UI, seeding orders achieves nothing.
Three test orders are already seeded there for 2026-09-24, orderNo ETHOS-TEST-1637-01..03, noted "safe to delete". They will plan once drivers have schedules.
Dispatching in the sandbox is safe ONLY because its orders have blank contacts. Verified: both existing sandbox orders have email: "" and phone: "", so notifications have nowhere to go. But notificationPreference is already "both" — the safety is the absence of contact details, not any account-level setting.
Do not point the order pipeline at the sandbox.
OptimoRouteOrderServicepopulatesPhone = appointmentMobilefrom Prime data (lines 722, 797, 1141). RunningCreateOrdersFromAppointmentQueueagainst the sandbox using the dev database — a copy of prod — would load it with real customer mobile numbers, and dispatching would then SMS real customers from the "safe" account. Seed the sandbox by hand, with blank contacts.
Dev data and the sandbox are not connected. Dev's optimoRouteOrder.optimoRouteID values point at prod OptimoRoute orders, so asking the sandbox about them returns an unrelated answer. No amount of seeding makes "realistic dispatch testing against dev data" coherent.
So the two things under test are split deliberately:
- The decision rule needs controlled inputs, not real orders. Reaching the resend branch through the service would need an account reporting changed routes, a send recorded minutes earlier, and a wall clock past the cutoff — simultaneously. It lives in
OptimoRouteDispatchRules, a pure function, andOptimoRouteDispatchRulesTestscovers every branch with no database, no account and no particular time of day. Keep it pure — aDateTime.UtcNowin there undoes the whole point. - The integration needs the sandbox, but not volume. One driver with three stops proves
send_routestransmits, thatget_dispatch_statusflips tosent, that a change bumpstotalCnt, and that the driver email renders.
Do not change RoutePlanningCutoffTime on dev to force a cutoff test. It also drives booking availability and vEthBookingEstimatorDayLoad, so it disturbs whatever else is being tested on that shared database.
7. Build order
Nothing can send until the date logic is proven.
- DONE —
fnOptimoRouteDispatchDate+OptimoRouteDispatch(migration0013) + tests. 8/8 pass against dev. Melbourne Cup 2026 (VIC holiday, QLD/NSW working) is the any-state case, plus a test asserting this function deliberately disagrees with the VIC-anchoredfnOptimoRoutePlanningCutoffUtcon that date. Also dropped the deprecatedfnOptimoRouteSearchCutOffviaProgrammable/zz_DropDeprecated.sql. - DONE — dispatch service +
get_dispatch_statusread path +EvaluateDispatch. Verified end to end against the sandbox. - DONE —
send_routeswrite path behind the two-switch interlock, recording toOptimoRouteDispatch, withERR_NOTHING_TO_SEND/ERR_ADP_SCHEDULED_NOTIFShandling. Recording and confirmation procs verified directly; the send path throughProcessDispatchand the two interlock block messages are still unexercised — they need a real past-cutoff moment, and the cutoff must not be changed on dev (see §6a). - DONE — driver email + template.
0015addsOptimoRouteDriverRouteEmail; the email is queued intoethEmailMessageand delivered by the existing n8n Email Queue - Send Prod flow, so no new n8n flow is needed for email. PDP chrome with a columnar stop table. Only drivers whose own route changed are emailed on a resend — see §5.4.
Built beyond the original four steps:
- Dispatch history UI —
spOptimoRouteDispatchGetCurrent, a dialog, and a filled FAB beside the Not Scheduled button on the three booking pages. Scoped to today and the next route date, with the upper bound taken fromfnOptimoRouteDispatchDateso a Friday afternoon shows Monday rather than an empty Saturday. Refresh is jittered 1:30–2:30 so open circuits do not converge on the same second. - n8n poll flow — OptimoRoute Dispatch - Process (API). The original export shipped
active: falsewith a placeholder SQL credential. - Alerting —
0017+spOptimoRouteDispatchAlertRaise, through the existingspMakesafeQueueExceptionexception-SMS list. See §8. - Cc on driver emails —
0016/DispatchDriverEmailCc, semicolon separated, seeded with andrew.badge and matt.sleigh.
dotnet testreports "zero tests ran" forCommonOptimoRoute.Testsunder the xunit.v3 in-process runner. Run the test exe directly:CommonOptimoRoute.Tests/bin/Debug/net10.0/CommonOptimoRoute.Tests.exe
8. Alerting
Alerts go out through the existing dbo.spMakesafeQueueException, which fans an SMS to everyone on the exception list.
The governing principle: alert immediately on what cannot self-heal, and only after a delay on everything else.
A failed send is retried by the next poll five minutes later, so alerting on the first failure would page the on-call list for a network blip that healed before anyone read the text. What matters is the outcome — past the cutoff and still not dispatched — whatever the cause.
| Condition | When it alerts | Type |
|---|---|---|
| Send fails once | Never — the next poll retries | — |
| Still not dispatched 30 min past the cutoff | after the grace period | NotDispatched |
| Accepted but never confirmed transmitted | after the grace period | Unconfirmed |
| Live-date mismatch | immediately | LiveDateMismatch |
fnOptimoRouteDispatchDate returns null | immediately | NoDispatchDate |
ERR_ADP_SCHEDULED_NOTIFS | immediately | ScheduledNotifs |
NotDispatched is the valuable one because it fires on the outcome rather than enumerating causes — it covers a persistent send failure, a plan that never finished, the interlock switched off in production, and failure modes this code does not know about.
DispatchAlertAfterMins (default 30) is the grace period for both delayed cases. One knob, not two: both answer "this should have finished by now".
Throttling is not optional. The poll runs every five minutes and every alertable condition persists until a person fixes it, so the naive version texts the whole on-call list twelve times an hour. OptimoRouteDispatchAlert holds one row per (PlanDate, AlertType) and the unique index insert is the lock — whoever wins sends the SMS, everyone else stays quiet. That is race-safe across scaled-out WebAPI instances, which check-then-send would not be.
NeedsAttention on the result is the single field n8n checks; the rules live in OptimoRouteDispatchRules.EvaluateAttention, not in the flow. It is independent of Action: a late dispatch keeps retrying while it alerts. Folding it into Action would stop the send.
Not covered, deliberately
The poll not running at all. The NotDispatched alert is computed by the poll, so a dead poll produces neither a dispatch nor an alert — the one fully silent failure left.
Accepted as a people backstop rather than building a watchdog: drivers and staff notice unsent routes. The trade is latency, not coverage — the automated alert catches it at 4:30pm, before anyone's day is affected; a human catches it that evening or the next morning. The dispatch button on the booking pages also goes grey when the next route date has not been sent, which is a passive signal someone in AdminPortal may notice during the afternoon.
9. State of play
Migrations
| # | What | Applied to dev |
|---|---|---|
0011 | RoutePlanningCutoffTime config | yes |
0013 | OptimoRouteDispatch + dispatch config | yes |
0015 | OptimoRouteDriverRouteEmail | yes |
0016 | DispatchDriverEmailCc | no |
0017 | OptimoRouteDispatchAlert + DispatchAlertAfterMins | no |
0012 belongs to the email send-failure change set, not this one. Nothing here has been applied to test or production.
fnOptimoRouteSearchCutOff was dropped via Programmable/zz_DropDeprecated.sql, which runs after the programmable phase so the window where its caller referenced a missing function never existed.
Before this can run unattended
OptimoRoute__DispatchAllowed = trueonapp-webapi-prod-australiasoutheast-001only. Double underscore — a colon is silently ignored on Linux App Service and reads as "off". Seedocs/secretmigration/AzureAppSettings.md.DispatchEnabled = 1in Settings → Booking. Ships at0. Both switches are required.- The n8n flow is OptimoRoute Dispatch - Process (API). Check its SQL credential; the original export shipped
active: false.
The Stagehand lane is gone
Before this API existed, a browser automation pressed Send Routes in the OptimoRoute web UI for the next VIC business day, fed by the work queues OptimoDispatchRoutes and OptimoDispatchRoutesTest. Both queues were paused when this dispatcher went live, so the two could never send the same date. The lane was decommissioned on 2026-10-01, and migration 0032 deletes its two queues, their portal resources and their message history. Nothing in this feature ever read them.
Test data to clear when finished
DELETE FROM dbo.OptimoRouteDispatch WHERE ResponseMessage LIKE 'SIMULATED%'— sample rows for the AdminPortal dialog.- Sandbox orders
ETHOS-TEST-1637-01..04on 2026-09-24, and the five sandbox drivers. DispatchEnabledback to0on dev.
Known, and deliberately not fixed
- Proc naming.
spEthOptimoRouteDispatchGetForDate/Insert/Confirmpredate the table rename and do not match thespOptimoRoute*convention the newer procs follow. - Multi-driver resend is unproven. The sandbox licence allows one driver per planning run, so "email only the drivers whose day changed" is covered by unit tests but has never run across several drivers end to end.
sendNotifications: truehas never fired against a real account. Answered by OptimoRoute rather than tested — see §6.

