Skip to content

Improve Fleet-Ops server test coverage - #277

Draft
roncodes wants to merge 662 commits into
mainfrom
feature/fleetops-server-coverage-100
Draft

Improve Fleet-Ops server test coverage#277
roncodes wants to merge 662 commits into
mainfrom
feature/fleetops-server-coverage-100

Conversation

@roncodes

@roncodes roncodes commented Jul 19, 2026

Copy link
Copy Markdown
Member

Summary

This is a progressive backend-only coverage PR for Fleet-Ops ./server.

  • Wires backend coverage reporting into the Composer workflow with a 100% fail-under gate.
  • Expands categorized server tests under server/tests/Unit/... and server/tests/Feature/... instead of adding more flat root-level coverage files.
  • Adds coverage for exports, imports, value objects, events, exceptions, registries, analytics options, mail, notifications, provider registration contracts, driver model helpers, internal driver controller helpers, and LiveController operations monitor snapshot behavior.
  • Extends the shared test bootstrap with additive shims so controller error branches (response()->apiError(...)) and job dispatch branches (Job::dispatch/dispatchIf) are reachable under test — previously these fatally errored and structurally blocked coverage on nearly every controller.
  • Covers the API DriverController protected helper methods, the self-contained route/order driving-simulation methods (no-route and successful-dispatch branches), and the driver-lookup/organization not-found error branches via an in-memory SQLite fixture.

Latest Local Coverage

Fresh host run from COMPOSER_PROCESS_TIMEOUT=0 composer coverage:baseline:

Metric Coverage Covered / Total
Lines 99.45% 33,938 / 34,125 statements
Methods 96.87% 4,211 / 4,347 methods
Classes 82.15% 428 / 521 classes

Coverage gate verification:

  • php scripts/coverage-summary.php coverage/clover.xml --fail-under=100 correctly fails while coverage is below 100%.
  • The fail-under gate remains red by design until backend line coverage reaches 100%.

Latest Progress

  • Line coverage is 99.45%, up from 79.53% at the start of this campaign. The last stretch came from pattern sweeps — one data-driven test per repeated code shape — covering export collection() scoping (17 classes), queryWithRequest across every API controller (19 classes), import createFromImport delegation (13 classes), filter date ranges, Excel download/import seams, request authorize() gates, resource relation resolution, observer/listener query seams, and assorted controller/command/job seams.
  • Fifteen production bugs found and fixed. Highlights:
    • Place::insertFromMixed() called insertFromGeocodingLookup() for any plain address string, but no such method existed on Place or any ancestor — every such call raised BadMethodCallException. Defined it as the insert-side twin of createFromGeocodingLookup, mirroring the existing insertFromGoogleAddress shape (the already-present but callerless getValuesFromGeocodingLookup() is exactly its other half).
    • Place::insertFromCoordinates() guarded empty reverse-geocoding results with !$results->count() === 0, comparing a bool to an int so the guard never fired — with no results the code fell through to an address array carrying location 0,0, which array_merge layered over the caller's real coordinates, silently inserting places at Null Island instead of returning false as documented.
    • Lalamove::getQuotationForMarket() called instance(null, null, $market), putting the market in the bool $sandbox slot — without strict_types the string coerced to true, so market-scoped quotations ran against the sandbox host with the market silently dropped.
    • OrderConfig::default() declared a non-nullable self return while returning first(), so companies without a stored transport config fataled instead of having one provisioned.
    • ServiceRate called Collection::sortByDesc() with no argument, throwing whenever a parcel outsized every fee tier.
  • Remaining residue is 187 statements across 94 files, and it is very flat: 44 files have exactly one uncovered line, 29 have two, 14 have three. Per-file targeting stopped paying a while ago; the productive method at this density is profiling the uncovered lines for repeated shapes and writing one data-driven test per shape, plus classifying each line as reachable or unreachable before investing in a fixture.
  • Recent sweeps: guard-clause bailouts across metrics/telematics providers/models, internal-only uuid resolution in relation filters, conditional requirement branches in place requests, uniqueness-rule scoping closures, relation-resource fallbacks for non-model resources, one-line delegation seams on controllers and commands, empty-input guards on multi-value filters, permission gating for global search, and service-area centre/centroid geometry.
  • Surfaces previously written off as blocked that turned out to be reachable: the queryWithRequest seam on all 19 API controllers, Lalamove response handling (Guzzle client is an injectable property), Support\Auth::can() and the form requests gating on it (a real spatie fixture works), and every GEOS-dependent geometry accessor (inject a Brick\Geo\Engine\GeometryEngineRegistry fake — no native extension needed). Blocked labels are worth re-probing rather than trusting.
  • The classification pass is as valuable as the tests: it repeatedly showed that a line I had queued as reachable was in fact unreachable, saving the fixture build. Roughly 40 lines are now confirmed dead by direct analysis.

Validation

Run on the host runtime (asdf PHP 8.4 with Xdebug, XDEBUG_MODE=coverage), no Docker:

  • php -l <changed test file>
  • php scripts/pest-runner.php <changed test file> — pass count compared against the declared test( count, since a fatal truncates a run without reporting any failure
  • XDEBUG_MODE=coverage php scripts/coverage-file-runner.php --coverage-clover=... <changed test file> — per-line slice check that the intended lines actually executed, before trusting a green assertion
  • vendor/bin/php-cs-fixer fix --using-cache=no <changed file>
  • composer test:lint
  • PATH=<asdf php 8.4 bin>:$PATH XDEBUG_MODE=coverage COMPOSER_PROCESS_TIMEOUT=0 composer coverage:baseline
  • php scripts/coverage-summary.php coverage/clover.xml --fail-under=100
  • git diff --check / git diff --cached --check

Current Lowest Coverage Targets

Next backend coverage slices, from the fresh local Clover report (highest absolute uncovered):

File Uncovered statements
server/src/Http/Controllers/Api/v1/OrderController.php 11
server/src/Http/Controllers/Internal/v1/OrderController.php 7
server/src/Models/Place.php 7
server/src/Http/Controllers/Api/v1/DriverController.php 5
server/src/Models/Payload.php 5

These files are now majority-dead rather than untested: 5 of the 7 remaining in Models/Place.php, 2 of the 5 in Api/v1/DriverController.php, and 2 of the 5 in Models/Payload.php are confirmed unreachable (see below).

Practical ceiling

A growing share of what remains is provably unreachable rather than untested. Confirmed by direct analysis so far:

Location Why it cannot execute
Casts/{Point,Polygon,MultiPolygon} instanceof GeometryInterface (or Expression) is tested before each concrete spatial type, so the later arms are shadowed
Http/Resources/v1/{Maintenance,MaintenanceSchedule,WorkOrder,Order} Find::httpResourceForModel() always resolves a class, falling back to FleetbaseResource, so the JsonResource fallbacks never run
Models/Place::createFromMixed re-tests isCoordinatesStrict() inside a branch already gated on it one arm above
Models/Place::findExistingSharedPlace $location is initialised to a SpatialPoint and the catch assigns one, so the non-point guard cannot fire
Models/Place::insertFromMixed (address key) the guard sits in the is_string($place) branch, and empty() on a non-numeric string offset is always true, so it never fires
Models/Place::insertFromMixed (GoogleAddress arm) shadowed by an earlier is_array($place) || is_object($place) arm that catches every object first
Http/Controllers/Api/v1/OrderController::updateActivity the if (!$order) guard follows a try/catch whose findOrder() is : Order-typed and throws rather than returning null
Support/Utils::coordsToCircle the 0..360 loop closes the circle exactly, so the closing-point guard never triggers
Http/Controllers/Internal/v1/GeocoderController guards the result of getPointFromCoordinates(), declared : Point (non-nullable)
Http/Controllers/Internal/v1/MetricsController guards $request->date() against not being a DateTime; it returns Carbon, which extends it
Http/Controllers/Api/v1/DriverController null-company fallback sits after the same variable is already dereferenced
Integrations/Lalamove::__callStatic instance is a defined public static, so PHP never routes it through __callStatic
Http/Controllers/Internal/v1/OrderController::statuses guards method_exists($config, 'activities'), but OrderConfig::activities() is declared
Http/Controllers/Internal/v1/TelematicController::testCredentials guards registry->resolve(), typed : TelematicProviderInterface, which throws rather than returning null (the surrounding try/catch reports it)
Models/ServiceArea::createMultiPolygonFromPoint duplicates the ring-closing guard; Utils::coordsToCircle() already returns a closed ring, so first !== last is never true
Http/Resources/v1/{PurchaseRate,TrackingStatus} relation accessors the middle branch is guarded by method_exists($this, 'loadMissing'), but no class in the hierarchy (FleetbaseResource -> JsonResource) declares it — it only resolves via __call, which method_exists does not see
Models/Payload::setPlace Place::createFromMixed(): ?Place always yields a Model or null, so the Str::isUuid() arm and the trailing else are both shadowed by the instanceof Model arm between them
Models/ServiceRate::getLngLatFromPlace guards Place::getLocationAsPoint(), declared : SpatialPoint (non-nullable), and a SpatialPoint always exposes getLat/getLng
Exports/VehicleExport::locationPart guards Utils::castPoint(), which catches every failure and falls back to Point(0, 0), so it is never falsy
Models/Place::insertFromMixed / createFromMixed see the two entries above plus the shadowed-arm rows — 5 of this file's 7 remaining lines are unreachable

Three things previously written off as untestable turned out not to be, and are now covered:

  • the queryWithRequest seam on all 19 API controllers — it needed only a session store, route-resolver stub and a couple of request macros;
  • the Lalamove API response handling — its post/request helpers are private, but the Guzzle client is an injectable property, so a MockHandler exercises the real code path without network access;
  • Support\Auth::can() and the form requests that gate on it — a real spatie fixture works (permissions seeded with guard sanctum, plus model_has_permissions rows keyed to the session user's CompanyUser uuid). The internal create-driver and create-order-config requests now resolve opposite ways through that one gate, which proves the permission is genuinely consulted rather than blanket-true.

The lesson is to re-probe "blocked" labels periodically rather than trusting them.

Two further lines (Internal/v1/OrderController 887-888) are reachable only once the upstream findByIdOrFail defect below is fixed, and are deliberately left uncovered rather than shimmed green.

A handful more are blocked by genuinely external dependencies — Firebase-backed push payloads, the Illuminate\Encryption\Encrypter (absent from this vendor tree), and MySQL-only st_distance_sphere spatial predicates that SQLite cannot evaluate.

Known upstream defect (needs a core-api patch)

Model::findByIdOrFail() in fleetbase/core-api throws BadMethodCallException instead of the ModelNotFoundException it documents, because it calls a getModelNotFoundException() method that does not exist on Eloquent's Builder. Four call sites in this package have catch (ModelNotFoundException) blocks that can never fire, so missing records surface as HTTP 500s. Details, affected lines and a suggested fix are in this PR comment. Two lines in Internal/v1/OrderController.php are intentionally left uncovered pending that fix rather than shimmed green.

Known in-package defect (deferred, not fixed here)

Api\v1\ServiceQuoteController::bestQuote() returns null for an empty collection and is passed straight into serviceQuoteResource(ServiceQuote $serviceQuote), which is non-nullable — so requesting single=1 when no service rate is servicable for the given waypoints returns HTTP 500 instead of a meaningful "no quotes available" response. The same shape appears at two entry points in that file. Details and a suggested guard are in this PR comment. Left unfixed deliberately: it is a behavior change that does not belong in a coverage PR.

Notes

  • This PR is intentionally still progressive and remains below the 100% final target.
  • Local validation is run before every push so broken test slices are not published.
  • Coverage work is pushed in batches of a few validated commits so progress is not stranded locally.
  • Slice-level coverage is verified per change: a passing assertion does not by itself prove the targeted lines executed, so each new test is checked line-by-line against a Clover slice before it is committed.

@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Welcome to Codecov 🎉

Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests.

Thanks for integrating Codecov - We've got you covered ☂️

roncodes and others added 29 commits July 28, 2026 09:31
Adds server/tests/Feature/Http/Api/DriverControllerOrganizationsTest.php
covering currentOrganization with the company-session fallback chain and
the unresolvable-company error branch, and listOrganizations returning
every company the driver's user account belongs to through company_users.

Adds server/tests/Feature/Http/Api/DriverControllerSessionHelpersTest.php
covering the protected session/persistence helpers: session and current
company resolution, the company-from-request seam, user info application,
user/driver/device persistence with firstOrCreate dedupe, uuid and point
utilities, null file resolution, driver lookup with resource wrapping,
and the queryWithRequest vendor filter pipeline (whose query param is
matched against vendors.public_id by the controller callback and
drivers.vendor_uuid by DriverFilter).

The switchOrganization endpoint remains structurally blocked: loading the
core SwitchOrganizationRequest fatals against the harness FormRequest
authorize() signature.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds server/tests/Feature/Http/Api/PurchaseRateControllerOrderCreationTest.php
covering createOrderFromServiceQuote against SQLite: the default-type
order path, payload construction from preliminary data with pickup,
dropoff, and return places persisted, adoption of the quote's payload and
service-rate type, and the integrated-vendor failure branch returning an
api error response with no order written.

Exercising this surface uncovered two latent bugs, both fixed:

- The preliminary-data block called setDropoff($return) instead of
  setReturn($return), which fatals with a TypeError for quotes without a
  return place (the common case) and silently overwrote the dropoff with
  the return place otherwise. The return place is now assigned through
  setReturn and only when present.
- The integrated-vendor catch branch returns response()->apiError(...)
  from a method declared to return ?Order, fataling with a TypeError on
  every vendor failure. The declaration is now Order|JsonResponse|null,
  matching the existing caller which already type-checks the result.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds server/tests/Feature/Http/Internal/LiveControllerEndpointsTest.php
covering the LiveController map feeds against SQLite with a tagged-cache
fake: active-order destination coordinates (hydrating real Point
instances from packed WKB so getCurrentDestinationLocation's lat/lng
filter executes), active routes with the nested driver/tracking/payload
constraints and the unassigned-driver drop, the live order query with
exclusion filtering, viewport-bounded driver and vehicle listings with
the spatial location guards, and filtered place listings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds server/tests/Feature/Http/Internal/SearchControllerEndpointTest.php
covering the internal global search endpoint against SQLite with an
admin session user bypassing per-type permission checks: the blank-query
short circuit, requested-type parsing, order search through the
tracking-number relation subquery, driver search through the user
relation, the generic column search used by vehicles and fleets with
limits, and the unmatched-query empty result.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds server/tests/Unit/Support/ResolvesOrderServiceStopsDbTest.php
covering the ResolvesOrderServiceStops helpers the fake-based trait test
cannot reach, against SQLite: proof resolution by instance/public-id with
invalid-input rejection, endpoint stop completion via stored tracking
statuses, next-incomplete-stop advancement persisting the payload's
current destination, current-stop activity detection through tracking
status codes for both endpoint and waypoint stops, endpoint
tracking-number creation with barcode fakes and payload linkage,
endpoint activity insertion writing tracking statuses and relinking the
number, tracking-number status lookups preferring the linked status
uuid, and updateCurrentServiceStopActivity's location guard,
endpoint-insertion, and skip branches.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds four categorized tests covering small remaining surfaces against
SQLite:

- server/tests/Unit/Observers/FleetOpsCompanyUserAndServiceAreaObserversTest.php:
  CompanyUserObserver driver deletion on membership removal and status
  syncing driven by wasChanged, and ServiceAreaObserver zone cascade
  deletion with the country-border polygon helper seam.
- server/tests/Unit/Listeners/HandleOrderDispatchFailedListenerTest.php:
  the order-creator lookup and failure notification hand-off through a
  notification dispatcher fake, plus the missing-creator skip.
- server/tests/Unit/Console/AssignDriverRolesCommandTest.php: company
  traversal with the users.driver expansion, the admin skip, the
  role-assignment error branch, and the empty-company quiet run.
- server/tests/Unit/Support/AiCapabilityPermissionsTest.php: the shared
  AbstractFleetOpsAICapability helpers through OperationalQueryCapability —
  admin permission bypass, gate delegation seam, search-term extraction
  with stop-word filtering, and the multi-column like matcher.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds server/tests/Unit/Models/SensorReadingsTest.php covering the Sensor
model reading pipeline against SQLite with the token-guard auth manager
and disabled activity log: out-of-threshold readings opening a single
threshold alert without duplication, normal readings resolving open
alerts, severity mapping and alert message generation, and subject
position creation from latitude/longitude and location-keyed attributes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds server/tests/Feature/Http/Internal/VendorControllerEndpointsTest.php
covering the internal VendorController against SQLite with an excel fake:
the export download with filename/format handling, the distinct status
listing, the import pipeline with resolved files and the invalid-file
error branch, the vendor/driver/contact lookup helpers including trashed
and or-fail variants, the contact resource payload projection, and the
vendor personnel updateOrCreate/list/create/delete helpers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds server/tests/Feature/Http/Internal/ContactControllerEndpointsTest.php
covering the internal ContactController against SQLite with an excel
fake: export downloads, the import pipeline with the invalid-file error
branch, contact lookup and vendor-conversion helpers (creation,
personnel linkage, transaction wrapper, vendor resource payload), the
customer context migration rewriting orders and customer-portal issue
metadata onto the converted vendor, and the customer portal
welcome-email guard branches with extension detection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds server/tests/Unit/Support/OperationalQueryDistributionTest.php
covering the OperationalQueryCapability driver geofence distribution
against SQLite with spatial containment stand-ins: the empty-fleet short
circuit, online and updated_at filter application, packed-WKB point
hydration, and per-service-area/zone containment counting as an admin
session user.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds server/tests/Unit/Notifications/OrderFailedChannelsTest.php covering
the OrderFailed notification title construction with the
waypoint-tracking-number preference, broadcast channel construction
across company/api/order channels, and the fcm/apn delegation seams.

Adds server/tests/Unit/Console/AssignCustomerRolesCommandTest.php
covering the fleetops:assign-customer-roles command traversal with user
resolution, the role-assignment error branch, and the quiet empty run.

Adds server/tests/Unit/Models/IntegratedVendorLifecycleTest.php covering
the IntegratedVendor created/updated/deleted boot hooks resolving the
lalamove provider, credential access, the provider/api bridges, and the
webhook-url mutator's explicit and derived-default branches.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds server/tests/Unit/Http/Resources/MaintenanceResourcesTest.php
covering both maintenance API resources: the Ember maintenance-subject
and facilitator type injections with empty passthroughs, the morph
transformer null and JsonResource fallbacks, and full serialization of
loaded polymorphic subject/maintainable relations through the whenLoaded
callbacks against SQLite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds server/tests/Feature/Http/Api/OrderControllerStartActivityTest.php
covering the API OrderController startOrder and updateActivity endpoints
against SQLite with a real transport order-config flow: the
unknown-order, already-started, missing-driver, adhoc-without-driver,
and not-dispatched error branches; the skip-dispatch success path that
starts the order, assigns the driver's current job, fires OrderStarted,
and delegates into updateActivity with the started activity; and the
updateActivity unknown/completed rejections plus the dispatched-activity
branch firing OrderDispatchFailed when no driver is assigned.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds server/tests/Feature/Http/Api/OrderControllerLifecycleActionsTest.php
covering the API OrderController lifecycle endpoints against SQLite with
the transport order-config flow: getNextActivity resolving flow steps
with the 404 branch and proof-of-delivery flag/method injection on
completing activities, completeOrder's incomplete-waypoint guard,
cancelOrder transitioning the order to canceled, and setDestination's
validation and current-service-stop persistence.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds server/tests/Feature/Http/Api/OrderControllerPersistenceHelpersTest.php
covering the API OrderController protected persistence helpers against
SQLite: customer contact firstOrCreate dedupe, the company timezone
fallback, order/proof/file creation, the finalize-order job dispatch
through the chainable dispatch shim, the routing-engine delegation seam,
storage writes via a filesystem fake, proof subject scoping for order
and entity subjects, entity editing settings lookup, and the
order/proof/comment resource wrappers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds server/tests/Feature/Http/Internal/OrderControllerActivityFlowsTest.php
covering the internal OrderController against SQLite with the transport
order-config flow: start's unknown/already-started/driverless guards and
the success path assigning the driver's current job and firing
OrderStarted, updateActivity's proof-of-delivery requirement with the
bypass flag, the dispatched-activity failure without an assigned driver,
lifecycle activity updates writing tracking statuses, next-activity flow
resolution, and setDestination's single-stop rejection plus
multi-waypoint validation and persistence.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds server/tests/Unit/Support/FuelEfficiencyAndCanceledHandlersTest.php
covering the FuelEfficiency weekly cost-per-distance aggregation with a
YEARWEEK SQLite stand-in and the empty-series fallback, the
HandleOrderCanceled listener's driver lookup and notification helpers
through a dispatcher fake with the unassigned fallback, and the
OrderCompleted notification broadcast channels plus fcm/apn delegation
seams.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds server/tests/Unit/Models/ServiceAreaGeometryTest.php covering the
ServiceArea geometry helpers: circular multi-polygon creation from a
point with ring closure, polygon extraction from the border with
geotools point-in-polygon containment for single and multiple
coordinates, and the centroid/location accessor seam that requires the
GEOS engine.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds server/tests/Feature/Http/Internal/ServiceQuoteControllerQueryRecordTest.php
covering queryRecord against SQLite with the calculate distance-matrix
provider: quoting a stored payload against a specific service rate with
single-quote selection, quoting all servicable company rates with
best-quote picking, the integrated-vendor resolution seam, and the
preliminary fallback for unknown payloads.

Exercising queryRecord surfaced a latent fatal, now fixed: the endpoint
called getAllStops()->mapInto(Place::class), but getAllStops() already
normalizes every stop into a Place instance — re-wrapping passed Place
models into the model constructor and threw a TypeError for every real
payload quote. The stops collection is now used directly. Seventh
production bug found and fixed by this campaign.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds server/tests/Unit/Models/DriverModelHelpersTest.php covering the
Driver model helpers against SQLite: position creation from
latitude/longitude and location-keyed coordinate variants with subject
scoping, user resolution through the relation with the uuid fallback,
driver creation from import rows exercising both the existing-user
lookup and fresh-user provisioning branches with vehicle resolution and
country normalization, and identifier lookups matching user
name/email and driver license/public-id columns.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds server/tests/Feature/Http/Internal/OrchestrationControllerImportOrdersTest.php
covering the internal OrchestrationController importOrders endpoint
against SQLite: the empty-rows 422 rejection; a full pickup/dropoff row
import resolving the order config, creating customer contacts and
facilitator vendors on demand, matching vehicles by plate and drivers by
identifier, persisting pickup/dropoff places with locations, required
skills, and entities; multi-waypoint groups collapsing into a single
order with one waypoint per row; and per-group failure capture with
transaction rollback on invalid dates.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds server/tests/Unit/Models/SmallModelSurfacesTest.php covering small
remaining surfaces against SQLite: Zone creation with the active-status
default and its relations/polygon construction with the GEOS centroid
seam, PurchaseRate relations plus resolveFromRequest by uuid and public
id, the Customer creating hook forcing the customer type, ContactObserver
email/phone availability checks with the user deletion path, and the
CreateTrackingStatusRequest session authorization plus its
duplicate-status validation closure across matched, unmatched, and
bypassed inputs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds server/tests/Unit/Support/TelematicServiceTest.php covering the
TelematicService against SQLite: device linking with provider-identity
validation, telemetry reconciliation, and dedupe on re-link; device event
storage with full telemetry field mapping, location normalization, and
payload-resolved device attachment; sensor storage with identity
validation, default locations, and device-linked identity fallback;
device filtering by status and search; connection test recording across
success and failure; credential decryption fallbacks for plain json and
empty values through an encrypter fake; and webhook telematic resolution
by integration id, provider account metadata, and device identity.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds server/tests/Feature/Http/Internal/PositionControllerReplayMetricsTest.php
covering the internal PositionController replay endpoint (channel and
position-id guards, missing-position rejection, and the replay job
dispatch with speed handling), the metrics endpoint with input guards and
calculation over stored positions with packed-WKB coordinates, and the
API ContactController protected helpers: create/update input
normalization with phone formatting and type defaults, contact
instantiation and upsert persistence, public-id lookup, place uuid and
related-user resolution, resource wrappers, and json/error responses.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds server/tests/Unit/Support/MaintainableAndOperationalAlertsTest.php
covering the Maintainable trait through the Part model against SQLite:
needsMaintenance across no-history, overdue-scheduled, and
interval-configured paths, maintenance scheduling with priority details,
and cost/frequency aggregation over completed history. Also covers the
ProcessOperationalAlerts helper bodies: the operational orders query
window with status/company filters, latest-position lookup, alert
settings resolution with defaults, route point collection with pair
normalization and axis-swap handling, minimum distance measurement, and
the once-only notification metadata guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds server/tests/Feature/Http/Internal/WorkOrderAndConfigSurfacesTest.php
covering the internal WorkOrderController sendEmail flow (missing
assignee and missing email 422 guards, and the success path sending the
dispatch mail through a mailer fake with activity logging disabled),
the excel export/import delegation helpers, the OrderConfigController
delete flow with the core-service guard and soft deletion, and the
SensorFilter public-relation resolution with company scoping against
SQLite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds server/tests/Unit/Models/OrderPurchaseAndDriversTest.php covering
the Order purchase helpers against SQLite: service quote resolution by
uuid and public id with purchase-rate creation, the unresolvable-quote
failure, the no-quote fallback creating an internal dispatch
transaction, purchase-rate attachment relinking the new transaction to
the order and voiding the superseded one, and closest-driver discovery
through the spatial company/user/driver chain with the located-pickup
requirement.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds server/tests/Unit/Support/OrchestrationPayloadBuilderTest.php
covering the OrchestrationPayloadBuilder computation helpers: payload
demand aggregation across entities with gram/pound weight conversion and
centimetre dimension normalization into litres, the order-meta fallback
for entity-less payloads, vehicle-only VROOM entries with location
requirements, capacity arrays, and max-task handling, safe meta access
swallowing accessor failures, and coordinate validation with
place-location resolution.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds server/tests/Feature/Http/Api/DeviceControllerCrudTest.php covering
the API DeviceController against SQLite: find/delete with not-found
handling and soft deletion, detach with the missing-device 404 and the
failure branch logging, device creation/update persistence helpers with
resource wrappers, input mapping building last-position points from
latitude/longitude, blank-attachable clearing, and the attachment
failure logging helpers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
roncodes and others added 6 commits July 30, 2026 01:07
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@roncodes

Copy link
Copy Markdown
Member Author

Second defect found during coverage work — single quote with no servicable rates returns a 500

Unlike the findByIdOrFail issue, this one is in this package and could be fixed here. Recording it rather than folding a behaviour change into a coverage PR.

What

Api\v1\ServiceQuoteController::query() ends its service-rate branch with:

if ($single) {
    $bestQuote = $this->bestQuote($serviceQuotes);

    return $this->serviceQuoteResource($bestQuote);
}

bestQuote() is collect($serviceQuotes)->sortBy('amount')->first() — it returns null for an empty collection. serviceQuoteResource() is typed serviceQuoteResource(ServiceQuote $serviceQuote), non-nullable.

Effect

Requesting single=1 when no service rate is servicable for the given waypoints raises:

TypeError: serviceQuoteResource(): Argument #1 ($serviceQuote) must be of type
Fleetbase\FleetOps\Models\ServiceQuote, null given

That surfaces as an HTTP 500 rather than a meaningful "no quotes available" response. Hit while building a fixture with two company service rates that were not servicable for the payload's waypoints — a realistic configuration, not a contrived one.

The same shape appears twice in the file (the equivalent block around line 155 and again around line 329), so both entry points are affected.

Suggested fix

Return an explicit empty-quote response instead of passing null through:

if ($single) {
    $bestQuote = $this->bestQuote($serviceQuotes);

    if (!$bestQuote instanceof ServiceQuote) {
        return response()->apiError('No service quotes available for this route.', 404);
    }

    return $this->serviceQuoteResource($bestQuote);
}

Coverage note

The four lines in the second block (ServiceQuoteController 287, 297, 329, 331) remain uncovered. Reaching them needs a fixture where company service rates are both servicable for the waypoints and quotable end to end; the attempt for this PR produced quotes only through the earlier, already-covered block.

roncodes and others added 22 commits July 30, 2026 02:32
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Place::insertFromMixed() called insertFromGeocodingLookup() for any plain
address string, but no such method existed on Place or any ancestor, so the
call always raised BadMethodCallException. Define it as the insert-side twin
of createFromGeocodingLookup, mirroring insertFromGoogleAddress.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Rule stand-in accepted where() closures and discarded them, so the
company/soft-delete scoping inside them never executed. Run them against a
recording double instead and assert the recorded constraints.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…y fallback

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

2 participants