diff --git a/addon/components/order/details/proof.js b/addon/components/order/details/proof.js index 40046c1d..ff4464d7 100644 --- a/addon/components/order/details/proof.js +++ b/addon/components/order/details/proof.js @@ -1,3 +1,30 @@ import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import { task } from 'ember-concurrency-decorators'; -export default class OrderDetailsProofComponent extends Component {} +export default class OrderDetailsProofComponent extends Component { + @service fetch; + @tracked proofs = []; + + constructor(owner, { resource }) { + super(...arguments); + this.loadOrderProofs.perform(resource); + } + + @task *loadOrderProofs(order) { + const proofs = yield this.fetch.get(`orders/${order.id}/proofs`); + + this.proofs = proofs.map((proof) => ({ + ...proof, + type: this.#getTypeFromRemarks(proof.remarks), + })); + } + + #getTypeFromRemarks(remarks = '') { + if (remarks.endsWith('Photo')) return 'photo'; + if (remarks.endsWith('Scan')) return 'scan'; + if (remarks.endsWith('Signature')) return 'signature'; + return undefined; + } +} diff --git a/composer.json b/composer.json index 994ab1b6..fea14a98 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "fleetbase/fleetops-api", - "version": "0.6.33", + "version": "0.6.34", "description": "Fleet & Transport Management Extension for Fleetbase", "keywords": [ "fleetbase-extension", diff --git a/extension.json b/extension.json index b481eae4..468c210b 100644 --- a/extension.json +++ b/extension.json @@ -1,6 +1,6 @@ { "name": "Fleet-Ops", - "version": "0.6.33", + "version": "0.6.34", "description": "Fleet & Transport Management Extension for Fleetbase", "repository": "https://github.com/fleetbase/fleetops", "license": "AGPL-3.0-or-later", diff --git a/package.json b/package.json index ceb31363..cd5b034d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@fleetbase/fleetops-engine", - "version": "0.6.33", + "version": "0.6.34", "description": "Fleet & Transport Management Extension for Fleetbase", "fleetbase": { "route": "fleet-ops" diff --git a/server/src/Console/Commands/TestEmail.php b/server/src/Console/Commands/TestEmail.php index afe820ce..058ffd34 100644 --- a/server/src/Console/Commands/TestEmail.php +++ b/server/src/Console/Commands/TestEmail.php @@ -33,7 +33,7 @@ class TestEmail extends Command public function handle() { $email = $this->argument('email'); - $type = $this->option('type'); + $type = $this->option('type'); $this->info('Sending test email...'); $this->info("Type: {$type}"); @@ -47,40 +47,40 @@ public function handle() default: $this->error("Unknown email type: {$type}"); + return Command::FAILURE; } $this->info('✓ Test email sent successfully!'); + return Command::SUCCESS; } catch (\Exception $e) { $this->error('Failed to send test email: ' . $e->getMessage()); + return Command::FAILURE; } } /** * Send a test customer credentials email. - * - * @param string $email - * @return void */ private function sendCustomerCredentialsEmail(string $email): void { // Create a mock user $user = new User([ - 'name' => 'Test Customer', + 'name' => 'Test Customer', 'email' => $email, ]); // Create a mock company $company = new Company([ - 'name' => 'Test Company', + 'name' => 'Test Company', 'public_id' => 'test_company_123', ]); // Create a mock customer $customer = new Contact([ - 'name' => 'Test Customer', + 'name' => 'Test Customer', 'email' => $email, 'phone' => '+1234567890', ]); diff --git a/server/src/Http/Controllers/Api/v1/ContactController.php b/server/src/Http/Controllers/Api/v1/ContactController.php index 3925804a..568b489f 100644 --- a/server/src/Http/Controllers/Api/v1/ContactController.php +++ b/server/src/Http/Controllers/Api/v1/ContactController.php @@ -8,7 +8,6 @@ use Fleetbase\FleetOps\Http\Resources\v1\DeletedResource; use Fleetbase\FleetOps\Models\Contact; use Fleetbase\Http\Controllers\Controller; -use Fleetbase\Models\File; use Fleetbase\Models\User; use Fleetbase\Support\Utils; use Illuminate\Http\Request; @@ -29,24 +28,13 @@ public function create(CreateContactRequest $request) $input['phone'] = is_string($input['phone']) ? Utils::formatPhoneNumber($input['phone']) : $input['phone']; $input['type'] = empty($input['type']) ? 'contact' : $input['type']; - // Handle photo as either file id/ or base64 data string - $photo = $request->input('photo'); - if ($photo) { - // Handle photo being a file id - if (Utils::isPublicId($photo)) { - $file = File::where('public_id', $photo)->first(); - if ($file) { - $input['photo_uuid'] = $file->uuid; - } - } + // Handle photo upload using FileResolverService + if ($request->has('photo')) { + $path = 'uploads/' . session('company') . '/contacts'; + $file = app(\Fleetbase\Services\FileResolverService::class)->resolve($request->input('photo'), $path); - // Handle the photo being base64 data string - if (Utils::isBase64String($photo)) { - $path = implode('/', ['uploads', session('company'), 'contacts']); - $file = File::createFromBase64($photo, null, $path); - if ($file) { - $input['photo_uuid'] = $file->uuid; - } + if ($file) { + $input['photo_uuid'] = $file->uuid; } } @@ -101,29 +89,20 @@ public function update($id, UpdateContactRequest $request) ]); } - // Handle photo as either file id/ or base64 data string - $photo = $request->input('photo'); - if ($photo) { - // Handle photo being a file id - if (Utils::isPublicId($photo)) { - $file = File::where('public_id', $photo)->first(); - if ($file) { - $input['photo_uuid'] = $file->uuid; - } - } - - // Handle the photo being base64 data string - if (Utils::isBase64String($photo)) { - $path = implode('/', ['uploads', session('company'), 'customers']); - $file = File::createFromBase64($photo, null, $path); - if ($file) { - $input['photo_uuid'] = $file->uuid; - } - } + // Handle photo upload using FileResolverService + if ($request->has('photo')) { + $photo = $request->input('photo'); // Handle removal key if ($photo === 'REMOVE') { $input['photo_uuid'] = null; + } else { + $path = 'uploads/' . session('company') . '/contacts'; + $file = app(\Fleetbase\Services\FileResolverService::class)->resolve($photo, $path); + + if ($file) { + $input['photo_uuid'] = $file->uuid; + } } } diff --git a/server/src/Http/Controllers/Api/v1/DriverController.php b/server/src/Http/Controllers/Api/v1/DriverController.php index c7c358e3..62c1f6f0 100644 --- a/server/src/Http/Controllers/Api/v1/DriverController.php +++ b/server/src/Http/Controllers/Api/v1/DriverController.php @@ -20,7 +20,6 @@ use Fleetbase\LaravelMysqlSpatial\Types\Point; use Fleetbase\Models\Company; use Fleetbase\Models\CompanyUser; -use Fleetbase\Models\File; use Fleetbase\Models\User; use Fleetbase\Models\UserDevice; use Fleetbase\Models\VerificationCode; @@ -124,20 +123,10 @@ public function create(CreateDriverRequest $request) // create the driver $driver = Driver::create($input); - // Handle photo as either file id/ or base64 data string - $photo = $request->input('photo'); - if ($photo) { - $file = null; - // Handle photo being a file id - if (Utils::isPublicId($photo)) { - $file = File::where('public_id', $photo)->first(); - } - - // Handle the photo being base64 data string - if (Utils::isBase64String($photo)) { - $path = implode('/', ['uploads', session('company'), 'drivers']); - $file = File::createFromBase64($photo, null, $path); - } + // Handle photo upload using FileResolverService + if ($request->has('photo')) { + $path = 'uploads/' . $company->uuid . '/drivers'; + $file = app(\Fleetbase\Services\FileResolverService::class)->resolve($request->input('photo'), $path); if ($file) { $user->update(['photo_uuid' => $file->uuid]); @@ -218,20 +207,10 @@ public function update($id, UpdateDriverRequest $request) $driver->update($input); $driver->flushAttributesCache(); - // Handle photo as either file id/ or base64 data string - $photo = $request->input('photo'); - if ($photo) { - $file = null; - // Handle photo being a file id - if (Utils::isPublicId($photo)) { - $file = File::where('public_id', $photo)->first(); - } - - // Handle the photo being base64 data string - if (Utils::isBase64String($photo)) { - $path = implode('/', ['uploads', session('company'), 'drivers']); - $file = File::createFromBase64($photo, null, $path); - } + // Handle photo upload using FileResolverService + if ($request->has('photo')) { + $path = 'uploads/' . session('company') . '/drivers'; + $file = app(\Fleetbase\Services\FileResolverService::class)->resolve($request->input('photo'), $path); if ($file) { $driver->user->update(['photo_uuid' => $file->uuid]); diff --git a/server/src/Http/Controllers/Api/v1/OrderController.php b/server/src/Http/Controllers/Api/v1/OrderController.php index 74ce7916..132a5d2b 100644 --- a/server/src/Http/Controllers/Api/v1/OrderController.php +++ b/server/src/Http/Controllers/Api/v1/OrderController.php @@ -352,7 +352,7 @@ public function update($id, UpdateOrderRequest $request) // find for the order try { - $order = Order::findRecordOrFail($id); + $order = Order::findRecordOrFail($id, ['trackingNumber', 'driverAssigned', 'purchaseRate', 'customer', 'facilitator']); } catch (ModelNotFoundException $exception) { return response()->json( [ @@ -518,6 +518,9 @@ public function update($id, UpdateOrderRequest $request) $order->update($input); $order->flushAttributesCache(); + // load required relations + $order->load(['trackingNumber', 'driverAssigned', 'purchaseRate', 'customer', 'facilitator']); + // response the order resource return new OrderResource($order); } @@ -725,7 +728,7 @@ public function find($id, Request $request) { // find for the order try { - $order = Order::findRecordOrFail($id); + $order = Order::findRecordOrFail($id, ['trackingNumber', 'driverAssigned', 'purchaseRate', 'customer', 'facilitator']); } catch (ModelNotFoundException $exception) { return response()->json( [ @@ -805,7 +808,7 @@ public function getDistanceMatrix(string $id) public function dispatchOrder(string $id) { try { - $order = Order::findRecordOrFail($id); + $order = Order::findRecordOrFail($id, ['trackingNumber', 'driverAssigned', 'purchaseRate', 'customer', 'facilitator']); } catch (ModelNotFoundException $exception) { return response()->json( [ @@ -887,7 +890,7 @@ public function startOrder(string $id, Request $request) $assignAdhocDriver = $request->input('assign'); try { - $order = Order::findRecordOrFail($id, ['payload.waypoints'], []); + $order = Order::findRecordOrFail($id, ['payload.waypoints', 'driverAssigned'], []); } catch (ModelNotFoundException $exception) { return response()->json( [ @@ -1220,14 +1223,11 @@ public function cancelOrder(string $id) public function setDestination(string $id, string $placeId) { try { - $order = Order::findRecordOrFail($id); + $order = Order::findRecordOrFail($id, ['payload.waypoints', 'payload.pickup', 'payload.dropoff', 'driverAssigned']); } catch (ModelNotFoundException $exception) { return response()->apiError('Order resource not found.', 404); } - // Load required relations - $order->loadMissing(['payload.waypoints', 'payload.pickup', 'payload.dropoff']); - // Get the order payload $payload = $order->payload; diff --git a/server/src/Http/Controllers/Api/v1/VehicleController.php b/server/src/Http/Controllers/Api/v1/VehicleController.php index 2a1c244f..07b00b64 100644 --- a/server/src/Http/Controllers/Api/v1/VehicleController.php +++ b/server/src/Http/Controllers/Api/v1/VehicleController.php @@ -27,7 +27,7 @@ public function create(CreateVehicleRequest $request) { // get request input $input = $request->only(['status', 'make', 'model', 'year', 'trim', 'type', 'plate_number', 'vin', 'meta', 'online', 'location', 'altitude', 'heading', 'speed']); - + // make sure company is set $input['company_uuid'] = session('company'); diff --git a/server/src/Http/Controllers/Internal/v1/DriverController.php b/server/src/Http/Controllers/Internal/v1/DriverController.php index 1daa9768..6e7968e3 100644 --- a/server/src/Http/Controllers/Internal/v1/DriverController.php +++ b/server/src/Http/Controllers/Internal/v1/DriverController.php @@ -136,7 +136,7 @@ function (&$request, &$input) { if ($input->has('user_uuid')) { $user = User::where('uuid', $input->get('user_uuid'))->first(); - + // If user doesn't exist with provided UUID, create new user if (!$user) { $userInput = $input diff --git a/server/src/Http/Requests/Internal/CreateDriverRequest.php b/server/src/Http/Requests/Internal/CreateDriverRequest.php index 08c4a48d..59deb2ad 100644 --- a/server/src/Http/Requests/Internal/CreateDriverRequest.php +++ b/server/src/Http/Requests/Internal/CreateDriverRequest.php @@ -26,8 +26,8 @@ public function authorize() */ public function rules() { - $isCreating = $this->isMethod('POST'); - $isCreatingWithUser = $this->filled('driver.user_uuid'); + $isCreating = $this->isMethod('POST'); + $isCreatingWithUser = $this->filled('driver.user_uuid'); $shouldValidateUserAttributes = $isCreating && !$isCreatingWithUser; return [ @@ -36,13 +36,13 @@ public function rules() 'email' => [ Rule::requiredIf($shouldValidateUserAttributes), Rule::when($this->filled('email'), ['email']), - Rule::when($shouldValidateUserAttributes, [Rule::unique('users')->whereNull('deleted_at')]) + Rule::when($shouldValidateUserAttributes, [Rule::unique('users')->whereNull('deleted_at')]), ], 'phone' => [ Rule::requiredIf($shouldValidateUserAttributes), - Rule::when($shouldValidateUserAttributes, [Rule::unique('users')->whereNull('deleted_at')]) + Rule::when($shouldValidateUserAttributes, [Rule::unique('users')->whereNull('deleted_at')]), ], - + // Optional fields 'password' => 'nullable|string|min:8', 'drivers_license_number' => 'nullable|string|max:255', @@ -56,7 +56,7 @@ public function rules() 'location' => ['nullable', new ResolvablePoint()], 'latitude' => ['nullable', 'required_with:longitude', 'numeric'], 'longitude' => ['nullable', 'required_with:latitude', 'numeric'], - + // Photo/avatar 'photo_uuid' => 'nullable|exists:files,uuid', 'avatar_uuid' => 'nullable|exists:files,uuid', diff --git a/server/src/Http/Resources/v1/Driver.php b/server/src/Http/Resources/v1/Driver.php index 016bb5d1..44c110d8 100644 --- a/server/src/Http/Resources/v1/Driver.php +++ b/server/src/Http/Resources/v1/Driver.php @@ -50,7 +50,7 @@ public function toArray($request) 'jobs' => $this->whenLoaded('jobs', fn () => $this->getJobs()), 'vendor' => $this->whenLoaded('vendor', fn () => new Vendor($this->vendor)), 'fleets' => $this->whenLoaded('fleets', fn () => Fleet::collection($this->fleets()->without('drivers')->get())), - 'location' => $this->wasRecentlyCreated ? new Point(0, 0) : data_get($this, 'location', new Point(0, 0)), + 'location' => $this->wasRecentlyCreated ? new Point(0, 0) : Utils::castPoint($this->location), 'heading' => (int) data_get($this, 'heading', 0), 'altitude' => (int) data_get($this, 'altitude', 0), 'speed' => (int) data_get($this, 'speed', 0), @@ -96,7 +96,7 @@ public function toWebhookPayload() 'vehicle' => data_get($this, 'vehicle.public_id'), 'current_job' => data_get($this, 'currentJob.public_id'), 'vendor' => data_get($this, 'vendor.public_id'), - 'location' => data_get($this, 'location', new Point(0, 0)), + 'location' => Utils::castPoint($this->location), 'heading' => (int) data_get($this, 'heading', 0), 'altitude' => (int) data_get($this, 'altitude', 0), 'speed' => (int) data_get($this, 'speed', 0), diff --git a/server/src/Http/Resources/v1/Index/Driver.php b/server/src/Http/Resources/v1/Index/Driver.php index 4cd16e28..5a27d023 100644 --- a/server/src/Http/Resources/v1/Index/Driver.php +++ b/server/src/Http/Resources/v1/Index/Driver.php @@ -2,6 +2,7 @@ namespace Fleetbase\FleetOps\Http\Resources\v1\Index; +use Fleetbase\FleetOps\Support\Utils; use Fleetbase\Http\Resources\FleetbaseResource; use Fleetbase\LaravelMysqlSpatial\Types\Point; use Fleetbase\Support\Http; @@ -35,7 +36,7 @@ public function toArray($request): array 'phone' => $this->phone, 'photo_url' => $this->photo_url, 'status' => $this->status, - 'location' => $this->wasRecentlyCreated ? new Point(0, 0) : data_get($this, 'location', new Point(0, 0)), + 'location' => $this->wasRecentlyCreated ? new Point(0, 0) : Utils::castPoint($this->location), 'heading' => (int) data_get($this, 'heading', 0), 'altitude' => (int) data_get($this, 'altitude', 0), 'speed' => (int) data_get($this, 'speed', 0), diff --git a/server/src/Http/Resources/v1/Index/Place.php b/server/src/Http/Resources/v1/Index/Place.php index f8f026fa..4a99451f 100644 --- a/server/src/Http/Resources/v1/Index/Place.php +++ b/server/src/Http/Resources/v1/Index/Place.php @@ -34,7 +34,7 @@ public function toArray($request): array 'city' => $this->city, 'country' => $this->country, 'avatar_url' => $this->avatar_url, - 'location' => Utils::getPointFromMixed($this->location), + 'location' => Utils::castPoint($this->location), // Meta flag to indicate this is an index resource 'meta' => [ diff --git a/server/src/Http/Resources/v1/Index/Vehicle.php b/server/src/Http/Resources/v1/Index/Vehicle.php index 10aeb744..a08946e9 100644 --- a/server/src/Http/Resources/v1/Index/Vehicle.php +++ b/server/src/Http/Resources/v1/Index/Vehicle.php @@ -2,8 +2,8 @@ namespace Fleetbase\FleetOps\Http\Resources\v1\Index; +use Fleetbase\FleetOps\Support\Utils; use Fleetbase\Http\Resources\FleetbaseResource; -use Fleetbase\LaravelMysqlSpatial\Types\Point; use Fleetbase\Support\Http; /** @@ -35,7 +35,7 @@ public function toArray($request): array 'year' => $this->year, 'photo_url' => $this->photo_url, 'status' => $this->status, - 'location' => data_get($this, 'location', new Point(0, 0)), + 'location' => Utils::castPoint($this->location), 'heading' => (int) data_get($this, 'heading', 0), 'altitude' => (int) data_get($this, 'altitude', 0), 'speed' => (int) data_get($this, 'speed', 0), diff --git a/server/src/Http/Resources/v1/Place.php b/server/src/Http/Resources/v1/Place.php index d54173fb..9bab2be6 100644 --- a/server/src/Http/Resources/v1/Place.php +++ b/server/src/Http/Resources/v1/Place.php @@ -28,7 +28,7 @@ public function toArray($request) 'owner_uuid' => $this->when(Http::isInternalRequest(), $this->owner_uuid), 'owner_type' => $this->when(Http::isInternalRequest(), $this->owner_type ? Utils::toEmberResourceType($this->owner_type) : null), 'name' => $this->name, - 'location' => Utils::getPointFromMixed($this->location), + 'location' => Utils::castPoint($this->location), 'address' => $this->address, 'address_html' => $this->when(Http::isInternalRequest(), $this->address_html), 'avatar_url' => $this->avatar_url, diff --git a/server/src/Http/Resources/v1/Vehicle.php b/server/src/Http/Resources/v1/Vehicle.php index ec5b8cf4..69162474 100644 --- a/server/src/Http/Resources/v1/Vehicle.php +++ b/server/src/Http/Resources/v1/Vehicle.php @@ -4,7 +4,6 @@ use Fleetbase\FleetOps\Support\Utils; use Fleetbase\Http\Resources\FleetbaseResource; -use Fleetbase\LaravelMysqlSpatial\Types\Point; use Fleetbase\Support\Http; class Vehicle extends FleetbaseResource @@ -132,7 +131,7 @@ public function toArray($request) 'updated_at' => $this->updated_at, 'created_at' => $this->created_at, // Location & telematics - 'location' => data_get($this, 'location', new Point(0, 0)), + 'location' => Utils::castPoint($this->location), 'heading' => (int) data_get($this, 'heading', 0), 'altitude' => (int) data_get($this, 'altitude', 0), 'speed' => (int) data_get($this, 'speed', 0), @@ -248,7 +247,7 @@ public function toWebhookPayload() 'updated_at' => $this->updated_at, 'created_at' => $this->created_at, // Location & telematics - 'location' => data_get($this, 'location', new Point(0, 0)), + 'location' => Utils::castPoint($this->location), 'heading' => (int) data_get($this, 'heading', 0), 'altitude' => (int) data_get($this, 'altitude', 0), 'speed' => (int) data_get($this, 'speed', 0), diff --git a/server/src/Http/Resources/v1/VehicleWithoutDriver.php b/server/src/Http/Resources/v1/VehicleWithoutDriver.php index cc9436b7..f13a3e54 100644 --- a/server/src/Http/Resources/v1/VehicleWithoutDriver.php +++ b/server/src/Http/Resources/v1/VehicleWithoutDriver.php @@ -4,7 +4,6 @@ use Fleetbase\FleetOps\Support\Utils; use Fleetbase\Http\Resources\FleetbaseResource; -use Fleetbase\LaravelMysqlSpatial\Types\Point; use Fleetbase\Support\Http; class VehicleWithoutDriver extends FleetbaseResource @@ -131,7 +130,7 @@ public function toArray($request) 'updated_at' => $this->updated_at, 'created_at' => $this->created_at, // Location & telematics - 'location' => data_get($this, 'location', new Point(0, 0)), + 'location' => Utils::castPoint($this->location), 'heading' => (int) data_get($this, 'heading', 0), 'altitude' => (int) data_get($this, 'altitude', 0), 'speed' => (int) data_get($this, 'speed', 0), @@ -245,7 +244,7 @@ public function toWebhookPayload() 'updated_at' => $this->updated_at, 'created_at' => $this->created_at, // Location & telematics - 'location' => data_get($this, 'location', new Point(0, 0)), + 'location' => Utils::castPoint($this->location), 'heading' => (int) data_get($this, 'heading', 0), 'altitude' => (int) data_get($this, 'altitude', 0), 'speed' => (int) data_get($this, 'speed', 0), diff --git a/server/src/Models/Payload.php b/server/src/Models/Payload.php index aa8d2ac9..db9f876e 100644 --- a/server/src/Models/Payload.php +++ b/server/src/Models/Payload.php @@ -245,6 +245,19 @@ public function setEntities($entities = []) } } + // Handle entity photo upload + if (isset($attributes['photo'])) { + $path = 'uploads/' . session('company') . '/entities'; + $file = app(\Fleetbase\Services\FileResolverService::class)->resolve($attributes['photo'], $path); + + if ($file) { + $attributes['photo_uuid'] = $file->uuid; + } + + // Clean up raw photo data + unset($attributes['photo']); + } + $entity = new Entity($attributes); $this->entities()->save($entity); } @@ -286,6 +299,19 @@ public function insertEntities($entities = []) } } + // Handle entity photo upload + if (isset($attributes['photo'])) { + $path = 'uploads/' . session('company') . '/entities'; + $file = app(\Fleetbase\Services\FileResolverService::class)->resolve($attributes['photo'], $path); + + if ($file) { + $attributes['photo_uuid'] = $file->uuid; + } + + // Clean up raw photo data + unset($attributes['photo']); + } + Entity::insertGetUuid($attributes, $this); } diff --git a/server/src/Models/ServiceRate.php b/server/src/Models/ServiceRate.php index 44324670..4e7053d8 100644 --- a/server/src/Models/ServiceRate.php +++ b/server/src/Models/ServiceRate.php @@ -12,7 +12,6 @@ use Fleetbase\Traits\HasUuid; use Fleetbase\Traits\SendsWebhooks; use Fleetbase\Traits\TracksApiCredential; -use Illuminate\Support\Facades\DB; class ServiceRate extends Model { @@ -469,12 +468,11 @@ public static function getServicableForWaypoints($waypoints = [], ?\Closure $que */ public static function getServicableForPlaces($places = [], $service = null, $currency = null, ?\Closure $queryCallback = null): array { - $reader = new GeoJSONReader(); - $applicableServiceRates = []; - $serviceRatesQuery = static::with(['zone', 'serviceArea', 'rateFees', 'parcelFees']); + $reader = new GeoJSONReader(); + $serviceRatesQuery = static::with(['zone', 'serviceArea', 'rateFees', 'parcelFees']); if ($currency) { - $serviceRatesQuery->where(DB::raw('lower(currency)'), strtolower($currency)); + $serviceRatesQuery->whereRaw('lower(currency) = ?', [strtolower($currency)]); } if ($service) { @@ -487,44 +485,115 @@ public static function getServicableForPlaces($places = [], $service = null, $cu $serviceRates = $serviceRatesQuery->get(); - $waypoints = collect($places)->map(function ($place) { - $place = Place::createFromMixed($place); + $waypoints = collect($places) + ->map(function ($place) { + $place = Place::createFromMixed($place); + + if (!$place instanceof Place) { + return null; + } - if ($place instanceof Place) { $point = $place->getLocationAsPoint(); - // Conver to brick gis point - return \Brick\Geo\Point::fromText(sprintf('POINT (%F %F)', $point->getLng(), $point->getLat()), 4326); + // Brick point: X=lng, Y=lat (WKT order) + return \Brick\Geo\Point::fromText( + sprintf('POINT (%F %F)', $point->getLng(), $point->getLat()), + 4326 + ); + }) + ->filter() + ->values(); + + if ($waypoints->isEmpty()) { + return []; + } + + /** + * Convert a casted spatial geometry (Zone::border / ServiceArea::border) + * into a Brick geometry using GeoJSONReader. + */ + $toBrickGeometry = function ($spatialGeometry) use ($reader) { + if (!$spatialGeometry) { + return null; + } + + // Most Fleetbase spatial casts/types implement toJson() + if (is_object($spatialGeometry) && method_exists($spatialGeometry, 'toJson')) { + $json = $spatialGeometry->toJson(); + $json = is_string($json) ? trim($json) : null; + + if ($json) { + return $reader->read($json); + } + + return null; + } + + // Fallback if the cast ever returns array/object + if (is_array($spatialGeometry) || is_object($spatialGeometry)) { + $json = json_encode($spatialGeometry, JSON_UNESCAPED_UNICODE); + if ($json && $json !== 'null') { + return $reader->read($json); + } + } + + // Fallback if it’s a raw JSON string + if (is_string($spatialGeometry) && trim($spatialGeometry) !== '') { + return $reader->read($spatialGeometry); + } + + return null; + }; + + /** + * Ensure ALL waypoints are inside the given Brick geometry. + */ + $containsAllWaypoints = function ($brickGeometry) use ($waypoints): bool { + if (!$brickGeometry) { + return false; + } + + foreach ($waypoints as $waypoint) { + if (!$brickGeometry->contains($waypoint)) { + return false; + } } - }); + + return true; + }; + + $applicableServiceRates = []; foreach ($serviceRates as $serviceRate) { + // If a service area exists, all waypoints must be inside its border if ($serviceRate->hasServiceArea()) { - // make sure all waypoints fall within the service area - foreach ($serviceRate->serviceArea->border as $polygon) { - $polygon = $reader->read($polygon->toJson()); - - /** @var \Brick\Geo\Point $waypoint */ - foreach ($waypoints as $waypoint) { - if (!$polygon->contains($waypoint)) { - // waypoint outside of service area, not applicable to route - continue; - } - } + $serviceAreaBorder = $serviceRate->serviceArea?->border; + + $serviceAreaGeom = null; + try { + $serviceAreaGeom = $toBrickGeometry($serviceAreaBorder); + } catch (\Throwable $e) { + continue; // invalid geojson / geometry -> reject this rate + } + + if (!$containsAllWaypoints($serviceAreaGeom)) { + continue; } } + // If a zone exists, all waypoints must be inside its border if ($serviceRate->hasZone()) { - // make sure all waypoints fall within the service area - foreach ($serviceRate->zone->border as $polygon) { - $polygon = $reader->read($polygon->toJson()); + $zoneBorder = $serviceRate->zone?->border; - foreach ($waypoints as $waypoint) { - if (!$polygon->contains($waypoint)) { - // waypoint outside of zone, not applicable to route - continue; - } - } + $zoneGeom = null; + try { + $zoneGeom = $toBrickGeometry($zoneBorder); + } catch (\Throwable $e) { + continue; + } + + if (!$containsAllWaypoints($zoneGeom)) { + continue; } } @@ -933,29 +1002,23 @@ public function quote(Payload $payload) */ public function findServiceRateFeeByDistance(int $totalDistance): ?ServiceRateFee { - $this->load('rateFees'); + $this->loadMissing('rateFees'); - $distanceInKms = round($totalDistance / 1000); - $distanceFee = null; + // Convert meters to kilometers WITHOUT rounding up + $distanceInKm = $totalDistance / 1000; - foreach ($this->rateFees as $rateFee) { - $previousRateFee = $rateFee; + // Ensure predictable order + $rateFees = $this->rateFees->sortBy('distance'); - if ($distanceInKms > $rateFee->distance) { - continue; - } elseif ($rateFee->distance > $distanceInKms) { - $distanceFee = $previousRateFee; - } else { - $distanceFee = $rateFee; + // Find the first tier that covers the distance + foreach ($rateFees as $rateFee) { + if ($distanceInKm <= $rateFee->distance) { + return $rateFee; } } - // if no distance fee use the last - if ($distanceFee === null) { - $distanceFee = $this->rateFees->sortByDesc('distance')->first(); - } - - return $distanceFee; + // If distance exceeds all tiers, use the largest tier + return $rateFees->last(); } /** diff --git a/server/src/Support/Utils.php b/server/src/Support/Utils.php index dc73a6d2..7201c2ce 100644 --- a/server/src/Support/Utils.php +++ b/server/src/Support/Utils.php @@ -334,6 +334,24 @@ public static function getPointFromMixed($coordinates): ?Point return new Point((float) $latitude, (float) $longitude); } + /** + * Always return spatial point. + * + * @param [type] $mixed + * + * @return void + */ + public static function castPoint($mixed) + { + try { + $point = static::getPointFromMixed($mixed); + + return $point; + } catch (\Throwable $e) { + return new Point(0, 0); + } + } + /** * Determines if the given coordinates strictly represent a Point object. * These will explude resolvable coordinates from records.