Skip to content

fix(orders): measure order distance and time by road using the company's routing engine - #406

Draft
roncodes wants to merge 1 commit into
mainfrom
fix/order-road-distance
Draft

roncodes wants to merge 1 commit into
mainfrom
fix/order-road-distance

Conversation

@roncodes

Copy link
Copy Markdown
Member

Problem

A Fleetbase Cloud customer in Australia reported a ~30 km run (Sunshine → Melton, VIC) showing 24,980 m / 1,800 s. Those are the numbers the calculate provider produces: straight-line metres, and a time of round(m / 100) × 7.2 s, a flat ~50 km/h. The customer asked when road distance is coming.

Root cause

  • server/config/fleetops.php defaults distance_matrix.provider to env('DISTANCE_MATRIX_PROVIDER', 'calculate'), and Cloud does not set the variable. So every order's distance and time come from Utils::calculateDrivingDistanceAndTime (Vincenty great-circle), not a road router.
  • Fleet-Ops Settings → Routing (display_engine: OSRM by default, or Google, or an extension engine such as Valhalla) is only read by the console (addon/services/route-engine.js) to draw the route on the map. No server code reads it. As a result the map shows a road route while the order stores a straight-line distance.
  • When a road provider was configured, it had no failure handling. An OSRM timeout (the client has a 1 s timeout and swallows errors into routes: []) or a Google error or ZERO_RESULTS element was stored on the order as 0 m / 0 s.
  • fleetops:update-estimations always passed config('fleetops.distance_matrix.provider') explicitly, so it would have ignored any per-company choice anyway.

#383 (no longer measuring from a driver's 0,0 placeholder) is correct and unchanged. This PR is about which provider measures the distance.

Design

One small resolver, Support/Routing/DistanceMatrixProviderResolver. Utils::getDrivingDistanceAndTime uses it when the caller does not pass provider:

  1. DISTANCE_MATRIX_PROVIDER when it is set. Self-hosters who chose a provider keep exactly that provider for every company. calculate acts as a platform kill switch. The config default changes from 'calculate' to null so "unset" can be detected. Utils::distanceMatrix (multi-stop service quotes) still falls back to calculate, so it behaves exactly as before.
  2. Otherwise, the company's routing engine (display_engine → routing_display_engine → router → osrm, the same chain SettingController::getRoutingSettings and the console use). It is used only if the server can serve it responsibly:
    • osrm needs fleetops.osrm.host to point somewhere other than the public demo server router.project-osrm.org. Server-side order estimates are never sent there implicitly; an operator who sets DISTANCE_MATRIX_PROVIDER=osrm can still opt in.
    • google needs a Maps API key (services.google_maps.api_key, then GOOGLE_MAPS_API_KEY).
    • Valhalla, VROOM and custom engines have no server-side client, so they fall through.
  3. Otherwise calculate.

The company comes from $options['company_uuid'], falling back to the session company. Order::setDistanceAndTime passes $this->company_uuid, so queued listeners (HandleOrderCreated, HandleOrderReady) and the cron, which have no session, resolve the right company. fleetops:update-estimations now passes a provider only when --provider is given; otherwise each order resolves its own company's provider.

Fallback: a road provider that throws, or returns no route (distance or time ≤ 0), now gives the straight-line estimate plus a Log::warning('Road distance unavailable, using the straight-line estimate instead.', {provider, origin, destination, error}). An order never stores 0 for two distinct points. Identical points return 0 without calling a provider.

Caching: road results are cached in Redis for 24 h (Utils::DISTANCE_MATRIX_CACHE_TTL). Before this PR they were cached with no expiry, and empty results were cached too. Empty results are no longer cached, so a failed lookup is retried next time. The Google key is now read through config before env(), matching GoogleRoutesTrackingProvider.

Side effect: ServiceRate::pointQuote and GET /v1/orders/{id}/distance-and-time also call getDrivingDistanceAndTime, so they follow the session company's provider too. That keeps them consistent with the stored order distance. Before, that endpoint could overwrite a stored road distance with a straight-line one. Multi-stop quotes (Utils::distanceMatrix) are unchanged.

Fleetbase Cloud configuration needed

For road distance on Cloud after this PR:

  • DISTANCE_MATRIX_PROVIDER must stay unset. If it is set, it overrides every company's routing engine.
  • For companies on the default OSRM engine (most of them, likely including this customer): set OSRM_HOST to a self-hosted OSRM instance whose extract covers the customers' regions (Australia here). With the demo host (the docker-compose default) these companies stay on calculate.
  • For companies that chose Google in Settings → Routing: GOOGLE_MAPS_API_KEY must be set, and the key's project must have the Distance Matrix API enabled. Google has classed that API as Legacy since March 2025, and new projects can no longer enable it, so check the Cloud project.
  • No company setting has to change. A company on the default engine gets road distance as soon as OSRM_HOST is private.

Would configuration alone, without this PR, fix the customer? Mostly yes. DISTANCE_MATRIX_PROVIDER=osrm with a private OSRM_HOST, or =google with an enabled key, would make new orders measure road distance on the current code. But it applies one provider to every tenant and ignores their routing setting. Any timeout, quota error or missing route would store 0 m / 0 s on the order. Results would also be cached in Redis forever, including empties. This PR makes the per-company path safe and the failure mode benign.

Cost and rate limits

  • Calls per order: one when it is created (HandleOrderCreated), one when ready (HandleOrderReady), one per edit, plus one every 10 minutes while it is started and unfinished (fleetops:update-estimations). In that last case the origin is the driver's live position, so the cache rarely hits. A one-hour delivery is roughly 2 + 6 = 8 lookups.
  • Google bills per element; each lookup is one element. List price for Distance Matrix is about US$5 per 1,000 elements (check current pricing and the free tier). For example, 10,000 one-hour orders a month ≈ 80,000 elements ≈ US$400 a month at list price. Google is only used for companies that picked Google, or when the operator sets DISTANCE_MATRIX_PROVIDER=google.
  • OSRM public demo (router.project-osrm.org): its usage policy is at most 1 request per second with no heavy use. A whole Cloud fleet calling from one IP would get throttled or blocked, which is why the company path refuses it. A self-hosted OSRM has no per-call cost and no rate limit beyond its own capacity.
  • The Redis cache key is the coordinate pair alone (shared by OSRM and Google, unchanged here). That only matters if two companies on different engines route the exact same coordinates.

Existing orders

No migration. fleetops:update-estimations runs every 10 minutes and will recompute started, unfinished orders created in the last 2 days with the new provider. It does not touch orders that are created but not started, or completed or cancelled ones, and its origin is the driver's current position (remaining distance, not total). Unstarted orders pick up road distance at their next setDistanceAndTime trigger (ready, edit, payload change). Completed orders keep their straight-line values unless they are backfilled separately.

Tests

  • New server/tests/Unit/Support/DistanceMatrixProviderResolverTest.php covers: explicit env wins and calculate as a kill switch; company OSRM, Google and legacy router keys; unsaved settings default to OSRM; the Google key requirement; Valhalla falling through; the public demo host refused in four spellings (and allowed when set explicitly); an unreadable settings store; the Sunshine → Melton OSRM call with its URL and the 24 h cache TTL; the session-company fallback; OSRM NoRoute, a Google timeout and a Google ZERO_RESULTS element each falling back to the estimate with a warning and no cache write; same-point and calculate lookups making no HTTP call.
  • OrderPayloadConfigAndDistanceTest: Order::setDistanceAndTime() with no session company stores the OSRM road distance, then keeps the straight-line estimate (not 0) when OSRM finds no route.
  • CommandContractsTest: the command without --provider calls setDistanceAndTime([]) and logs Using provider: company routing settings.
  • Every changed source line is covered (coverage slice). php-cs-fixer clean.

…y's routing engine

Order distance/time came from the straight-line 'calculate' provider because
DISTANCE_MATRIX_PROVIDER defaults to it and the routing setting is only read by
the console map. Resolve the provider from an explicit DISTANCE_MATRIX_PROVIDER,
then the company's routing engine when the server can call it (private OSRM
host, Google key), then 'calculate'. Fall back to the straight-line estimate
with a warning when a road provider fails instead of storing 0, cache road
results for 24h and stop caching empty ones.

Co-Authored-By: Claude <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (3b4ba17) to head (cb17e8e).
⚠️ Report is 4 commits behind head on release/v0.6.73.

Additional details and impacted files
@@                 Coverage Diff                 @@
##             release/v0.6.73      #406   +/-   ##
===================================================
  Coverage             100.00%   100.00%           
- Complexity             13585     13613   +28     
===================================================
  Files                    634       635    +1     
  Lines                  49308     49373   +65     
===================================================
+ Hits                   49308     49373   +65     
Flag Coverage Δ
backend 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@roncodes
roncodes marked this pull request as draft October 15, 2026 07:58
Base automatically changed from release/v0.6.73 to main October 16, 2026 08:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant