Skip to content

release: v0.6.61 - #303

Merged
roncodes merged 39 commits into
mainfrom
dev-v0.6.61
Aug 31, 2026
Merged

release: v0.6.61#303
roncodes merged 39 commits into
mainfrom
dev-v0.6.61

Conversation

@roncodes

@roncodes roncodes commented Aug 23, 2026

Copy link
Copy Markdown
Member

Release branch for v0.6.61. Versions bumped in composer.json, package.json and extension.json via flb version-bump --patch, and RELEASE.md written for the tag body.

Included

PR
#300 feat(api): publish the order config flow's graph so consumers can sequence it
#301 fix(api): stop PUT /v1/vehicles/{id} silently discarding the odometer
#302 fix(api): scope issue and fuel-report lists by driver_uuid, not silently by none

Each of the above is retargeted onto this branch and merges here before this PR merges to main.

janni1288 and others added 17 commits August 8, 2026 12:56
…uence it

The public order-config resource projected each activity down to code, status,
details, color, complete, pod_method and require_pod. Those describe an activity
but say nothing about the flow's shape, so what reaches an API consumer is an
unordered set of activities with no way to put them in order.

The stored flow is a directed graph, and the fields that express it were the
ones being dropped: `activities` names the codes an activity can transition to,
`sequence` orders activities reachable from the same parent, and `logic` gates
availability. OrderConfig::nextActivity walks exactly these server-side, and the
console's internal resource returns the flow whole, so the gap is only visible
from the public API.

The consequence is not cosmetic. A client rendering progress from array position
marks an order complete whenever `completed` happens to be declared before the
order's current activity — the default transport config lists `completed` fourth
and `dispatched` last, so a freshly dispatched order shows as finished and
offers no next step at all.

These describe the configured workflow rather than internal state, so there is
nothing here a consumer of the config should not already see. Transitions are
normalised to a list of codes, since flows have been authored both as bare codes
and as objects carrying one, and the three fields are always present — null or
empty rather than absent — so a client can read the contract instead of feeling
for it.
`odometer` is fillable on the Vehicle model and unrestricted by the request
rules, but `VehicleController::vehicleInputFromRequest()` builds its input with
`$request->only([...])` and that allowlist had no odometer in it. A caller
sending one therefore received a 200 and a response body that looked correct,
while the reading went nowhere.

Recording mileage is the most common write a driver app makes against a vehicle
— it is what a fuel report is checked against — and a silent no-op is the worst
of the three possible answers. Accept it or reject it; reporting success for a
discarded field leaves the client with no way to tell.

Adds `odometer` and `odometer_unit` to the projection, and validates them rather
than merely accepting them: the model casts odometer to an integer, so an
unchecked string would have been stored as 0, which reads as a vehicle that has
never moved rather than as an error.

Two tests, using the probe already in the suite for protected helpers: one that
the odometer survives the projection, and one that the projection is still an
allowlist — adding a field must not turn it into "whatever the caller sent", so
company_uuid and uuid must still be dropped.
…tly by none

The base filter resolves a query parameter to a method of the same name and
silently ignores anything it cannot match. `IssueFilter` and `FuelReportFilter`
define `driver()` and nothing else, so a client narrowing a list with
`driver_uuid` — the column's own name, and the name the rest of the payload uses
— had its filter dropped on the floor.

What came back was scoped only by `company_uuid`: every driver's issues, every
driver's fuel reports, with a 200 and nothing in the response to say the request
had not been narrowed. For a driver app that is a disclosure rather than a
nuisance — a driver asking for their own fuel reports receives the whole
company's, and neither side can tell from the exchange that anything went wrong.

Adds `driverUuid`, `driverAssigned` and `vehicleUuid` as aliases on both
filters, delegating to the existing implementations so uuid, public id and
search fallback all behave identically.

Tests assert the alias constrains the driver relation and routes a uuid the same
way `driver()` does. Both fail without the change, with "Call to undefined
method" — which is precisely the failure a caller could not see.

Deliberately not addressed here: the general behaviour of ignoring unrecognised
filter parameters. Rejecting them would be the stronger fix and a breaking one,
since any client currently passing an unknown key would start receiving 400s.
Worth deciding separately.
…wns it

The two odometer tests were appended to VehicleControllerHelperContractsTest,
whose probe extends Internal\v1\VehicleController. The helper they exercise,
vehicleInputFromRequest, lives on Api\v1\VehicleController, so the reflection
lookup errored with "method does not exist" and took PHP CI down.

Move them to ApiVehicleControllerContractsTest, which already has a probe
exposing that helper, and pin the new odometer validation rules in
RequestContractsTest alongside the rest of the vehicle request contract.
…ters

The coverage gate held at 100%; the new vehicleUuid() alias on FuelReportFilter
and IssueFilter was the one statement in each file no test reached, dropping
them to 44/45 and 50/51 and failing the gate.

Exercise the alias through both routing branches — uuid and public id — so it
is pinned the same way driverUuid() already is.
…fields

The compact-resource test pinned the pre-change flow entry, so adding sequence,
activities and logic to the projection made its whole-array comparison fail and
took PHP CI down. Update the expectation to the shape the resource now emits.

Also cover projectTransitions' non-array guard, which no fixture reached — it
was the one statement standing between the new code and the 100% gate.
Inlining the layout switch into the actionButtons getter put `this.layout =`
inside a computed property, which ember/no-side-effects rejects — four errors,
and lint:js took the Ember CI build down. Put the assignment back behind the
`changeLayout` action the getter had replaced, now persisting the choice to
appCache, and let both menu items call it.

Also strip the trailing whitespace and restore the final newlines that prettier
flagged in card.js and this file.
A local symlink into a sibling checkout slipped into the previous commit and
broke `pnpm install` in CI with ENOTDIR. .gitignore's `/node_modules/` has a
trailing slash, so it matches the directory but not a symlink of that name.
A local symlink into a sibling checkout slipped into the previous commit and
broke `pnpm install` in CI with ENOTDIR. .gitignore's `/node_modules/` has a
trailing slash, so it matches the directory but not a symlink of that name.
The trailing slash in `/node_modules/` restricts the pattern to directories, so
a symlink of that name is untracked-but-visible and easy to commit by accident.
A committed one breaks `pnpm install` in CI with ENOTDIR.
@codecov

codecov Bot commented Aug 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (4e0fc5f) to head (853b9aa).

Additional details and impacted files
@@             Coverage Diff              @@
##                main      #303    +/-   ##
============================================
  Coverage     100.00%   100.00%            
- Complexity      9815      9899    +84     
============================================
  Files            523       526     +3     
  Lines          37888     38163   +275     
============================================
+ Hits           37888     38163   +275     
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.

…graph

feat(api): publish the order config flow's graph so consumers can sequence it
fix(api): scope issue and fuel-report lists by driver_uuid, not silently by none
fix(api): stop PUT /v1/vehicles/{id} silently discarding the odometer
…update

PUT/PATCH /v1/drivers/{id} passed `password` straight through to the driver's
user record. Anyone holding a driver's token — or an unlocked handset — could
set a new password without proving they knew the old one, and the account was
theirs. Nothing in the request rules or the controller asked for the current
password, and nothing logged that it had changed.

Changing a password is an authorisation decision, not an attribute update, so
it gets its own operations:

  POST /v1/drivers/{id}/change-password   current_password + password (confirmed)
  POST /v1/drivers/forgot-password        identity -> code by email or SMS
  POST /v1/drivers/reset-password         identity + code + password

`update()` no longer accepts `password` at all. Creating a driver still may:
setting a password on an account that does not exist yet proves nothing about
anyone.

Three things the implementation is deliberate about. A change revokes every
other token and hands the caller a fresh one in the same response, so the
password change ends other sessions without signing the driver out of the phone
in their hand. `forgot-password` answers identically whether or not the identity
exists, because a reset endpoint that 404s on an unknown number is a way to
enumerate a company's drivers. And a wrong code, an expired code and an unknown
identity all return the same message, so reset cannot be used as an oracle
either.

Follows the pattern already established for customers, and reuses the
VerificationCode mechanism drivers already use for OTP sign-in.

The regression test asserts a password sent to update() never reaches the user.
It could not be run locally: every test touching a controller in this package
fatals here with "Trait AuthorizesRequests not found", on a clean checkout too —
a missing illuminate/foundation in the local server_vendor, not this change.
A manifest is a driver's route: an order-agnostic sequence of stops that may
span several orders, or none the driver has ever seen as an order. The models
are complete — Manifest and ManifestStop carry sequence, per-leg distance and
duration, estimated and actual arrival, and markArrived/markCompleted/
markSkipped with their side effects — but every endpoint lived behind the
console's internal namespace. A driver could be assigned a route with no way to
read it, which is why the Navigator's Route tab has been a placeholder.

  GET   /v1/drivers/{id}/manifests   the driver's routes, newest first
  GET   /v1/manifests/{id}           the route, stops in sequence
  PATCH /v1/manifest-stops/{id}      arrive, complete or skip a stop
  POST  /v1/manifests/{id}/optimize  re-sequence what is still to do

Deliberately narrow: a driver may read their own manifests, update the stops on
them, and reorder the ones they have not done. Creating, cancelling and deleting
a manifest is dispatch work and stays internal.

Status changes go through the model's transitions rather than being written as a
column, because arriving and completing carry side effects the column does not.

On optimize, and what it is not. This is the driver's optimise, not the
orchestrator's: the orchestrator allocates orders across a fleet and produces
manifests, while this reorders the stops on one manifest already assigned. It is
a nearest-neighbour heuristic over real road distances from OSRM — walk to the
closest remaining stop, then the closest from there. That is usually a large
improvement on an arbitrary order and is not guaranteed optimal, and the code
says so, because calling it anything stronger would be a claim the
implementation does not support. Completed and skipped stops keep their place: a
route already driven is not re-planned. Fewer than three pending stops returns
unchanged, since there is no ordering to find.

The stop resource carries its place inline rather than as an id — a route of
twenty stops should be one request, not twenty-one — and the manifest resource
omits stops unless they were loaded, so the list endpoint stays a list.

Three resource tests, run locally.
CI enforces 100% line coverage and the new controller was 0/77, because I had
written no controller tests — every such test fatals on my machine, where
illuminate/foundation is not installed.

That was the wrong conclusion to stop at. The driver contracts test already
stubs a missing Laravel class behind a class_exists guard; the same trick works
for the three foundation traits and the response() helper, and does nothing at
all in CI where the real ones exist. So these run locally now, and so does every
other controller test in the package.

Three changes to the controller itself, all for the better:

The status check now runs before the transition rather than after it. An
unrecognised status previously fell through a match with a null default and was
then refused — the right answer by accident, and only because the default did
nothing.

The database lookups and the distance calculation moved behind protected seams.
That is what lets the nearest-neighbour walk be exercised against known
distances rather than a live OSRM.

Fifteen tests: the not-found paths, each status transition, meta with and
without, the filters applied only when asked, the default cap, the too-short
route that returns unchanged, a stop whose place never resolved, and the walk
itself ordering three stops nearest-first while a completed one keeps its place.

The walk test failed at first and the fixture was wrong, not the code: the
controller reads `place.location`, as a Point sits on the real model, and my
fake exposed getLat()/getLng() at the top level. Both ends resolved to null, no
distance was ever measured, and the stops kept their declared order. Worth
recording because a fixture that silently produces null is the kind that makes a
green test meaningless.
…pendency

CI enforces 100% line coverage; the three new methods were 61 uncovered lines.

Validation now follows the house style rather than $request->validate(). The
customer password endpoints next door do explicit checks and return apiError,
and $request->validate() is a foundation macro — matching the neighbours means
the same shape of response and code that can be exercised without booting an
application.

Four small seams make the rest testable without changing any production
behaviour: the identity lookup, the reset-code lookup, sending the code, and the
password comparison. Each is a one-line method delegating to what it did
inline, and the probe overrides them.

Fifteen tests covering every branch: both missing inputs, a password too short,
a mismatched confirmation, a missing driver, a driver with no user account, the
wrong current password changing nothing, and the successful change ending other
sessions while returning a fresh token. For reset: the same-answer-for-everything
property is asserted directly — an unknown identity and a bad code return
identical bodies, so neither can be used as an oracle — along with the code being
spent and every session ending, because a reset is a recovery from losing
control and nothing should keep working.

The tests stub the foundation traits and response() behind existence guards, as
the driver contracts test already does for one Laravel class. In CI, where the
real ones are installed, the guards do nothing. Locally they are what makes a
controller test runnable at all — I had previously concluded these could not be
run here, which was wrong.
Covering the distance helper made its cost obvious. A nearest-neighbour walk
compares every remaining stop at every step, so a twenty-stop route asks roughly
four hundred distance questions — as OSRM lookups that is four hundred network
round trips for one tap, on a handset with a driver waiting.

Straight-line distance instead. Ordering by straight line and ordering by road
rarely disagree about which of several stops is nearest, and where they do the
result is still a valid route, just not the shortest one. The method is a
heuristic either way, so the cheap version is the honest one, and the docblock
no longer claims road distances.

Also covers the geometry itself against known coordinates, and reaches the four
database lookups the probe otherwise replaces — a lookup that swallowed a
connection failure and returned null would make a broken database
indistinguishable from a missing record, and both would answer 404.
The seams that made the endpoints testable were themselves uncovered, because
the probe replaces every one of them. Each is a one-line delegation — to
Eloquent, to the verification-code generator, to the hasher, to the application
— and what is worth asserting is that they delegate at all: a password
comparison that quietly returned true, or a code sender that quietly did
nothing, would each turn a security control into decoration.

Both delivery branches are exercised, an identity shaped like an email address
and one shaped like a phone number, since they take different paths.
The coverage gate rounded to 100% while the file sat at 545/546, and codecov's
patch check — which does not round — caught the one line: a `return` placed
after a call that can throw, in the email branch of the reset-code sender.
Nothing can reach it, so if/else replaces the early return.

Also runs php-cs-fixer over the two files this branch touches. Its alignment
pass reaches further, but reformatting files this change never went near does
not belong in a security fix.
… default

CARTO's basemaps.cartocdn.com raster tiles now require an API key
(https://carto.com/basemaps/apikey/) and render 'API key required'
watermarks on every Fleetbase instance using the Leaflet map.

- Default all Leaflet maps to the keyless OpenStreetMap tile server
- Add Leaflet tile provider URL settings (light + dark mode) to
  Fleet-Ops map settings, persisted per-company via the existing
  fleet-ops/settings/map endpoint
- Resolve tile URLs through the map-settings service / a new
  leaflet-tile-url helper so a custom provider (including keyed
  CARTO URLs) applies across all console Leaflet maps
- Sanitize custom tile URLs server-side (http/https only)
CI enforces 100% line coverage on server/src; the new sanitizeTileUrl
branches (non-string input and valid custom URL passthrough) were the
two uncovered statements failing the coverage gate.
…config-130982

feat(map): configurable Leaflet tile provider + keyless OpenStreetMap default
`password` is guarded on User, so `User::create()` dropped it without a
word — while CreateDriverRequest has always accepted and validated one.
A driver created through the public API could never sign in with the
password their operator chose for them, and the first change-password
call would be refused because the stored hash was never theirs.

Set it after creation, where the model's mutator hashes it.

The helper test harness only had the hashing contract, not an
implementation, so the mutator had nothing to call; bind PHP's own
hashing behind the contract so the password path is exercised.
changePassword resolved the account through Driver::getUser(), which
goes through the `user` relation — and that relation selects a named
subset of columns with `password` not among them. The comparison
therefore ran against an empty string and refused every caller,
whatever they typed. Verified against a live instance: the same call
made in-process returns 200 where the endpoint returned 422.

Load the record directly when a password is at stake.
…uires-current

fix(security): a driver's password can no longer be set by a general update
feat(api): driver-facing manifests — read, run, and re-sequence a route
feat(drivers): add card view layout for Drivers Management
…lace-editing

Fix editing unsaved geocoded places in order form
@roncodes
roncodes merged commit a9131da into main Aug 31, 2026
11 checks passed
@roncodes
roncodes deleted the dev-v0.6.61 branch August 31, 2026 09:31
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