From 3f24b3040fb2977ab42a73e48e1bbaabd329abe9 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Mon, 19 Oct 2026 12:11:22 +0800 Subject: [PATCH 1/2] fix: resolve geocoder endpoints through the configured fleetops geocoder The internal geocoder/query and geocoder/reverse endpoints used the geocoder-laravel facade, whose Google provider is built from config/geocoder.php when the config loads, before the API key from the settings table is set. Its Chain provider swallowed the resulting failure, so both endpoints returned [] while places/lookup, which goes through Geocoding, resolved the same address. Route both lookups through Geocoding::geocoder() so they share the settings-backed API key, region and locale. Fixes fleetbase/fleetbase#723 --- .../Internal/v1/GeocoderController.php | 12 +++- server/src/Support/Geocoding.php | 10 +++ ...okupAndGeocoderControllerContractsTest.php | 67 +++++++++++++++++++ 3 files changed, 86 insertions(+), 3 deletions(-) diff --git a/server/src/Http/Controllers/Internal/v1/GeocoderController.php b/server/src/Http/Controllers/Internal/v1/GeocoderController.php index b753b26e6..6d3b5a3fb 100644 --- a/server/src/Http/Controllers/Internal/v1/GeocoderController.php +++ b/server/src/Http/Controllers/Internal/v1/GeocoderController.php @@ -3,9 +3,11 @@ namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1; use Fleetbase\FleetOps\Models\Place; +use Fleetbase\FleetOps\Support\Geocoding; use Fleetbase\FleetOps\Support\Utils; use Fleetbase\Http\Controllers\Controller; -use Geocoder\Laravel\Facades\Geocoder; +use Geocoder\Query\GeocodeQuery; +use Geocoder\Query\ReverseQuery; use Illuminate\Http\Request; class GeocoderController extends Controller @@ -97,14 +99,18 @@ function ($googleAddress) { return response()->json([]); } + // Both lookups go through the same geocoder places/lookup uses. The + // geocoder-laravel facade built its provider from config/geocoder.php at + // boot, before the settings-backed API key was loaded, and its chain + // provider swallowed the resulting failure into an empty result. protected function reverseGeocode(float $latitude, float $longitude) { - return Geocoder::reverse($latitude, $longitude)->get(); + return collect(Geocoding::geocoder()->reverseQuery(ReverseQuery::fromCoordinates($latitude, $longitude))->all()); } protected function forwardGeocode(string $query) { - return Geocoder::geocode($query)->get(); + return collect(Geocoding::geocoder()->geocodeQuery(GeocodeQuery::create($query))->all()); } protected function placeFromGoogleAddress($googleAddress) diff --git a/server/src/Support/Geocoding.php b/server/src/Support/Geocoding.php index ab33f7c9b..0422d3289 100644 --- a/server/src/Support/Geocoding.php +++ b/server/src/Support/Geocoding.php @@ -233,6 +233,16 @@ public static function getRegion(): ?string return config('services.google_maps.region') ?: env('GOOGLE_MAPS_REGION', 'us'); } + /** + * The configured geocoder — Google Maps with the settings-backed API key, + * region and locale — for callers that need raw address results rather + * than places. + */ + public static function geocoder(): object + { + return static::makeGeocoder(); + } + protected static function makeGeocoder(): object { // Allow an alternative geocoder (or test double) to be injected diff --git a/server/tests/LookupAndGeocoderControllerContractsTest.php b/server/tests/LookupAndGeocoderControllerContractsTest.php index 6aee55a4c..2006298fe 100644 --- a/server/tests/LookupAndGeocoderControllerContractsTest.php +++ b/server/tests/LookupAndGeocoderControllerContractsTest.php @@ -73,6 +73,20 @@ protected function placeFromGoogleAddress($googleAddress) } } +// Keeps the real forward/reverse lookups so they run against whatever +// geocoder Geocoding resolves, mapping each address to its display string +class FleetOpsGeocoderControllerLookupProbe extends GeocoderController +{ + public function __construct() + { + } + + protected function placeFromGoogleAddress($googleAddress) + { + return ['street' => $googleAddress->getStreetName()]; + } +} + class FleetOpsLookupControllerProbe extends FleetOpsLookupController { public Collection $contacts; @@ -262,6 +276,59 @@ public function toArray(): array ->and($controller->forwardCalls)->toBe(['depot', 'depot']); }); +test('geocoder controller resolves lookups through the fleet ops geocoder rather than the facade', function () { + $geocoder = new class { + public array $queries = []; + + public function geocodeQuery($query) + { + $this->queries[] = $query; + + return new Geocoder\Model\AddressCollection([ + Geocoder\Model\Address::createFromArray(['providedBy' => 'google', 'streetName' => 'улица Зенкова', 'latitude' => 43.2551875, 'longitude' => 76.9567153]), + ]); + } + + public function reverseQuery($query) + { + $this->queries[] = $query; + + return new Geocoder\Model\AddressCollection([ + Geocoder\Model\Address::createFromArray(['providedBy' => 'google', 'streetName' => 'Reverse Way', 'latitude' => 1.3, 'longitude' => 103.8]), + ]); + } + }; + app()->instance('fleetops.geocoder', $geocoder); + + // A facade-bound geocoder must not be consulted: it is the one built + // before the settings-backed API key is available + app()->instance('geocoder', new class { + public function __call($method, $arguments) + { + throw new RuntimeException('geocoder facade should not be used'); + } + }); + Geocoder\Laravel\Facades\Geocoder::clearResolvedInstance('geocoder'); + + try { + $controller = new FleetOpsGeocoderControllerLookupProbe(); + $forward = $controller->geocode(new Request(['query' => 'зенкова 21 алматы', 'single' => true])); + $reverse = $controller->reverse(new Request(['coordinates' => '1.3,103.8'])); + } finally { + app()->forgetInstance('fleetops.geocoder'); + app()->forgetInstance('geocoder'); + Geocoder\Laravel\Facades\Geocoder::clearResolvedInstance('geocoder'); + } + + expect($forward->getData(true))->toBe(['street' => 'улица Зенкова']) + ->and($reverse->getData(true))->toBe([['street' => 'Reverse Way']]) + ->and($geocoder->queries[0])->toBeInstanceOf(Geocoder\Query\GeocodeQuery::class) + ->and($geocoder->queries[0]->getText())->toBe('зенкова 21 алматы') + ->and($geocoder->queries[1])->toBeInstanceOf(Geocoder\Query\ReverseQuery::class) + ->and($geocoder->queries[1]->getCoordinates()->getLatitude())->toBe(1.3) + ->and($geocoder->queries[1]->getCoordinates()->getLongitude())->toBe(103.8); +}); + test('fleet ops lookup controller merges polymorphic contacts vendors and integrated facilitators', function () { session(['company' => 'company-uuid']); From 891672ddcc16adbdb2504232117ca5776c622858 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Mon, 19 Oct 2026 12:28:40 +0800 Subject: [PATCH 2/2] test: bind the fleetops geocoder in the geocoder controller seam test GeocoderController no longer resolves the geocoder-laravel facade, so the seam test's app('geocoder') stand-in was never consulted and the unbound fleetops geocoder tried a live Google request. --- .../tests/Unit/Support/MoreQuerySeamsTest.php | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/server/tests/Unit/Support/MoreQuerySeamsTest.php b/server/tests/Unit/Support/MoreQuerySeamsTest.php index c095ae011..1fc1a05d0 100644 --- a/server/tests/Unit/Support/MoreQuerySeamsTest.php +++ b/server/tests/Unit/Support/MoreQuerySeamsTest.php @@ -305,23 +305,17 @@ public function listTransactions(Fleetbase\FleetOps\Models\FuelProviderConnectio test('geocoder controller seams call the geocoder and build places', function () { fleetopsMoreSeamBoot(); - app()->instance('geocoder', new class { - public function geocode($address) + app()->instance('fleetops.geocoder', new class { + public function geocodeQuery($query) { - return $this; + return new Geocoder\Model\AddressCollection([]); } - public function reverse($latitude, $longitude) + public function reverseQuery($query) { - return $this; - } - - public function get() - { - return collect(); + return new Geocoder\Model\AddressCollection([]); } }); - Geocoder\Laravel\Facades\Geocoder::clearResolvedInstances(); $class = Fleetbase\FleetOps\Http\Controllers\Internal\v1\GeocoderController::class; $controller = (new ReflectionClass($class))->newInstanceWithoutConstructor(); @@ -329,6 +323,7 @@ public function get() // Both directions delegate to the configured geocoder expect(fleetopsMoreSeamInvoke($controller, $class, 'reverseGeocode', [1.30, 103.80]))->toHaveCount(0) ->and(fleetopsMoreSeamInvoke($controller, $class, 'forwardGeocode', ['1 Marina Bay']))->toHaveCount(0); + app()->forgetInstance('fleetops.geocoder'); // And a resolved address is turned into an unsaved place $address = Geocoder\Provider\GoogleMaps\Model\GoogleAddress::createFromArray([