diff --git a/server/config/geocoder.php b/server/config/geocoder.php index 789eac854..44fb31b22 100644 --- a/server/config/geocoder.php +++ b/server/config/geocoder.php @@ -6,7 +6,6 @@ return [ 'cache' => [ - /* |----------------------------------------------------------------------- | Cache Store @@ -17,9 +16,12 @@ | | Default: null | + | FleetOps geocodes through Fleetbase\FleetOps\Support\Geocoding, so + | this only applies to code that still uses the geocoder-laravel facade. + | */ - 'store' => 'geocode', + 'store' => null, /* |----------------------------------------------------------------------- @@ -56,10 +58,14 @@ */ 'providers' => [ Chain::class => [ + // GoogleMaps takes (client, region, apiKey). The settings-backed + // API key is not loaded yet when this file is read, so only the + // environment is consulted here; use Geocoding::geocoder() for a + // geocoder that honours the settings. GoogleMaps::class => [ - config('services.google_maps.locale', env('GOOGLE_MAPS_LOCALE', 'en')), - config('services.google_maps.api_key', env('GOOGLE_MAPS_API_KEY')), - ] + env('GOOGLE_MAPS_REGION'), + env('GOOGLE_MAPS_API_KEY'), + ], ], ], @@ -104,5 +110,4 @@ | */ 'reader' => null, - ]; diff --git a/server/src/Http/Controllers/Api/v1/DriverController.php b/server/src/Http/Controllers/Api/v1/DriverController.php index 2deb0b7a8..e1b331c50 100644 --- a/server/src/Http/Controllers/Api/v1/DriverController.php +++ b/server/src/Http/Controllers/Api/v1/DriverController.php @@ -23,6 +23,7 @@ use Fleetbase\FleetOps\Models\Vehicle; use Fleetbase\FleetOps\Models\Vendor; use Fleetbase\FleetOps\Support\ApiCacheThrottle; +use Fleetbase\FleetOps\Support\Geocoding; use Fleetbase\FleetOps\Support\GeofenceIntersectionService; use Fleetbase\FleetOps\Support\OSRM; use Fleetbase\FleetOps\Support\ProfileAccountManager; @@ -38,7 +39,7 @@ use Fleetbase\Models\UserDevice; use Fleetbase\Models\VerificationCode; use Fleetbase\Support\Auth; -use Geocoder\Laravel\Facades\Geocoder; +use Geocoder\Query\ReverseQuery; use Illuminate\Http\Request; use Illuminate\Support\Arr; use Illuminate\Support\Carbon; @@ -368,7 +369,7 @@ public function track(string $id, Request $request) if ($isGeocodable) { try { - $geocoded = Geocoder::reverse($latitude, $longitude)->get()->first(); + $geocoded = collect(Geocoding::geocoder()->reverseQuery(ReverseQuery::fromCoordinates($latitude, $longitude))->all())->first(); if ($geocoded) { $driver->updateQuietly([ 'city' => $geocoded->getLocality(), diff --git a/server/src/Http/Controllers/Api/v1/PlaceController.php b/server/src/Http/Controllers/Api/v1/PlaceController.php index 43c19bb6a..2b4bc87ca 100644 --- a/server/src/Http/Controllers/Api/v1/PlaceController.php +++ b/server/src/Http/Controllers/Api/v1/PlaceController.php @@ -7,11 +7,11 @@ use Fleetbase\FleetOps\Http\Resources\v1\DeletedResource; use Fleetbase\FleetOps\Http\Resources\v1\Place as PlaceResource; use Fleetbase\FleetOps\Models\Place; +use Fleetbase\FleetOps\Support\Geocoding; use Fleetbase\FleetOps\Support\PlaceSearch; use Fleetbase\FleetOps\Support\Utils; use Fleetbase\Http\Controllers\Controller; use Fleetbase\LaravelMysqlSpatial\Types\Point; -use Geocoder\Laravel\Facades\Geocoder; use Illuminate\Http\Request; use Illuminate\Support\Str; @@ -375,7 +375,7 @@ protected function firstOrNewPlace(array $attributes): Place protected function geocodeAddress(string $address): mixed { - return Geocoder::geocode($address)->get()->first(); + return Geocoding::lookupAddress($address)->first(); } protected function findPlaceOrFail(string $id): Place 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/Models/Place.php b/server/src/Models/Place.php index 1c3e8ea27..ce68be110 100644 --- a/server/src/Models/Place.php +++ b/server/src/Models/Place.php @@ -367,7 +367,7 @@ public static function insertFromGoogleAddress(\Geocoder\Provider\GoogleMaps\Mod */ public static function createFromGeocodingLookup(string $address, $saveInstance = false): ?Place { - $results = \Geocoder\Laravel\Facades\Geocoder::geocode($address)->get(); + $results = Geocoding::lookupAddress($address); if ($results->isEmpty() || !$results->first()) { $place = (new static())->newInstance(['street1' => $address, 'location' => new SpatialPoint(0, 0)]); @@ -386,7 +386,7 @@ public static function createFromGeocodingLookup(string $address, $saveInstance */ public static function getValuesFromGeocodingLookup(string $address): array { - $results = \Geocoder\Laravel\Facades\Geocoder::geocode($address)->get(); + $results = Geocoding::lookupAddress($address); if ($results->isEmpty() || !$results->first()) { return ['street1' => $address, 'location' => new SpatialPoint(0, 0)]; @@ -617,7 +617,7 @@ public static function createFromCoordinates($coordinates, array $attributes = [ $instance->setAttribute('location', new SpatialPoint($latitude, $longitude)); $instance->fill($attributes); - $results = \Geocoder\Laravel\Facades\Geocoder::reverse($latitude, $longitude)->get(); + $results = Geocoding::lookupCoordinates($latitude, $longitude); if (!$results->isEmpty()) { $instance->fillWithGoogleAddress($results->first()); @@ -653,7 +653,7 @@ public static function insertFromCoordinates($coordinates, array $attributes = [ $longitude = $point->getLng(); } - $results = \Geocoder\Laravel\Facades\Geocoder::reverse($latitude, $longitude)->get(); + $results = Geocoding::lookupCoordinates($latitude, $longitude); if ($results->count() === 0) { return false; diff --git a/server/src/Support/Geocoding.php b/server/src/Support/Geocoding.php index ab33f7c9b..7fc73cf79 100644 --- a/server/src/Support/Geocoding.php +++ b/server/src/Support/Geocoding.php @@ -10,6 +10,7 @@ use Geocoder\StatefulGeocoder; use GuzzleHttp\Client; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Log; /** * Class Geocoding. @@ -233,6 +234,47 @@ 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(); + } + + /** + * Raw address results for a free-text address from the configured + * geocoder. A failed lookup resolves to an empty collection, as it did + * through the geocoder-laravel facade, but is logged rather than hidden. + */ + public static function lookupAddress(string $address): Collection + { + try { + return collect(static::geocoder()->geocodeQuery(GeocodeQuery::create($address))->all()); + } catch (\Throwable $e) { + Log::warning('Geocoding lookup failed: ' . $e->getMessage(), ['address' => $address]); + + return collect(); + } + } + + /** + * Raw address results for a coordinate pair from the configured geocoder, + * resolving a failed lookup to an empty collection like lookupAddress(). + */ + public static function lookupCoordinates(float $latitude, float $longitude): Collection + { + try { + return collect(static::geocoder()->reverseQuery(ReverseQuery::fromCoordinates($latitude, $longitude))->all()); + } catch (\Throwable $e) { + Log::warning('Reverse geocoding lookup failed: ' . $e->getMessage(), ['latitude' => $latitude, 'longitude' => $longitude]); + + return collect(); + } + } + protected static function makeGeocoder(): object { // Allow an alternative geocoder (or test double) to be injected diff --git a/server/src/Support/PlaceSearch.php b/server/src/Support/PlaceSearch.php index fc503c247..cf6ecf7c2 100644 --- a/server/src/Support/PlaceSearch.php +++ b/server/src/Support/PlaceSearch.php @@ -4,7 +4,6 @@ use Fleetbase\FleetOps\Models\Place; use Fleetbase\LaravelMysqlSpatial\Types\Point; -use Geocoder\Laravel\Facades\Geocoder; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Collection; use Illuminate\Support\Str; @@ -69,8 +68,7 @@ public static function geocode(?string $searchQuery = null, $latitude = null, $l return collect(); } - $results = Geocoder::geocode($searchQuery) - ->get() + $results = Geocoding::lookupAddress($searchQuery) ->map(fn ($googleAddress) => Place::createFromGoogleAddress($googleAddress)) ->values(); diff --git a/server/tests/Feature/Http/Api/DriverControllerTrackTest.php b/server/tests/Feature/Http/Api/DriverControllerTrackTest.php index aa075ddb8..02a4091d9 100644 --- a/server/tests/Feature/Http/Api/DriverControllerTrackTest.php +++ b/server/tests/Feature/Http/Api/DriverControllerTrackTest.php @@ -267,15 +267,15 @@ public function getPickupOrCurrentWaypoint(): object $driver->setRelation('vehicle', null); // Reverse geocoding resolves the locality onto the driver record - app()->instance('geocoder', new class { - public function reverse($lat, $lng) + app()->instance('fleetops.geocoder', new class { + public function reverseQuery($query) { return $this; } - public function get() + public function all() { - return collect([new class { + return [new class { public function getLocality() { return 'Singapore'; @@ -290,7 +290,7 @@ public function getCode() } }; } - }]); + }]; } public function __call($method, $arguments) @@ -298,7 +298,6 @@ public function __call($method, $arguments) return $this; } }); - Geocoder\Laravel\Facades\Geocoder::clearResolvedInstance('geocoder'); // Geofence detection failures never block the tracking response $throwingService = new class extends GeofenceIntersectionService { @@ -348,8 +347,8 @@ public function captureException($exception) }; app()->instance('sentry', $sentry); - app()->instance('geocoder', new class { - public function reverse($lat, $lng) + app()->instance('fleetops.geocoder', new class { + public function reverseQuery($query) { throw new RuntimeException('geocoder offline'); } @@ -359,7 +358,6 @@ public function __call($method, $arguments) return $this; } }); - Geocoder\Laravel\Facades\Geocoder::clearResolvedInstance('geocoder'); $throwingService = new class extends GeofenceIntersectionService { public function detectDriverCrossings($driver, $location): array diff --git a/server/tests/Feature/Http/Api/OrderControllerNearbyFiltersTest.php b/server/tests/Feature/Http/Api/OrderControllerNearbyFiltersTest.php index 49e5ccd02..87dc1367b 100644 --- a/server/tests/Feature/Http/Api/OrderControllerNearbyFiltersTest.php +++ b/server/tests/Feature/Http/Api/OrderControllerNearbyFiltersTest.php @@ -116,15 +116,15 @@ public function __call($method, $arguments) app()->instance('db.schema', $connection->getSchemaBuilder()); Illuminate\Support\Facades\DB::clearResolvedInstance('db'); - app()->instance('geocoder', new class { - public function geocode($query) + app()->instance('fleetops.geocoder', new class { + public function geocodeQuery($query) { return $this; } - public function get() + public function all() { - return collect(); + return []; } public function __call($method, $arguments) diff --git a/server/tests/Feature/Http/Api/OrderControllerStartActivityTest.php b/server/tests/Feature/Http/Api/OrderControllerStartActivityTest.php index d3f8f2ea1..5309ffca3 100644 --- a/server/tests/Feature/Http/Api/OrderControllerStartActivityTest.php +++ b/server/tests/Feature/Http/Api/OrderControllerStartActivityTest.php @@ -107,20 +107,20 @@ public function __call($method, $arguments) }); Illuminate\Support\Facades\DB::clearResolvedInstance('db'); - app()->instance('geocoder', new class { - public function geocode($query) + app()->instance('fleetops.geocoder', new class { + public function geocodeQuery($query) { return $this; } - public function reverse($lat, $lng) + public function reverseQuery($query) { return $this; } - public function get() + public function all() { - return collect(); + return []; } public function __call($method, $arguments) diff --git a/server/tests/Feature/Http/Api/PlaceAndServiceQuoteSeamsTest.php b/server/tests/Feature/Http/Api/PlaceAndServiceQuoteSeamsTest.php index 19a2b5742..8e497a0e2 100644 --- a/server/tests/Feature/Http/Api/PlaceAndServiceQuoteSeamsTest.php +++ b/server/tests/Feature/Http/Api/PlaceAndServiceQuoteSeamsTest.php @@ -120,20 +120,20 @@ public function __call($method, $arguments) } }); - app()->instance('geocoder', new class { - public function geocode($query) + app()->instance('fleetops.geocoder', new class { + public function geocodeQuery($query) { return $this; } - public function reverse($latitude, $longitude) + public function reverseQuery($query) { return $this; } - public function get() + public function all() { - return collect(); + return []; } public function __call($method, $arguments) @@ -319,9 +319,17 @@ function fleetopsPlaceSeamsQuoteRequest(string $uri, array $input): Fleetbase\Fl $withLocation = $probe->callHelper('withLocationFromRequest', [], fleetopsPlaceSeamsRequest('v1/places', ['location' => ['lat' => 1.33, 'lng' => 103.83]])); expect($withLocation)->toHaveKey('location'); - // Reverse lookups surface the keyless geocoder rejection pre-network + // Reverse lookups surface geocoder failures rather than swallowing them + $geocoder = app('fleetops.geocoder'); + app()->instance('fleetops.geocoder', new class { + public function reverseQuery($query) + { + throw new Exception('reverse geocoder rejected the request'); + } + }); expect(fn () => $probe->callHelper('createPlaceFromReverseGeocodingLookup', new Fleetbase\LaravelMysqlSpatial\Types\Point(1.3, 103.8))) - ->toThrow(Exception::class); + ->toThrow(Exception::class, 'reverse geocoder rejected the request'); + app()->instance('fleetops.geocoder', $geocoder); $place = Place::where('uuid', '11111111-1111-4111-8111-111111111111')->first(); expect($probe->callHelper('placeResource', $place))->not->toBeNull() @@ -413,24 +421,12 @@ public function __construct(private Geocoder\Provider\GoogleMaps\Model\GoogleAdd { } - public function geocode($query): self - { - return $this; - } - public function geocodeQuery($query) { return new Geocoder\Model\AddressCollection([$this->address]); } - - public function get() - { - return collect([$this->address]); - } }; - app()->instance('geocoder', $geocoder); app()->instance('fleetops.geocoder', $geocoder); - Geocoder\Laravel\Facades\Geocoder::clearResolvedInstances(); // No coordinates were submitted, so the address string is geocoded forward and // the resolved address is filled onto the place rather than the (0, 0) default diff --git a/server/tests/Feature/Http/Api/PurchaseRateControllerOrderCreationTest.php b/server/tests/Feature/Http/Api/PurchaseRateControllerOrderCreationTest.php index e5fd13780..76b568b09 100644 --- a/server/tests/Feature/Http/Api/PurchaseRateControllerOrderCreationTest.php +++ b/server/tests/Feature/Http/Api/PurchaseRateControllerOrderCreationTest.php @@ -60,25 +60,24 @@ public function __call($method, $arguments) }); Illuminate\Support\Facades\DB::clearResolvedInstance('db'); - app()->instance('geocoder', new class { + app()->instance('fleetops.geocoder', new class { public $results; - public function geocode($address) + public function geocodeQuery($query) { return $this; } - public function reverse($latitude, $longitude) + public function reverseQuery($query) { return $this; } - public function get() + public function all() { - return collect(); + return []; } }); - Geocoder\Laravel\Facades\Geocoder::clearResolvedInstances(); Http::clearResolvedInstances(); app()->forgetInstance(Illuminate\Http\Client\Factory::class); diff --git a/server/tests/Feature/Http/Internal/OrchestrationControllerImportOrdersTest.php b/server/tests/Feature/Http/Internal/OrchestrationControllerImportOrdersTest.php index 4cfbd3b72..097768a89 100644 --- a/server/tests/Feature/Http/Internal/OrchestrationControllerImportOrdersTest.php +++ b/server/tests/Feature/Http/Internal/OrchestrationControllerImportOrdersTest.php @@ -62,25 +62,24 @@ public function __call($method, $arguments) }); Illuminate\Support\Facades\DB::clearResolvedInstance('db'); - app()->instance('geocoder', new class { + app()->instance('fleetops.geocoder', new class { public $results; - public function geocode($address) + public function geocodeQuery($query) { return $this; } - public function reverse($latitude, $longitude) + public function reverseQuery($query) { return $this; } - public function get() + public function all() { - return collect(); + return []; } }); - Geocoder\Laravel\Facades\Geocoder::clearResolvedInstances(); $barcodeFake = new class { public function __call($method, $arguments) diff --git a/server/tests/Feature/Http/Internal/OrderControllerImportFromFilesTest.php b/server/tests/Feature/Http/Internal/OrderControllerImportFromFilesTest.php index cd4cba0b2..aa447430b 100644 --- a/server/tests/Feature/Http/Internal/OrderControllerImportFromFilesTest.php +++ b/server/tests/Feature/Http/Internal/OrderControllerImportFromFilesTest.php @@ -54,20 +54,20 @@ public function __call($method, $arguments) app()->instance('request', Request::create('/int/v1/orders/import-from-files', 'POST')); - app()->instance('geocoder', new class { - public function geocode($query) + app()->instance('fleetops.geocoder', new class { + public function geocodeQuery($query) { return $this; } - public function reverse($latitude, $longitude) + public function reverseQuery($query) { return $this; } - public function get() + public function all() { - return collect(); + return []; } public function __call($method, $arguments) diff --git a/server/tests/Feature/Http/Internal/ServiceQuoteControllerQueryRecordTest.php b/server/tests/Feature/Http/Internal/ServiceQuoteControllerQueryRecordTest.php index a9056f85f..ecf6ee994 100644 --- a/server/tests/Feature/Http/Internal/ServiceQuoteControllerQueryRecordTest.php +++ b/server/tests/Feature/Http/Internal/ServiceQuoteControllerQueryRecordTest.php @@ -76,25 +76,24 @@ public function __call($method, $arguments) }); Illuminate\Support\Facades\Redis::clearResolvedInstance('redis'); - app()->instance('geocoder', new class { + app()->instance('fleetops.geocoder', new class { public $results; - public function geocode($address) + public function geocodeQuery($query) { return $this; } - public function reverse($latitude, $longitude) + public function reverseQuery($query) { return $this; } - public function get() + public function all() { - return collect(); + return []; } }); - Geocoder\Laravel\Facades\Geocoder::clearResolvedInstances(); $schema = $connection->getSchemaBuilder(); $tables = [ 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']); diff --git a/server/tests/Unit/Imports/ImportDelegationTest.php b/server/tests/Unit/Imports/ImportDelegationTest.php index 971bef5ee..79afb013b 100644 --- a/server/tests/Unit/Imports/ImportDelegationTest.php +++ b/server/tests/Unit/Imports/ImportDelegationTest.php @@ -99,23 +99,22 @@ public function __call($method, $arguments) }); } - app()->instance('geocoder', new class { - public function geocode($address) + app()->instance('fleetops.geocoder', new class { + public function geocodeQuery($query) { return $this; } - public function reverse($latitude, $longitude) + public function reverseQuery($query) { return $this; } - public function get() + public function all() { - return collect(); + return []; } }); - Geocoder\Laravel\Facades\Geocoder::clearResolvedInstances(); session(['company' => 'company-import-1']); $connection->table('companies')->insert(['uuid' => 'company-import-1', 'name' => 'Import Co']); diff --git a/server/tests/Unit/Models/EntityTest.php b/server/tests/Unit/Models/EntityTest.php index 4efbd72c4..ef45a9168 100644 --- a/server/tests/Unit/Models/EntityTest.php +++ b/server/tests/Unit/Models/EntityTest.php @@ -60,20 +60,20 @@ public function __call($method, $arguments) return null; } }); - app()->instance('geocoder', new class { - public function geocode($query) + app()->instance('fleetops.geocoder', new class { + public function geocodeQuery($query) { return $this; } - public function reverse($lat, $lng) + public function reverseQuery($query) { return $this; } - public function get() + public function all() { - return collect(); + return []; } public function __call($method, $arguments) diff --git a/server/tests/Unit/Models/PayloadWaypointsAndEntitiesTest.php b/server/tests/Unit/Models/PayloadWaypointsAndEntitiesTest.php index 95475732f..3442e77db 100644 --- a/server/tests/Unit/Models/PayloadWaypointsAndEntitiesTest.php +++ b/server/tests/Unit/Models/PayloadWaypointsAndEntitiesTest.php @@ -181,15 +181,15 @@ function fleetopsPayloadWaypointFetch(string $uuid): Payload '33333333-3333-4333-8333-333333333333', ]); - app()->instance('geocoder', new class { - public function geocode($query) + app()->instance('fleetops.geocoder', new class { + public function geocodeQuery($query) { return $this; } - public function get() + public function all() { - return collect(); + return []; } public function __call($method, $arguments) @@ -197,7 +197,6 @@ public function __call($method, $arguments) return $this; } }); - Geocoder\Laravel\Facades\Geocoder::clearResolvedInstances(); // A console place search hands back a uuid that no place carries yet. The // waypoint is created from the rest of the attributes and keeps the search diff --git a/server/tests/Unit/Models/PlaceCreationAndImportTest.php b/server/tests/Unit/Models/PlaceCreationAndImportTest.php index 9ecba1f0e..551dd7413 100644 --- a/server/tests/Unit/Models/PlaceCreationAndImportTest.php +++ b/server/tests/Unit/Models/PlaceCreationAndImportTest.php @@ -184,21 +184,20 @@ public function __call($method, $arguments) }); Illuminate\Support\Facades\Storage::clearResolvedInstance('filesystem'); - app()->forgetInstance('fleetops.geocoder'); - app()->instance('geocoder', new class { - public function geocode($query) + app()->instance('fleetops.geocoder', new class { + public function geocodeQuery($query) { return $this; } - public function reverse($lat, $lng) + public function reverseQuery($query) { return $this; } - public function get() + public function all() { - return collect(); + return []; } public function __call($method, $arguments) @@ -373,9 +372,17 @@ public function reverseQuery($query) $imported = Place::createFromImport(['address' => '88 Single Column'], true); expect($imported)->toBeInstanceOf(Place::class); - // Reverse lookups surface the keyless geocoder rejection pre-network + // Reverse lookups surface geocoder failures rather than swallowing them + $geocoder = app('fleetops.geocoder'); + app()->instance('fleetops.geocoder', new class { + public function reverseQuery($query) + { + throw new Exception('reverse geocoder rejected the request'); + } + }); expect(fn () => Place::createFromReverseGeocodingLookup(new Point(1.36, 103.86), true)) - ->toThrow(Exception::class); + ->toThrow(Exception::class, 'reverse geocoder rejected the request'); + app()->instance('fleetops.geocoder', $geocoder); // Array coordinates resolve through the mixed point branch, then report // failure because the geocoder returns no address to build the place from diff --git a/server/tests/Unit/Models/PlaceGeocodingResultsTest.php b/server/tests/Unit/Models/PlaceGeocodingResultsTest.php index 7c6866b74..514f1b6f7 100644 --- a/server/tests/Unit/Models/PlaceGeocodingResultsTest.php +++ b/server/tests/Unit/Models/PlaceGeocodingResultsTest.php @@ -61,16 +61,6 @@ public function __construct(private array $results) { } - public function geocode($address) - { - return $this; - } - - public function reverse($latitude, $longitude) - { - return $this; - } - public function geocodeQuery($query) { return new AddressCollection($this->results); @@ -80,11 +70,6 @@ public function reverseQuery($query) { return new AddressCollection($this->results); } - - public function get() - { - return collect($this->results); - } }; app()->instance('redis', new class { @@ -99,9 +84,7 @@ public function __call(string $method, array $arguments): mixed } }); - app()->instance('geocoder', $geocoder); app()->instance('fleetops.geocoder', $geocoder); - Geocoder\Laravel\Facades\Geocoder::clearResolvedInstances(); session(['company' => 'company-geo-1']); diff --git a/server/tests/Unit/Models/PlaceSharedAndGeocodingTest.php b/server/tests/Unit/Models/PlaceSharedAndGeocodingTest.php index 996850416..8eb6e9278 100644 --- a/server/tests/Unit/Models/PlaceSharedAndGeocodingTest.php +++ b/server/tests/Unit/Models/PlaceSharedAndGeocodingTest.php @@ -20,19 +20,19 @@ public function __construct() $this->results = collect(); } - public function geocode($address) + public function geocodeQuery($query) { return $this; } - public function reverse($latitude, $longitude) + public function reverseQuery($query) { return $this; } - public function get() + public function all() { - return $this->results; + return $this->results->all(); } } @@ -70,8 +70,7 @@ public function __call($method, $arguments) } }); $geocoder = new FleetOpsPlaceGeocoderFake(); - app()->instance('geocoder', $geocoder); - Geocoder\Laravel\Facades\Geocoder::clearResolvedInstances(); + app()->instance('fleetops.geocoder', $geocoder); Illuminate\Support\Facades\DB::clearResolvedInstance('db'); $schema = $connection->getSchemaBuilder(); diff --git a/server/tests/Unit/Models/ServiceRatePersistenceAndServicabilityTest.php b/server/tests/Unit/Models/ServiceRatePersistenceAndServicabilityTest.php index dd39bcd15..471992385 100644 --- a/server/tests/Unit/Models/ServiceRatePersistenceAndServicabilityTest.php +++ b/server/tests/Unit/Models/ServiceRatePersistenceAndServicabilityTest.php @@ -215,20 +215,20 @@ public function __call($method, $arguments) }); } - app()->instance('geocoder', new class { - public function geocode($query) + app()->instance('fleetops.geocoder', new class { + public function geocodeQuery($query) { return $this; } - public function reverse($lat, $lng) + public function reverseQuery($query) { return $this; } - public function get() + public function all() { - return collect(); + return []; } public function __call($method, $arguments) diff --git a/server/tests/Unit/Support/CreateOrderPreviewDraftingTest.php b/server/tests/Unit/Support/CreateOrderPreviewDraftingTest.php index be20af1bf..0a14ad1b5 100644 --- a/server/tests/Unit/Support/CreateOrderPreviewDraftingTest.php +++ b/server/tests/Unit/Support/CreateOrderPreviewDraftingTest.php @@ -71,15 +71,15 @@ public function __call($method, $arguments) }); Illuminate\Support\Facades\Redis::clearResolvedInstance('redis'); - app()->instance('geocoder', new class { - public function geocode($query) + app()->instance('fleetops.geocoder', new class { + public function geocodeQuery($query) { return $this; } - public function get() + public function all() { - return collect(); + return []; } public function __call($method, $arguments) diff --git a/server/tests/Unit/Support/GeocodingInjectedClientTest.php b/server/tests/Unit/Support/GeocodingInjectedClientTest.php index cdabf4d8f..773aaaf12 100644 --- a/server/tests/Unit/Support/GeocodingInjectedClientTest.php +++ b/server/tests/Unit/Support/GeocodingInjectedClientTest.php @@ -101,33 +101,6 @@ public function __call($method, $arguments) 'country' => 'Singapore', ]); - app()->instance('geocoder', new class($address) { - public function __construct(public GoogleAddress $address) - { - } - - public function geocode($query) - { - return $this; - } - - public function reverse($lat, $lng) - { - return $this; - } - - public function get() - { - return collect([$this->address]); - } - - public function __call($method, $arguments) - { - return $this; - } - }); - Geocoder\Laravel\Facades\Geocoder::clearResolvedInstance('geocoder'); - app()->instance('fleetops.geocoder', new class($address) { public array $queries = []; @@ -232,11 +205,11 @@ public function reverseQuery($query) config()->set('services.google_maps.api_key', null); expect(PlaceSearch::geocode(null))->toHaveCount(0); - // Failures in the fallback geocoder facade collapse to empty results - app()->instance('geocoder', new class { - public function geocode($query) + // Failures in the plain address lookup collapse to empty results + app()->instance('fleetops.geocoder', new class { + public function geocodeQuery($query) { - throw new RuntimeException('facade geocoder down'); + throw new RuntimeException('address geocoder down'); } public function __call($method, $arguments) @@ -244,8 +217,112 @@ public function __call($method, $arguments) return $this; } }); - Geocoder\Laravel\Facades\Geocoder::clearResolvedInstance('geocoder'); expect(PlaceSearch::geocode('anywhere'))->toHaveCount(0); + + // lookupAddress() already absorbs geocoder failures, so the outer guard + // is what catches a result that cannot be turned into a place + app()->instance('fleetops.geocoder', new class { + public function geocodeQuery($query) + { + return $this; + } + + public function all() + { + return [new stdClass()]; + } + }); + expect(PlaceSearch::geocode('anywhere'))->toHaveCount(0); +}); + +test('merged queries rethrow geocoder failures and imports fall back to the raw address', function () { + fleetopsGeocodingInjectedBoot(); + app()->instance('fleetops.geocoder', new class { + public function geocodeQuery($query) + { + throw new RuntimeException('geocode backend down'); + } + + public function reverseQuery($query) + { + throw new RuntimeException('reverse backend down'); + } + }); + + expect(fn () => Geocoding::query('88 Injected Way', 1.34, 103.84))->toThrow(RuntimeException::class, 'reverse backend down'); + + // A single-column import still produces a place from the raw address + $imported = Place::createFromImport(['address' => '88 Injected Way'], false); + expect($imported)->toBeInstanceOf(Place::class) + ->and($imported->street1)->toBe('88 Injected Way'); +}); + +test('without an injected geocoder google maps is built from settings', function () { + app()->forgetInstance('fleetops.geocoder'); + config()->set('services.google_maps.api_key', 'settings-key'); + + try { + $geocoder = Geocoding::geocoder(); + } finally { + config()->set('services.google_maps.api_key', null); + } + + // Constructing it makes no request, so no live client is exercised here + expect($geocoder)->toBeInstanceOf(Geocoder\StatefulGeocoder::class); +}); + +test('address lookups return raw results and log failures as empty results', function () { + fleetopsGeocodingInjectedBoot(); + + // Successful lookups hand back the raw addresses rather than places + expect(Geocoding::lookupAddress('88 Injected Way')->first())->toBeInstanceOf(GoogleAddress::class) + ->and(Geocoding::lookupCoordinates(1.34, 103.84)->first())->toBeInstanceOf(GoogleAddress::class); + + $log = new class { + public array $warnings = []; + + public function warning($message, array $context = []): void + { + $this->warnings[] = [$message, $context]; + } + + public function __call($method, $arguments) + { + return null; + } + }; + app()->instance('log', $log); + Illuminate\Support\Facades\Log::clearResolvedInstance('log'); + + // A rejected lookup collapses to no results, but is logged rather than hidden + app()->instance('fleetops.geocoder', new class { + public function geocodeQuery($query) + { + throw new RuntimeException('REQUEST_DENIED'); + } + + public function reverseQuery($query) + { + throw new RuntimeException('REQUEST_DENIED'); + } + }); + + try { + expect(Geocoding::lookupAddress('88 Injected Way'))->toHaveCount(0) + ->and(Geocoding::lookupCoordinates(1.34, 103.84))->toHaveCount(0) + ->and($log->warnings)->toBe([ + ['Geocoding lookup failed: REQUEST_DENIED', ['address' => '88 Injected Way']], + ['Reverse geocoding lookup failed: REQUEST_DENIED', ['latitude' => 1.34, 'longitude' => 103.84]], + ]); + + // Place creation keeps its raw-address fallback when the lookup fails + $place = Place::createFromGeocodingLookup('88 Injected Way'); + expect($place->street1)->toBe('88 Injected Way') + ->and($place->location->getLat())->toBe(0.0); + } finally { + app()->forgetInstance('log'); + Illuminate\Support\Facades\Log::clearResolvedInstance('log'); + } }); test('locale and region resolve independently from configuration', function () { diff --git a/server/tests/Unit/Support/MoreQuerySeamsTest.php b/server/tests/Unit/Support/MoreQuerySeamsTest.php index c095ae011..09cb8a377 100644 --- a/server/tests/Unit/Support/MoreQuerySeamsTest.php +++ b/server/tests/Unit/Support/MoreQuerySeamsTest.php @@ -305,23 +305,22 @@ 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; } - public function reverse($latitude, $longitude) + public function reverseQuery($query) { return $this; } - public function get() + public function all() { - return collect(); + return []; } }); - Geocoder\Laravel\Facades\Geocoder::clearResolvedInstances(); $class = Fleetbase\FleetOps\Http\Controllers\Internal\v1\GeocoderController::class; $controller = (new ReflectionClass($class))->newInstanceWithoutConstructor(); diff --git a/server/tests/Unit/Support/PlaceSearchRankingTest.php b/server/tests/Unit/Support/PlaceSearchRankingTest.php index e0fde1758..f1cae40dd 100644 --- a/server/tests/Unit/Support/PlaceSearchRankingTest.php +++ b/server/tests/Unit/Support/PlaceSearchRankingTest.php @@ -80,12 +80,12 @@ function fleetopsPlaceSearchWkb(float $lat, float $lng): string function fleetopsPlaceSearchGeocoderFake(bool $throws = false): void { - app()->instance('geocoder', new class($throws) { + app()->instance('fleetops.geocoder', new class($throws) { public function __construct(public bool $throws) { } - public function geocode($query) + public function geocodeQuery($query) { if ($this->throws) { throw new RuntimeException('geocoder down'); @@ -94,9 +94,9 @@ public function geocode($query) return $this; } - public function get() + public function all() { - return collect(); + return []; } public function __call($method, $arguments)