@fleetbase/fleetops-engine 0.6.33 → 0.6.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,30 @@
1
1
  import Component from '@glimmer/component';
2
+ import { tracked } from '@glimmer/tracking';
3
+ import { inject as service } from '@ember/service';
4
+ import { task } from 'ember-concurrency-decorators';
2
5
 
3
- export default class OrderDetailsProofComponent extends Component {}
6
+ export default class OrderDetailsProofComponent extends Component {
7
+ @service fetch;
8
+ @tracked proofs = [];
9
+
10
+ constructor(owner, { resource }) {
11
+ super(...arguments);
12
+ this.loadOrderProofs.perform(resource);
13
+ }
14
+
15
+ @task *loadOrderProofs(order) {
16
+ const proofs = yield this.fetch.get(`orders/${order.id}/proofs`);
17
+
18
+ this.proofs = proofs.map((proof) => ({
19
+ ...proof,
20
+ type: this.#getTypeFromRemarks(proof.remarks),
21
+ }));
22
+ }
23
+
24
+ #getTypeFromRemarks(remarks = '') {
25
+ if (remarks.endsWith('Photo')) return 'photo';
26
+ if (remarks.endsWith('Scan')) return 'scan';
27
+ if (remarks.endsWith('Signature')) return 'signature';
28
+ return undefined;
29
+ }
30
+ }
package/composer.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fleetbase/fleetops-api",
3
- "version": "0.6.33",
3
+ "version": "0.6.34",
4
4
  "description": "Fleet & Transport Management Extension for Fleetbase",
5
5
  "keywords": [
6
6
  "fleetbase-extension",
package/extension.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "Fleet-Ops",
3
- "version": "0.6.33",
3
+ "version": "0.6.34",
4
4
  "description": "Fleet & Transport Management Extension for Fleetbase",
5
5
  "repository": "https://github.com/fleetbase/fleetops",
6
6
  "license": "AGPL-3.0-or-later",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fleetbase/fleetops-engine",
3
- "version": "0.6.33",
3
+ "version": "0.6.34",
4
4
  "description": "Fleet & Transport Management Extension for Fleetbase",
5
5
  "fleetbase": {
6
6
  "route": "fleet-ops"
@@ -33,7 +33,7 @@ class TestEmail extends Command
33
33
  public function handle()
34
34
  {
35
35
  $email = $this->argument('email');
36
- $type = $this->option('type');
36
+ $type = $this->option('type');
37
37
 
38
38
  $this->info('Sending test email...');
39
39
  $this->info("Type: {$type}");
@@ -47,40 +47,40 @@ class TestEmail extends Command
47
47
 
48
48
  default:
49
49
  $this->error("Unknown email type: {$type}");
50
+
50
51
  return Command::FAILURE;
51
52
  }
52
53
 
53
54
  $this->info('✓ Test email sent successfully!');
55
+
54
56
  return Command::SUCCESS;
55
57
  } catch (\Exception $e) {
56
58
  $this->error('Failed to send test email: ' . $e->getMessage());
59
+
57
60
  return Command::FAILURE;
58
61
  }
59
62
  }
60
63
 
61
64
  /**
62
65
  * Send a test customer credentials email.
63
- *
64
- * @param string $email
65
- * @return void
66
66
  */
67
67
  private function sendCustomerCredentialsEmail(string $email): void
68
68
  {
69
69
  // Create a mock user
70
70
  $user = new User([
71
- 'name' => 'Test Customer',
71
+ 'name' => 'Test Customer',
72
72
  'email' => $email,
73
73
  ]);
74
74
 
75
75
  // Create a mock company
76
76
  $company = new Company([
77
- 'name' => 'Test Company',
77
+ 'name' => 'Test Company',
78
78
  'public_id' => 'test_company_123',
79
79
  ]);
80
80
 
81
81
  // Create a mock customer
82
82
  $customer = new Contact([
83
- 'name' => 'Test Customer',
83
+ 'name' => 'Test Customer',
84
84
  'email' => $email,
85
85
  'phone' => '+1234567890',
86
86
  ]);
@@ -8,7 +8,6 @@ use Fleetbase\FleetOps\Http\Resources\v1\Contact as ContactResource;
8
8
  use Fleetbase\FleetOps\Http\Resources\v1\DeletedResource;
9
9
  use Fleetbase\FleetOps\Models\Contact;
10
10
  use Fleetbase\Http\Controllers\Controller;
11
- use Fleetbase\Models\File;
12
11
  use Fleetbase\Models\User;
13
12
  use Fleetbase\Support\Utils;
14
13
  use Illuminate\Http\Request;
@@ -29,24 +28,13 @@ class ContactController extends Controller
29
28
  $input['phone'] = is_string($input['phone']) ? Utils::formatPhoneNumber($input['phone']) : $input['phone'];
30
29
  $input['type'] = empty($input['type']) ? 'contact' : $input['type'];
31
30
 
32
- // Handle photo as either file id/ or base64 data string
33
- $photo = $request->input('photo');
34
- if ($photo) {
35
- // Handle photo being a file id
36
- if (Utils::isPublicId($photo)) {
37
- $file = File::where('public_id', $photo)->first();
38
- if ($file) {
39
- $input['photo_uuid'] = $file->uuid;
40
- }
41
- }
31
+ // Handle photo upload using FileResolverService
32
+ if ($request->has('photo')) {
33
+ $path = 'uploads/' . session('company') . '/contacts';
34
+ $file = app(\Fleetbase\Services\FileResolverService::class)->resolve($request->input('photo'), $path);
42
35
 
43
- // Handle the photo being base64 data string
44
- if (Utils::isBase64String($photo)) {
45
- $path = implode('/', ['uploads', session('company'), 'contacts']);
46
- $file = File::createFromBase64($photo, null, $path);
47
- if ($file) {
48
- $input['photo_uuid'] = $file->uuid;
49
- }
36
+ if ($file) {
37
+ $input['photo_uuid'] = $file->uuid;
50
38
  }
51
39
  }
52
40
 
@@ -101,29 +89,20 @@ class ContactController extends Controller
101
89
  ]);
102
90
  }
103
91
 
104
- // Handle photo as either file id/ or base64 data string
105
- $photo = $request->input('photo');
106
- if ($photo) {
107
- // Handle photo being a file id
108
- if (Utils::isPublicId($photo)) {
109
- $file = File::where('public_id', $photo)->first();
110
- if ($file) {
111
- $input['photo_uuid'] = $file->uuid;
112
- }
113
- }
114
-
115
- // Handle the photo being base64 data string
116
- if (Utils::isBase64String($photo)) {
117
- $path = implode('/', ['uploads', session('company'), 'customers']);
118
- $file = File::createFromBase64($photo, null, $path);
119
- if ($file) {
120
- $input['photo_uuid'] = $file->uuid;
121
- }
122
- }
92
+ // Handle photo upload using FileResolverService
93
+ if ($request->has('photo')) {
94
+ $photo = $request->input('photo');
123
95
 
124
96
  // Handle removal key
125
97
  if ($photo === 'REMOVE') {
126
98
  $input['photo_uuid'] = null;
99
+ } else {
100
+ $path = 'uploads/' . session('company') . '/contacts';
101
+ $file = app(\Fleetbase\Services\FileResolverService::class)->resolve($photo, $path);
102
+
103
+ if ($file) {
104
+ $input['photo_uuid'] = $file->uuid;
105
+ }
127
106
  }
128
107
  }
129
108
 
@@ -20,7 +20,6 @@ use Fleetbase\Http\Resources\Organization;
20
20
  use Fleetbase\LaravelMysqlSpatial\Types\Point;
21
21
  use Fleetbase\Models\Company;
22
22
  use Fleetbase\Models\CompanyUser;
23
- use Fleetbase\Models\File;
24
23
  use Fleetbase\Models\User;
25
24
  use Fleetbase\Models\UserDevice;
26
25
  use Fleetbase\Models\VerificationCode;
@@ -124,20 +123,10 @@ class DriverController extends Controller
124
123
  // create the driver
125
124
  $driver = Driver::create($input);
126
125
 
127
- // Handle photo as either file id/ or base64 data string
128
- $photo = $request->input('photo');
129
- if ($photo) {
130
- $file = null;
131
- // Handle photo being a file id
132
- if (Utils::isPublicId($photo)) {
133
- $file = File::where('public_id', $photo)->first();
134
- }
135
-
136
- // Handle the photo being base64 data string
137
- if (Utils::isBase64String($photo)) {
138
- $path = implode('/', ['uploads', session('company'), 'drivers']);
139
- $file = File::createFromBase64($photo, null, $path);
140
- }
126
+ // Handle photo upload using FileResolverService
127
+ if ($request->has('photo')) {
128
+ $path = 'uploads/' . $company->uuid . '/drivers';
129
+ $file = app(\Fleetbase\Services\FileResolverService::class)->resolve($request->input('photo'), $path);
141
130
 
142
131
  if ($file) {
143
132
  $user->update(['photo_uuid' => $file->uuid]);
@@ -218,20 +207,10 @@ class DriverController extends Controller
218
207
  $driver->update($input);
219
208
  $driver->flushAttributesCache();
220
209
 
221
- // Handle photo as either file id/ or base64 data string
222
- $photo = $request->input('photo');
223
- if ($photo) {
224
- $file = null;
225
- // Handle photo being a file id
226
- if (Utils::isPublicId($photo)) {
227
- $file = File::where('public_id', $photo)->first();
228
- }
229
-
230
- // Handle the photo being base64 data string
231
- if (Utils::isBase64String($photo)) {
232
- $path = implode('/', ['uploads', session('company'), 'drivers']);
233
- $file = File::createFromBase64($photo, null, $path);
234
- }
210
+ // Handle photo upload using FileResolverService
211
+ if ($request->has('photo')) {
212
+ $path = 'uploads/' . session('company') . '/drivers';
213
+ $file = app(\Fleetbase\Services\FileResolverService::class)->resolve($request->input('photo'), $path);
235
214
 
236
215
  if ($file) {
237
216
  $driver->user->update(['photo_uuid' => $file->uuid]);
@@ -352,7 +352,7 @@ class OrderController extends Controller
352
352
 
353
353
  // find for the order
354
354
  try {
355
- $order = Order::findRecordOrFail($id);
355
+ $order = Order::findRecordOrFail($id, ['trackingNumber', 'driverAssigned', 'purchaseRate', 'customer', 'facilitator']);
356
356
  } catch (ModelNotFoundException $exception) {
357
357
  return response()->json(
358
358
  [
@@ -518,6 +518,9 @@ class OrderController extends Controller
518
518
  $order->update($input);
519
519
  $order->flushAttributesCache();
520
520
 
521
+ // load required relations
522
+ $order->load(['trackingNumber', 'driverAssigned', 'purchaseRate', 'customer', 'facilitator']);
523
+
521
524
  // response the order resource
522
525
  return new OrderResource($order);
523
526
  }
@@ -725,7 +728,7 @@ class OrderController extends Controller
725
728
  {
726
729
  // find for the order
727
730
  try {
728
- $order = Order::findRecordOrFail($id);
731
+ $order = Order::findRecordOrFail($id, ['trackingNumber', 'driverAssigned', 'purchaseRate', 'customer', 'facilitator']);
729
732
  } catch (ModelNotFoundException $exception) {
730
733
  return response()->json(
731
734
  [
@@ -805,7 +808,7 @@ class OrderController extends Controller
805
808
  public function dispatchOrder(string $id)
806
809
  {
807
810
  try {
808
- $order = Order::findRecordOrFail($id);
811
+ $order = Order::findRecordOrFail($id, ['trackingNumber', 'driverAssigned', 'purchaseRate', 'customer', 'facilitator']);
809
812
  } catch (ModelNotFoundException $exception) {
810
813
  return response()->json(
811
814
  [
@@ -887,7 +890,7 @@ class OrderController extends Controller
887
890
  $assignAdhocDriver = $request->input('assign');
888
891
 
889
892
  try {
890
- $order = Order::findRecordOrFail($id, ['payload.waypoints'], []);
893
+ $order = Order::findRecordOrFail($id, ['payload.waypoints', 'driverAssigned'], []);
891
894
  } catch (ModelNotFoundException $exception) {
892
895
  return response()->json(
893
896
  [
@@ -1220,14 +1223,11 @@ class OrderController extends Controller
1220
1223
  public function setDestination(string $id, string $placeId)
1221
1224
  {
1222
1225
  try {
1223
- $order = Order::findRecordOrFail($id);
1226
+ $order = Order::findRecordOrFail($id, ['payload.waypoints', 'payload.pickup', 'payload.dropoff', 'driverAssigned']);
1224
1227
  } catch (ModelNotFoundException $exception) {
1225
1228
  return response()->apiError('Order resource not found.', 404);
1226
1229
  }
1227
1230
 
1228
- // Load required relations
1229
- $order->loadMissing(['payload.waypoints', 'payload.pickup', 'payload.dropoff']);
1230
-
1231
1231
  // Get the order payload
1232
1232
  $payload = $order->payload;
1233
1233
 
@@ -27,7 +27,7 @@ class VehicleController extends Controller
27
27
  {
28
28
  // get request input
29
29
  $input = $request->only(['status', 'make', 'model', 'year', 'trim', 'type', 'plate_number', 'vin', 'meta', 'online', 'location', 'altitude', 'heading', 'speed']);
30
-
30
+
31
31
  // make sure company is set
32
32
  $input['company_uuid'] = session('company');
33
33
 
@@ -136,7 +136,7 @@ class DriverController extends FleetOpsController
136
136
 
137
137
  if ($input->has('user_uuid')) {
138
138
  $user = User::where('uuid', $input->get('user_uuid'))->first();
139
-
139
+
140
140
  // If user doesn't exist with provided UUID, create new user
141
141
  if (!$user) {
142
142
  $userInput = $input
@@ -26,8 +26,8 @@ class CreateDriverRequest extends CreateDriverApiRequest
26
26
  */
27
27
  public function rules()
28
28
  {
29
- $isCreating = $this->isMethod('POST');
30
- $isCreatingWithUser = $this->filled('driver.user_uuid');
29
+ $isCreating = $this->isMethod('POST');
30
+ $isCreatingWithUser = $this->filled('driver.user_uuid');
31
31
  $shouldValidateUserAttributes = $isCreating && !$isCreatingWithUser;
32
32
 
33
33
  return [
@@ -36,13 +36,13 @@ class CreateDriverRequest extends CreateDriverApiRequest
36
36
  'email' => [
37
37
  Rule::requiredIf($shouldValidateUserAttributes),
38
38
  Rule::when($this->filled('email'), ['email']),
39
- Rule::when($shouldValidateUserAttributes, [Rule::unique('users')->whereNull('deleted_at')])
39
+ Rule::when($shouldValidateUserAttributes, [Rule::unique('users')->whereNull('deleted_at')]),
40
40
  ],
41
41
  'phone' => [
42
42
  Rule::requiredIf($shouldValidateUserAttributes),
43
- Rule::when($shouldValidateUserAttributes, [Rule::unique('users')->whereNull('deleted_at')])
43
+ Rule::when($shouldValidateUserAttributes, [Rule::unique('users')->whereNull('deleted_at')]),
44
44
  ],
45
-
45
+
46
46
  // Optional fields
47
47
  'password' => 'nullable|string|min:8',
48
48
  'drivers_license_number' => 'nullable|string|max:255',
@@ -56,7 +56,7 @@ class CreateDriverRequest extends CreateDriverApiRequest
56
56
  'location' => ['nullable', new ResolvablePoint()],
57
57
  'latitude' => ['nullable', 'required_with:longitude', 'numeric'],
58
58
  'longitude' => ['nullable', 'required_with:latitude', 'numeric'],
59
-
59
+
60
60
  // Photo/avatar
61
61
  'photo_uuid' => 'nullable|exists:files,uuid',
62
62
  'avatar_uuid' => 'nullable|exists:files,uuid',
@@ -50,7 +50,7 @@ class Driver extends FleetbaseResource
50
50
  'jobs' => $this->whenLoaded('jobs', fn () => $this->getJobs()),
51
51
  'vendor' => $this->whenLoaded('vendor', fn () => new Vendor($this->vendor)),
52
52
  'fleets' => $this->whenLoaded('fleets', fn () => Fleet::collection($this->fleets()->without('drivers')->get())),
53
- 'location' => $this->wasRecentlyCreated ? new Point(0, 0) : data_get($this, 'location', new Point(0, 0)),
53
+ 'location' => $this->wasRecentlyCreated ? new Point(0, 0) : Utils::castPoint($this->location),
54
54
  'heading' => (int) data_get($this, 'heading', 0),
55
55
  'altitude' => (int) data_get($this, 'altitude', 0),
56
56
  'speed' => (int) data_get($this, 'speed', 0),
@@ -96,7 +96,7 @@ class Driver extends FleetbaseResource
96
96
  'vehicle' => data_get($this, 'vehicle.public_id'),
97
97
  'current_job' => data_get($this, 'currentJob.public_id'),
98
98
  'vendor' => data_get($this, 'vendor.public_id'),
99
- 'location' => data_get($this, 'location', new Point(0, 0)),
99
+ 'location' => Utils::castPoint($this->location),
100
100
  'heading' => (int) data_get($this, 'heading', 0),
101
101
  'altitude' => (int) data_get($this, 'altitude', 0),
102
102
  'speed' => (int) data_get($this, 'speed', 0),
@@ -2,6 +2,7 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Http\Resources\v1\Index;
4
4
 
5
+ use Fleetbase\FleetOps\Support\Utils;
5
6
  use Fleetbase\Http\Resources\FleetbaseResource;
6
7
  use Fleetbase\LaravelMysqlSpatial\Types\Point;
7
8
  use Fleetbase\Support\Http;
@@ -35,7 +36,7 @@ class Driver extends FleetbaseResource
35
36
  'phone' => $this->phone,
36
37
  'photo_url' => $this->photo_url,
37
38
  'status' => $this->status,
38
- 'location' => $this->wasRecentlyCreated ? new Point(0, 0) : data_get($this, 'location', new Point(0, 0)),
39
+ 'location' => $this->wasRecentlyCreated ? new Point(0, 0) : Utils::castPoint($this->location),
39
40
  'heading' => (int) data_get($this, 'heading', 0),
40
41
  'altitude' => (int) data_get($this, 'altitude', 0),
41
42
  'speed' => (int) data_get($this, 'speed', 0),
@@ -34,7 +34,7 @@ class Place extends FleetbaseResource
34
34
  'city' => $this->city,
35
35
  'country' => $this->country,
36
36
  'avatar_url' => $this->avatar_url,
37
- 'location' => Utils::getPointFromMixed($this->location),
37
+ 'location' => Utils::castPoint($this->location),
38
38
 
39
39
  // Meta flag to indicate this is an index resource
40
40
  'meta' => [
@@ -2,8 +2,8 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Http\Resources\v1\Index;
4
4
 
5
+ use Fleetbase\FleetOps\Support\Utils;
5
6
  use Fleetbase\Http\Resources\FleetbaseResource;
6
- use Fleetbase\LaravelMysqlSpatial\Types\Point;
7
7
  use Fleetbase\Support\Http;
8
8
 
9
9
  /**
@@ -35,7 +35,7 @@ class Vehicle extends FleetbaseResource
35
35
  'year' => $this->year,
36
36
  'photo_url' => $this->photo_url,
37
37
  'status' => $this->status,
38
- 'location' => data_get($this, 'location', new Point(0, 0)),
38
+ 'location' => Utils::castPoint($this->location),
39
39
  'heading' => (int) data_get($this, 'heading', 0),
40
40
  'altitude' => (int) data_get($this, 'altitude', 0),
41
41
  'speed' => (int) data_get($this, 'speed', 0),
@@ -28,7 +28,7 @@ class Place extends FleetbaseResource
28
28
  'owner_uuid' => $this->when(Http::isInternalRequest(), $this->owner_uuid),
29
29
  'owner_type' => $this->when(Http::isInternalRequest(), $this->owner_type ? Utils::toEmberResourceType($this->owner_type) : null),
30
30
  'name' => $this->name,
31
- 'location' => Utils::getPointFromMixed($this->location),
31
+ 'location' => Utils::castPoint($this->location),
32
32
  'address' => $this->address,
33
33
  'address_html' => $this->when(Http::isInternalRequest(), $this->address_html),
34
34
  'avatar_url' => $this->avatar_url,
@@ -4,7 +4,6 @@ namespace Fleetbase\FleetOps\Http\Resources\v1;
4
4
 
5
5
  use Fleetbase\FleetOps\Support\Utils;
6
6
  use Fleetbase\Http\Resources\FleetbaseResource;
7
- use Fleetbase\LaravelMysqlSpatial\Types\Point;
8
7
  use Fleetbase\Support\Http;
9
8
 
10
9
  class Vehicle extends FleetbaseResource
@@ -132,7 +131,7 @@ class Vehicle extends FleetbaseResource
132
131
  'updated_at' => $this->updated_at,
133
132
  'created_at' => $this->created_at,
134
133
  // Location & telematics
135
- 'location' => data_get($this, 'location', new Point(0, 0)),
134
+ 'location' => Utils::castPoint($this->location),
136
135
  'heading' => (int) data_get($this, 'heading', 0),
137
136
  'altitude' => (int) data_get($this, 'altitude', 0),
138
137
  'speed' => (int) data_get($this, 'speed', 0),
@@ -248,7 +247,7 @@ class Vehicle extends FleetbaseResource
248
247
  'updated_at' => $this->updated_at,
249
248
  'created_at' => $this->created_at,
250
249
  // Location & telematics
251
- 'location' => data_get($this, 'location', new Point(0, 0)),
250
+ 'location' => Utils::castPoint($this->location),
252
251
  'heading' => (int) data_get($this, 'heading', 0),
253
252
  'altitude' => (int) data_get($this, 'altitude', 0),
254
253
  'speed' => (int) data_get($this, 'speed', 0),
@@ -4,7 +4,6 @@ namespace Fleetbase\FleetOps\Http\Resources\v1;
4
4
 
5
5
  use Fleetbase\FleetOps\Support\Utils;
6
6
  use Fleetbase\Http\Resources\FleetbaseResource;
7
- use Fleetbase\LaravelMysqlSpatial\Types\Point;
8
7
  use Fleetbase\Support\Http;
9
8
 
10
9
  class VehicleWithoutDriver extends FleetbaseResource
@@ -131,7 +130,7 @@ class VehicleWithoutDriver extends FleetbaseResource
131
130
  'updated_at' => $this->updated_at,
132
131
  'created_at' => $this->created_at,
133
132
  // Location & telematics
134
- 'location' => data_get($this, 'location', new Point(0, 0)),
133
+ 'location' => Utils::castPoint($this->location),
135
134
  'heading' => (int) data_get($this, 'heading', 0),
136
135
  'altitude' => (int) data_get($this, 'altitude', 0),
137
136
  'speed' => (int) data_get($this, 'speed', 0),
@@ -245,7 +244,7 @@ class VehicleWithoutDriver extends FleetbaseResource
245
244
  'updated_at' => $this->updated_at,
246
245
  'created_at' => $this->created_at,
247
246
  // Location & telematics
248
- 'location' => data_get($this, 'location', new Point(0, 0)),
247
+ 'location' => Utils::castPoint($this->location),
249
248
  'heading' => (int) data_get($this, 'heading', 0),
250
249
  'altitude' => (int) data_get($this, 'altitude', 0),
251
250
  'speed' => (int) data_get($this, 'speed', 0),
@@ -245,6 +245,19 @@ class Payload extends Model
245
245
  }
246
246
  }
247
247
 
248
+ // Handle entity photo upload
249
+ if (isset($attributes['photo'])) {
250
+ $path = 'uploads/' . session('company') . '/entities';
251
+ $file = app(\Fleetbase\Services\FileResolverService::class)->resolve($attributes['photo'], $path);
252
+
253
+ if ($file) {
254
+ $attributes['photo_uuid'] = $file->uuid;
255
+ }
256
+
257
+ // Clean up raw photo data
258
+ unset($attributes['photo']);
259
+ }
260
+
248
261
  $entity = new Entity($attributes);
249
262
  $this->entities()->save($entity);
250
263
  }
@@ -286,6 +299,19 @@ class Payload extends Model
286
299
  }
287
300
  }
288
301
 
302
+ // Handle entity photo upload
303
+ if (isset($attributes['photo'])) {
304
+ $path = 'uploads/' . session('company') . '/entities';
305
+ $file = app(\Fleetbase\Services\FileResolverService::class)->resolve($attributes['photo'], $path);
306
+
307
+ if ($file) {
308
+ $attributes['photo_uuid'] = $file->uuid;
309
+ }
310
+
311
+ // Clean up raw photo data
312
+ unset($attributes['photo']);
313
+ }
314
+
289
315
  Entity::insertGetUuid($attributes, $this);
290
316
  }
291
317
 
@@ -12,7 +12,6 @@ use Fleetbase\Traits\HasPublicId;
12
12
  use Fleetbase\Traits\HasUuid;
13
13
  use Fleetbase\Traits\SendsWebhooks;
14
14
  use Fleetbase\Traits\TracksApiCredential;
15
- use Illuminate\Support\Facades\DB;
16
15
 
17
16
  class ServiceRate extends Model
18
17
  {
@@ -469,12 +468,11 @@ class ServiceRate extends Model
469
468
  */
470
469
  public static function getServicableForPlaces($places = [], $service = null, $currency = null, ?\Closure $queryCallback = null): array
471
470
  {
472
- $reader = new GeoJSONReader();
473
- $applicableServiceRates = [];
474
- $serviceRatesQuery = static::with(['zone', 'serviceArea', 'rateFees', 'parcelFees']);
471
+ $reader = new GeoJSONReader();
472
+ $serviceRatesQuery = static::with(['zone', 'serviceArea', 'rateFees', 'parcelFees']);
475
473
 
476
474
  if ($currency) {
477
- $serviceRatesQuery->where(DB::raw('lower(currency)'), strtolower($currency));
475
+ $serviceRatesQuery->whereRaw('lower(currency) = ?', [strtolower($currency)]);
478
476
  }
479
477
 
480
478
  if ($service) {
@@ -487,44 +485,115 @@ class ServiceRate extends Model
487
485
 
488
486
  $serviceRates = $serviceRatesQuery->get();
489
487
 
490
- $waypoints = collect($places)->map(function ($place) {
491
- $place = Place::createFromMixed($place);
488
+ $waypoints = collect($places)
489
+ ->map(function ($place) {
490
+ $place = Place::createFromMixed($place);
491
+
492
+ if (!$place instanceof Place) {
493
+ return null;
494
+ }
492
495
 
493
- if ($place instanceof Place) {
494
496
  $point = $place->getLocationAsPoint();
495
497
 
496
- // Conver to brick gis point
497
- return \Brick\Geo\Point::fromText(sprintf('POINT (%F %F)', $point->getLng(), $point->getLat()), 4326);
498
+ // Brick point: X=lng, Y=lat (WKT order)
499
+ return \Brick\Geo\Point::fromText(
500
+ sprintf('POINT (%F %F)', $point->getLng(), $point->getLat()),
501
+ 4326
502
+ );
503
+ })
504
+ ->filter()
505
+ ->values();
506
+
507
+ if ($waypoints->isEmpty()) {
508
+ return [];
509
+ }
510
+
511
+ /**
512
+ * Convert a casted spatial geometry (Zone::border / ServiceArea::border)
513
+ * into a Brick geometry using GeoJSONReader.
514
+ */
515
+ $toBrickGeometry = function ($spatialGeometry) use ($reader) {
516
+ if (!$spatialGeometry) {
517
+ return null;
518
+ }
519
+
520
+ // Most Fleetbase spatial casts/types implement toJson()
521
+ if (is_object($spatialGeometry) && method_exists($spatialGeometry, 'toJson')) {
522
+ $json = $spatialGeometry->toJson();
523
+ $json = is_string($json) ? trim($json) : null;
524
+
525
+ if ($json) {
526
+ return $reader->read($json);
527
+ }
528
+
529
+ return null;
530
+ }
531
+
532
+ // Fallback if the cast ever returns array/object
533
+ if (is_array($spatialGeometry) || is_object($spatialGeometry)) {
534
+ $json = json_encode($spatialGeometry, JSON_UNESCAPED_UNICODE);
535
+ if ($json && $json !== 'null') {
536
+ return $reader->read($json);
537
+ }
538
+ }
539
+
540
+ // Fallback if it’s a raw JSON string
541
+ if (is_string($spatialGeometry) && trim($spatialGeometry) !== '') {
542
+ return $reader->read($spatialGeometry);
543
+ }
544
+
545
+ return null;
546
+ };
547
+
548
+ /**
549
+ * Ensure ALL waypoints are inside the given Brick geometry.
550
+ */
551
+ $containsAllWaypoints = function ($brickGeometry) use ($waypoints): bool {
552
+ if (!$brickGeometry) {
553
+ return false;
554
+ }
555
+
556
+ foreach ($waypoints as $waypoint) {
557
+ if (!$brickGeometry->contains($waypoint)) {
558
+ return false;
559
+ }
498
560
  }
499
- });
561
+
562
+ return true;
563
+ };
564
+
565
+ $applicableServiceRates = [];
500
566
 
501
567
  foreach ($serviceRates as $serviceRate) {
568
+ // If a service area exists, all waypoints must be inside its border
502
569
  if ($serviceRate->hasServiceArea()) {
503
- // make sure all waypoints fall within the service area
504
- foreach ($serviceRate->serviceArea->border as $polygon) {
505
- $polygon = $reader->read($polygon->toJson());
506
-
507
- /** @var \Brick\Geo\Point $waypoint */
508
- foreach ($waypoints as $waypoint) {
509
- if (!$polygon->contains($waypoint)) {
510
- // waypoint outside of service area, not applicable to route
511
- continue;
512
- }
513
- }
570
+ $serviceAreaBorder = $serviceRate->serviceArea?->border;
571
+
572
+ $serviceAreaGeom = null;
573
+ try {
574
+ $serviceAreaGeom = $toBrickGeometry($serviceAreaBorder);
575
+ } catch (\Throwable $e) {
576
+ continue; // invalid geojson / geometry -> reject this rate
577
+ }
578
+
579
+ if (!$containsAllWaypoints($serviceAreaGeom)) {
580
+ continue;
514
581
  }
515
582
  }
516
583
 
584
+ // If a zone exists, all waypoints must be inside its border
517
585
  if ($serviceRate->hasZone()) {
518
- // make sure all waypoints fall within the service area
519
- foreach ($serviceRate->zone->border as $polygon) {
520
- $polygon = $reader->read($polygon->toJson());
586
+ $zoneBorder = $serviceRate->zone?->border;
521
587
 
522
- foreach ($waypoints as $waypoint) {
523
- if (!$polygon->contains($waypoint)) {
524
- // waypoint outside of zone, not applicable to route
525
- continue;
526
- }
527
- }
588
+ $zoneGeom = null;
589
+ try {
590
+ $zoneGeom = $toBrickGeometry($zoneBorder);
591
+ } catch (\Throwable $e) {
592
+ continue;
593
+ }
594
+
595
+ if (!$containsAllWaypoints($zoneGeom)) {
596
+ continue;
528
597
  }
529
598
  }
530
599
 
@@ -933,29 +1002,23 @@ class ServiceRate extends Model
933
1002
  */
934
1003
  public function findServiceRateFeeByDistance(int $totalDistance): ?ServiceRateFee
935
1004
  {
936
- $this->load('rateFees');
1005
+ $this->loadMissing('rateFees');
937
1006
 
938
- $distanceInKms = round($totalDistance / 1000);
939
- $distanceFee = null;
1007
+ // Convert meters to kilometers WITHOUT rounding up
1008
+ $distanceInKm = $totalDistance / 1000;
940
1009
 
941
- foreach ($this->rateFees as $rateFee) {
942
- $previousRateFee = $rateFee;
1010
+ // Ensure predictable order
1011
+ $rateFees = $this->rateFees->sortBy('distance');
943
1012
 
944
- if ($distanceInKms > $rateFee->distance) {
945
- continue;
946
- } elseif ($rateFee->distance > $distanceInKms) {
947
- $distanceFee = $previousRateFee;
948
- } else {
949
- $distanceFee = $rateFee;
1013
+ // Find the first tier that covers the distance
1014
+ foreach ($rateFees as $rateFee) {
1015
+ if ($distanceInKm <= $rateFee->distance) {
1016
+ return $rateFee;
950
1017
  }
951
1018
  }
952
1019
 
953
- // if no distance fee use the last
954
- if ($distanceFee === null) {
955
- $distanceFee = $this->rateFees->sortByDesc('distance')->first();
956
- }
957
-
958
- return $distanceFee;
1020
+ // If distance exceeds all tiers, use the largest tier
1021
+ return $rateFees->last();
959
1022
  }
960
1023
 
961
1024
  /**
@@ -334,6 +334,24 @@ class Utils extends FleetbaseUtils
334
334
  return new Point((float) $latitude, (float) $longitude);
335
335
  }
336
336
 
337
+ /**
338
+ * Always return spatial point.
339
+ *
340
+ * @param [type] $mixed
341
+ *
342
+ * @return void
343
+ */
344
+ public static function castPoint($mixed)
345
+ {
346
+ try {
347
+ $point = static::getPointFromMixed($mixed);
348
+
349
+ return $point;
350
+ } catch (\Throwable $e) {
351
+ return new Point(0, 0);
352
+ }
353
+ }
354
+
337
355
  /**
338
356
  * Determines if the given coordinates strictly represent a Point object.
339
357
  * These will explude resolvable coordinates from records.