Skip to content

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 (was docs/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 in C:\GitHub\n8n-backup, which holds the OptimoRoute Dispatch - Enqueue / Process Queue / Queue Tasks flows. The operational behaviour is summarised in OptimoRoute-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.

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 returnsLive dateCorrect?
Tue 10th 09:00Tue 10thTue 10thyes — still inside the window opened Mon 4PM
Tue 10th 16:00Wed 11thWed 11thyes — cutoff send
Tue 10th 16:30Wed 11thWed 11thyes — the resend example
Fri 18th 16:00Mon 21stMon 21styes
Fri 18th 16:00, Mon 21st holidayTue 22ndTue 22ndyes

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.

QuestionDecision
Which calendar picks the target date? send_routes takes ONE account-wide date, but ethCalendar is per-stateBusiness 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 modeln8n 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 resendAlways 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 resendCutoff 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 resendAny 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:

  1. Never call get_dispatch_status without a date. The no-date call returned 2026-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.

  2. live is a reliable per-date boolean. true only on 2026-09-22, false on 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.

  3. success: false arrives on an HTTP 200. Confirmed by calling send_routes on 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 in OptimoRouteController.OrphanedOrders was delete_order returning 200 with success: false while the code cleared optimoRouteID anyway.

  4. ERR_NOTHING_TO_SEND is the normal resend outcome, not a failure. Record it as a no-op.

  5. Routes and notifications have independent states. On 2026-09-22, routes were sent while notifications were not_sent. They can diverge, and notifications.state is the only way to verify the customer side actually went. Record both.

  6. Querying arbitrary dates is free and error-free — Christmas Day returns a clean not_sent, no error. The poll can check freely without guarding.

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

  8. optimoRouteOrder.ScheduledAt is stored LOCAL, not UTC (found 2026-09-17, the hard way). Written by spOptimoRouteUpsertStop straight from get_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 with optimoRouteDay; stored UTC would put an Australian working day at 21:00–05:00. An early version of spOptimoRouteDispatchGetDriverRoutes converted 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 / ConfirmedSentUTC genuinely 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.

ColumnNotes
IDidentity PK
PlanDatedate dispatched
SendAttemptUTCwhen we called send_routes
TriggerCutoff / Change / Manual
SendNotificationsbit, what we asked for
Successfrom the response body, not the HTTP code
ResponseCode, ResponseMessagee.g. ERR_NOTHING_TO_SEND
ChangesTotal/Added/Modified/Removedthe counts that justified the send
RoutesState, NotificationsStatelast known from get_dispatch_status
ConfirmedSentUTCset when routes.state == "sent" is later observed
DriverEmailsQueuedint
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():

  1. Bail if DispatchEnabled = 0.
  2. Resolve the dispatch date (§5.1).
  3. Refresh our own route data for that date first, so the driver email matches what was sent. (GetPlanningRoutesRefreshQueue already does this work.)
  4. Not sent yet and now ≥ cutoff → send_routes, sendNotifications: true. Trigger Cutoff.
  5. Already sent → get_dispatch_status(date, includeRouteChanges=true). If totalCnt > 0and the last successful send is older than DispatchMinResendGapMins → resend. Trigger Change.
  6. Guard: plan status must be PlanningCompleted or RoutesCompleted. A date still in Planning at cutoff is logged and skipped, and alerts after N consecutive skips.
  7. Record the attempt in OptimoRouteDispatch; queue driver emails.
  8. Confirm delivery on the next poll, not inline. The docs are explicit that success: true only means "will transmit shortly" — routes.state == "sent" is the real confirmation.

Error handling:

CodeHandling
ERR_NOTHING_TO_SENDno-op, record, not an error
ERR_SYNCroutes and notifications must go together — log; likely caused by an earlier routes-only send for that date
ERR_ADP_SCHEDULED_NOTIFSneeds 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 poll
  • POST SendRoutes?date=&force=true — manual recovery
  • GET 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).

ElementSpec
Top accent barfour blocks: #07272D / #344E41 / #6D8242 / #67823A
Card600px, 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 labels10px, 700, uppercase, letter-spacing:0.1em, #67823A
Data tables1px 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" /> to CommonDataAccess.csproj — templates are listed individually there, not globbed.
  • Put the loader in CommonOptimoRoute, not FormsData. 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.

KeyDefaultPurpose
RoutePlanningCutoffTime16:00already exists
DispatchEnabled0kill switch — ship disabled
DispatchOperatingStatesVIC,QLD,NSWwhich states count for the any-state business-day rule
DispatchMinResendGapMins15resend throttle
DispatchDriverEmailEnabled1

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:

SwitchWherePurpose
OptimoRoute__DispatchAllowedAzure App Setting, prod WebAPI onlyper-environment; does not travel with a database restore
DispatchEnabledethBookingConfig, Settings > Bookingoperational 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:

  1. "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:

  1. Is VIC,QLD,NSW the right operating-state list?
  2. 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".
  3. On a resend, email all drivers or only those whose day changed? get_dispatch_status gives counts only, no driver breakdown, so changed-only means diffing our own optimoRouteDriverDay snapshot. (This was a Claude suggestion, not a requirement.)
  4. 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. OptimoRouteOrderService populates Phone = appointmentMobile from Prime data (lines 722, 797, 1141). Running CreateOrdersFromAppointmentQueue against 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, and OptimoRouteDispatchRulesTests covers every branch with no database, no account and no particular time of day. Keep it pure — a DateTime.UtcNow in there undoes the whole point.
  • The integration needs the sandbox, but not volume. One driver with three stops proves send_routes transmits, that get_dispatch_status flips to sent, that a change bumps totalCnt, 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.

  1. DONE — fnOptimoRouteDispatchDate + OptimoRouteDispatch (migration 0013) + 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-anchored fnOptimoRoutePlanningCutoffUtc on that date. Also dropped the deprecated fnOptimoRouteSearchCutOff via Programmable/zz_DropDeprecated.sql.
  2. DONE — dispatch service + get_dispatch_status read path + EvaluateDispatch. Verified end to end against the sandbox.
  3. DONE — send_routes write path behind the two-switch interlock, recording to OptimoRouteDispatch, with ERR_NOTHING_TO_SEND / ERR_ADP_SCHEDULED_NOTIFS handling. Recording and confirmation procs verified directly; the send path through ProcessDispatch and 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).
  4. DONE — driver email + template. 0015 adds OptimoRouteDriverRouteEmail; the email is queued into ethEmailMessage and 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:

  1. 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 from fnOptimoRouteDispatchDate so 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.
  2. n8n poll flow — OptimoRoute Dispatch - Process (API). The original export shipped active: false with a placeholder SQL credential.
  3. Alerting — 0017 + spOptimoRouteDispatchAlertRaise, through the existing spMakesafeQueueException exception-SMS list. See §8.
  4. Cc on driver emails — 0016 / DispatchDriverEmailCc, semicolon separated, seeded with andrew.badge and matt.sleigh.

dotnet test reports "zero tests ran" for CommonOptimoRoute.Tests under 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.

ConditionWhen it alertsType
Send fails onceNever — the next poll retries—
Still not dispatched 30 min past the cutoffafter the grace periodNotDispatched
Accepted but never confirmed transmittedafter the grace periodUnconfirmed
Live-date mismatchimmediatelyLiveDateMismatch
fnOptimoRouteDispatchDate returns nullimmediatelyNoDispatchDate
ERR_ADP_SCHEDULED_NOTIFSimmediatelyScheduledNotifs

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 ​

#WhatApplied to dev
0011RoutePlanningCutoffTime configyes
0013OptimoRouteDispatch + dispatch configyes
0015OptimoRouteDriverRouteEmailyes
0016DispatchDriverEmailCcno
0017OptimoRouteDispatchAlert + DispatchAlertAfterMinsno

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 ​

  1. OptimoRoute__DispatchAllowed = true on app-webapi-prod-australiasoutheast-001 only. Double underscore — a colon is silently ignored on Linux App Service and reads as "off". See docs/secretmigration/AzureAppSettings.md.
  2. DispatchEnabled = 1 in Settings → Booking. Ships at 0. Both switches are required.
  3. 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..04 on 2026-09-24, and the five sandbox drivers.
  • DispatchEnabled back to 0 on dev.

Known, and deliberately not fixed ​

  • Proc naming. spEthOptimoRouteDispatchGetForDate / Insert / Confirm predate the table rename and do not match the spOptimoRoute* 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: true has never fired against a real account. Answered by OptimoRoute rather than tested — see §6.