Skip to content

fix: driver and vehicle observer cleanup, index latest status, contact address filter - #391

Merged
roncodes merged 11 commits into
release/v0.6.72from
fix/driver-vehicle-observer-audit
Oct 13, 2026
Merged

roncodes merged 11 commits into
release/v0.6.72from
fix/driver-vehicle-observer-audit

Conversation

@roncodes

Copy link
Copy Markdown
Member

Fixes five bugs from an automated audit of the server code. I reproduced each one with a failing test before fixing it. Each fix is its own commit.

This branch is built on #390 (fix/api-cache-dependencies), so merge #390 first. #390 rewrites the same lines in Driver::assignVehicle, VehicleObserver, UserObserver and CompanyUserObserver. Until #390 merges, this PR's diff also shows its commits.

What changed

  1. Deleting a vehicle deleted its drivers (VehicleObserver::deleted). The comment says the drivers are unassigned, but the code soft-deleted every driver holding the vehicle. Now it sets their vehicle_uuid to null.
  2. Re-assigning a driver the vehicle it already holds removed it (Driver::assignVehicle). The builder update cleared vehicle_uuid on every driver holding the vehicle, including this one. The save that followed was not dirty, so nothing wrote the vehicle back. The update now skips this driver.
  3. A membership change touched the user's drivers in every company (CompanyUserObserver). It matched drivers by user only. Removing someone from company A therefore deleted their driver profile in company B too, and a status change in A could land on B's driver. It now also matches on the membership's company, as HandleUserRemovedFromCompany already does. This is a separate commit because item 4 depends on it.
  4. Driver deletes skipped DriverObserver (UserObserver, CompanyUserObserver, and the same pattern in HandleUserRemovedFromCompany). Builder deletes fire no model events. As a result, orders stayed assigned to the deleted driver and the driver's managed login account was never released. Each driver is now deleted through the model. The model's deleted event also invalidates the API cache, so fix(api-cache): invalidate cached lists when the tables they embed are written #390's manual invalidation in these helpers is removed.
  5. The index latest_status was usually the oldest status (Http/Resources/v1/Index/Order.php). It took trackingStatuses->first(), but the relation has no ordering, so lists generally showed "Created". It now picks the newest status by created_at, then by id for statuses created in the same second.
  6. Filtering contacts by address threw an error (ContactFilter::address). It queried an addresses relation that Contact doesn't have, so ?address= returned a 500. It now searches the contact's place, which is the address the contacts list shows.

Behaviour to review

  • Removing a managed driver account from its only organization now deletes the login account. Before item 4 the account stayed behind with no organization. This is what DriverObserver does whenever a driver is deleted (ProfileAccountManager::releaseForProfile). Staff accounts linked to a driver are untouched. A user who still has a driver or contact profile in another organization keeps their account, which item 3 makes safe.
  • Deleting a user or membership now also unassigns that driver's orders.

Tests

New tests run against an in-memory SQLite database:

  • Unit/Observers/VehicleObserverDeleteTest
  • Unit/Models/DriverAssignVehicleTest
  • Unit/Observers/DriverDeletionCleanupTest: registers the real DriverObserver and covers the user, membership and listener paths.
  • A cross-company case in Unit/Observers/FleetOpsCompanyUserAndServiceAreaObserversTest
  • A newest-status case in Unit/Http/Resources/IndexOrderResourceTest
  • Unit/Http/Filter/ContactFilterAddressTest

Existing tests that asserted the old behaviour were updated, because the old behaviour was the bug:

  • ObserverListenerSeamsTest asserted that vehicle deletion soft-deletes drivers.
  • ControllerFilterContractsTest asserted the addresses relation.
  • Probes and query fakes in ObserverContractsTest, EventContractsTest and HandleUserRemovedFromCompanyTest were changed to match the renamed or re-signed helpers.

Validation

  • Every new test failed before its fix and passes after it.
  • php-cs-fixer --dry-run is clean on all changed files.
  • Full server/tests suite, run file by file: 2,730 tests. 13 fail, and the same 13 fail on fix(api-cache): invalidate cached lists when the tables they embed are written #390's branch without these commits (tracking page, notifications, telematics prune, position replay). None touch this change.
  • PHPStan (level max, no baseline, not run in CI) reports no new kinds of error on the changed files. The only new messages are the usual Eloquent magic-property and mixed ones already present across server/src.

Docs / API impact

There are no request or response schema changes, so neither fleetbase/postman nor fleetbase/fleetbase.io needs updating:

  • latest_status and latest_status_code keep their shape and now carry the correct value.
  • The contacts address filter now works as its name implies instead of returning a 500.

Cached API lists hold every relation their query eager loads, and the
whereHas filters that decide membership, but their keys were only
versioned by the model's own table. Declare `$apiCacheDependencies` on
the cached models (Order, Driver, Vehicle, Device, DeviceEvent, Vendor,
ServiceArea) so a write to an embedded table invalidates the list. The
property is read by fleetbase/core-api#301; until that is released it is
unused.

Waypoint, FleetDriver, FleetVehicle and AssetConnection had no cache
trait, so their writes bumped nothing. They now use HasApiModelCache,
which only registers the invalidation hooks: they have no
HasApiModelBehavior, so none of them gains a cached list.
Order writes that skip model events left the cached order lists, and the
order's own cached lookups, on the old version: dispatching, attaching a
route, purchase rate or transaction, setting distance and time,
scheduling, bulk driver assignment and unassignment, unassigning a
deleted driver's or a vehicle's orders, saving recipient instructions
and recording operational alerts. Each now invalidates the orders cache
for the order's company after the write. The writes themselves stay
quiet.
Raw inserts, query builder updates and deletes, and quiet saves bump no
cache version, so lists embedding those tables kept serving stale rows.
Invalidate the written table after each of them:

- waypoints: raw insert, payload waypoint removal, orchestrator resequence
- payloads: current waypoint and pickup/dropoff/return writes, service
  stop tracking numbers
- tracking numbers and statuses: raw inserts, status pointer updates, and
  the owner's status and tracking number columns
- entities and places: raw inserts and payload reassignment
- drivers: vehicle unassignment and deletes cascaded from vehicles and users
- fleet memberships, trailer detach, contact and issue quiet saves,
  customer reassignment to a vendor
- order delete and restore cascades (payloads, entities, waypoints,
  tracking numbers and statuses)
- telematics: a sweep that changes the connection status; the sync meta
  it rewrites every minute does not invalidate

The writes themselves stay quiet.
…d online writes

The driver and vehicle track endpoints write location quietly on every
ping, so cached driver, vehicle and order lists (and storefront lists
that embed vehicles) showed a location up to the list TTL old.
Invalidating on every ping would keep those lists from ever being served
from cache, so ApiCacheThrottle invalidates a table at most once a
minute per company: a ping costs one Cache::add, and a cached location
is at most a minute old. The online toggle is infrequent and
invalidates on every call.
…dation

Walk every declared relation path through the real models and check
that named tables are exactly the morphTo targets' tables, so a renamed
relation fails here rather than in production (fleetbase/core-api#301
throws on unresolvable entries only in debug). Check that the newly
hooked models bump their table on writes, that Order::dispatch(), raw
waypoint and tracking status inserts and waypoint removal invalidate,
and that throttled invalidation runs once per window.

The tests read the property directly and use only released core-api
APIs, since CI installs the released package.
…leting them

VehicleObserver::deleted soft-deleted every driver holding the vehicle,
although its comment says it unassigns them. Clear their vehicle_uuid
instead.
assignVehicle cleared vehicle_uuid on every driver holding the vehicle,
this driver included, then saved a model that was not dirty, so the
cleared value stayed. Skip this driver when unassigning the others.
… driver

CompanyUserObserver matched drivers by user alone, so removing a user from
one organization deleted their driver profiles in every organization, and a
status change could land on another organization's driver. Match on the
membership's company too, as HandleUserRemovedFromCompany already does.
…bership goes

UserObserver, CompanyUserObserver and HandleUserRemovedFromCompany deleted
drivers with a query builder delete, so DriverObserver never ran: orders
stayed assigned to the deleted driver and a managed login account was never
released. Delete each driver through the model instead; its deleted event
also invalidates the API cache, so the manual invalidation goes.
The index resource took the first loaded tracking status as the latest, but
the relation has no order, so lists usually reported the oldest one
(Created). Pick the newest by created_at, then id.
ContactFilter::address queried an addresses relation that Contact does not
have, so filtering contacts by address threw. Search the contact's place,
the address the contacts list shows.
@roncodes roncodes added the bug Something isn't working label Oct 12, 2026
@roncodes
roncodes merged commit 70485c4 into release/v0.6.72 Oct 13, 2026
4 of 6 checks passed
@roncodes
roncodes deleted the fix/driver-vehicle-observer-audit branch October 13, 2026 06:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant