@fleetbase/fleetops-engine 0.6.32 → 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.
Files changed (27) hide show
  1. package/addon/components/order/details/proof.js +28 -1
  2. package/composer.json +1 -1
  3. package/extension.json +1 -1
  4. package/package.json +1 -1
  5. package/server/src/Console/Commands/TestEmail.php +98 -0
  6. package/server/src/Http/Controllers/Api/v1/ContactController.php +16 -37
  7. package/server/src/Http/Controllers/Api/v1/DriverController.php +11 -29
  8. package/server/src/Http/Controllers/Api/v1/OrderController.php +8 -8
  9. package/server/src/Http/Controllers/Api/v1/VehicleController.php +3 -8
  10. package/server/src/Http/Controllers/Internal/v1/DriverController.php +31 -1
  11. package/server/src/Http/Controllers/Internal/v1/OrderController.php +0 -32
  12. package/server/src/Http/Controllers/Internal/v1/PlaceController.php +0 -33
  13. package/server/src/Http/Controllers/Internal/v1/VendorController.php +0 -31
  14. package/server/src/Http/Requests/CreateServiceRateRequest.php +2 -2
  15. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +70 -6
  16. package/server/src/Http/Resources/v1/Driver.php +2 -2
  17. package/server/src/Http/Resources/v1/Index/Driver.php +2 -1
  18. package/server/src/Http/Resources/v1/Index/Place.php +1 -1
  19. package/server/src/Http/Resources/v1/Index/Vehicle.php +2 -2
  20. package/server/src/Http/Resources/v1/Place.php +1 -1
  21. package/server/src/Http/Resources/v1/Vehicle.php +2 -3
  22. package/server/src/Http/Resources/v1/VehicleWithoutDriver.php +2 -3
  23. package/server/src/Models/Payload.php +26 -0
  24. package/server/src/Models/Place.php +22 -0
  25. package/server/src/Models/ServiceRate.php +111 -48
  26. package/server/src/Providers/FleetOpsServiceProvider.php +1 -0
  27. package/server/src/Support/Utils.php +18 -0
@@ -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.32",
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.32",
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.32",
3
+ "version": "0.6.34",
4
4
  "description": "Fleet & Transport Management Extension for Fleetbase",
5
5
  "fleetbase": {
6
6
  "route": "fleet-ops"
@@ -0,0 +1,98 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Console\Commands;
4
+
5
+ use Fleetbase\FleetOps\Mail\CustomerCredentialsMail;
6
+ use Fleetbase\FleetOps\Models\Contact;
7
+ use Fleetbase\Models\Company;
8
+ use Fleetbase\Models\User;
9
+ use Illuminate\Console\Command;
10
+ use Illuminate\Support\Facades\Mail;
11
+
12
+ class TestEmail extends Command
13
+ {
14
+ /**
15
+ * The name and signature of the console command.
16
+ *
17
+ * @var string
18
+ */
19
+ protected $signature = 'fleetops:test-email {email} {--type=customer_credentials : The type of email to test}';
20
+
21
+ /**
22
+ * The console command description.
23
+ *
24
+ * @var string
25
+ */
26
+ protected $description = 'Test FleetOps email templates';
27
+
28
+ /**
29
+ * Execute the console command.
30
+ *
31
+ * @return int
32
+ */
33
+ public function handle()
34
+ {
35
+ $email = $this->argument('email');
36
+ $type = $this->option('type');
37
+
38
+ $this->info('Sending test email...');
39
+ $this->info("Type: {$type}");
40
+ $this->info("To: {$email}");
41
+
42
+ try {
43
+ switch ($type) {
44
+ case 'customer_credentials':
45
+ $this->sendCustomerCredentialsEmail($email);
46
+ break;
47
+
48
+ default:
49
+ $this->error("Unknown email type: {$type}");
50
+
51
+ return Command::FAILURE;
52
+ }
53
+
54
+ $this->info('✓ Test email sent successfully!');
55
+
56
+ return Command::SUCCESS;
57
+ } catch (\Exception $e) {
58
+ $this->error('Failed to send test email: ' . $e->getMessage());
59
+
60
+ return Command::FAILURE;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Send a test customer credentials email.
66
+ */
67
+ private function sendCustomerCredentialsEmail(string $email): void
68
+ {
69
+ // Create a mock user
70
+ $user = new User([
71
+ 'name' => 'Test Customer',
72
+ 'email' => $email,
73
+ ]);
74
+
75
+ // Create a mock company
76
+ $company = new Company([
77
+ 'name' => 'Test Company',
78
+ 'public_id' => 'test_company_123',
79
+ ]);
80
+
81
+ // Create a mock customer
82
+ $customer = new Contact([
83
+ 'name' => 'Test Customer',
84
+ 'email' => $email,
85
+ 'phone' => '+1234567890',
86
+ ]);
87
+
88
+ // Set relations
89
+ $customer->setRelation('company', $company);
90
+ $customer->setRelation('user', $user);
91
+
92
+ // Mock password
93
+ $plaintextPassword = 'TestPassword123!';
94
+
95
+ // Send the email
96
+ Mail::to($email)->send(new CustomerCredentialsMail($plaintextPassword, $customer));
97
+ }
98
+ }
@@ -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;
@@ -62,6 +61,9 @@ class DriverController extends Controller
62
61
  // Apply user infos
63
62
  $userDetails = User::applyUserInfoFromRequest($request, $userDetails);
64
63
 
64
+ // Set company_uuid before creating user
65
+ $userDetails['company_uuid'] = $company->uuid;
66
+
65
67
  // create user account for driver
66
68
  $user = User::create($userDetails);
67
69
 
@@ -121,20 +123,10 @@ class DriverController extends Controller
121
123
  // create the driver
122
124
  $driver = Driver::create($input);
123
125
 
124
- // Handle photo as either file id/ or base64 data string
125
- $photo = $request->input('photo');
126
- if ($photo) {
127
- $file = null;
128
- // Handle photo being a file id
129
- if (Utils::isPublicId($photo)) {
130
- $file = File::where('public_id', $photo)->first();
131
- }
132
-
133
- // Handle the photo being base64 data string
134
- if (Utils::isBase64String($photo)) {
135
- $path = implode('/', ['uploads', session('company'), 'drivers']);
136
- $file = File::createFromBase64($photo, null, $path);
137
- }
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);
138
130
 
139
131
  if ($file) {
140
132
  $user->update(['photo_uuid' => $file->uuid]);
@@ -215,20 +207,10 @@ class DriverController extends Controller
215
207
  $driver->update($input);
216
208
  $driver->flushAttributesCache();
217
209
 
218
- // Handle photo as either file id/ or base64 data string
219
- $photo = $request->input('photo');
220
- if ($photo) {
221
- $file = null;
222
- // Handle photo being a file id
223
- if (Utils::isPublicId($photo)) {
224
- $file = File::where('public_id', $photo)->first();
225
- }
226
-
227
- // Handle the photo being base64 data string
228
- if (Utils::isBase64String($photo)) {
229
- $path = implode('/', ['uploads', session('company'), 'drivers']);
230
- $file = File::createFromBase64($photo, null, $path);
231
- }
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);
232
214
 
233
215
  if ($file) {
234
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,12 +27,10 @@ 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
  // make sure company is set
31
32
  $input['company_uuid'] = session('company');
32
33
 
33
- // create instance of vehicle model
34
- $vehicle = new Vehicle();
35
-
36
34
  // set default online
37
35
  if (!isset($input['online'])) {
38
36
  $input['online'] = 0;
@@ -51,11 +49,8 @@ class VehicleController extends Controller
51
49
  $input['location'] = Utils::getPointFromCoordinates($request->only(['latitude', 'longitude']));
52
50
  }
53
51
 
54
- // apply user input to vehicle
55
- $vehicle = $vehicle->fill($input);
56
-
57
- // save the vehicle
58
- $vehicle->save();
52
+ // create the vehicle (fires 'created' event for billing resource tracking)
53
+ $vehicle = Vehicle::create($input);
59
54
 
60
55
  // driver assignment
61
56
  if ($request->has('driver')) {
@@ -136,7 +136,37 @@ 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
- if ($user && $input->has('photo_uuid')) {
139
+
140
+ // If user doesn't exist with provided UUID, create new user
141
+ if (!$user) {
142
+ $userInput = $input
143
+ ->only(['name', 'password', 'email', 'phone', 'status', 'avatar_uuid'])
144
+ ->filter()
145
+ ->toArray();
146
+
147
+ // handle `photo_uuid`
148
+ if (isset($input['photo_uuid']) && Str::isUuid($input['photo_uuid'])) {
149
+ $userInput['avatar_uuid'] = $input['photo_uuid'];
150
+ }
151
+
152
+ // Make sure password is set
153
+ if (empty($userInput['password'])) {
154
+ $userInput['password'] = Str::random(14);
155
+ }
156
+
157
+ // Set user company
158
+ $userInput['company_uuid'] = session('company', $company->uuid);
159
+
160
+ // Apply user infos
161
+ $userInput = User::applyUserInfoFromRequest($request, $userInput);
162
+
163
+ // Create user account
164
+ $user = User::create($userInput);
165
+
166
+ // Set the user type to driver
167
+ $user->setType('driver');
168
+ } elseif ($input->has('photo_uuid')) {
169
+ // Update existing user's avatar if photo provided
140
170
  $user->update(['avatar_uuid' => $input->get('photo_uuid')]);
141
171
  }
142
172
  } else {
@@ -29,14 +29,12 @@ use Fleetbase\FleetOps\Models\Waypoint;
29
29
  use Fleetbase\FleetOps\Support\Utils;
30
30
  use Fleetbase\Http\Requests\ExportRequest;
31
31
  use Fleetbase\Http\Requests\Internal\BulkActionRequest;
32
- use Fleetbase\Http\Requests\Internal\BulkDeleteRequest;
33
32
  use Fleetbase\Models\File;
34
33
  use Fleetbase\Models\Type;
35
34
  use Fleetbase\Support\TemplateString;
36
35
  use Illuminate\Database\Eloquent\ModelNotFoundException;
37
36
  use Illuminate\Database\QueryException;
38
37
  use Illuminate\Http\Request;
39
- use Illuminate\Support\Collection;
40
38
  use Illuminate\Support\Facades\Cache;
41
39
  use Illuminate\Support\Facades\DB;
42
40
  use Illuminate\Support\Facades\Validator;
@@ -330,36 +328,6 @@ class OrderController extends FleetOpsController
330
328
  );
331
329
  }
332
330
 
333
- /**
334
- * Updates a order to canceled and updates order activity.
335
- *
336
- * @return \Illuminate\Http\Response
337
- */
338
- public function bulkDelete(BulkDeleteRequest $request)
339
- {
340
- $ids = $request->input('ids', []);
341
-
342
- if (!$ids) {
343
- return response()->error('Nothing to delete.');
344
- }
345
-
346
- /** @var Order */
347
- $count = Order::whereIn('uuid', $ids)->count();
348
- $deleted = Order::whereIn('uuid', $ids)->delete();
349
-
350
- if (!$deleted) {
351
- return response()->error('Failed to bulk delete orders.');
352
- }
353
-
354
- return response()->json(
355
- [
356
- 'status' => 'OK',
357
- 'message' => 'Deleted ' . $count . ' orders',
358
- 'count' => $count,
359
- ]
360
- );
361
- }
362
-
363
331
  /**
364
332
  * Updates a order to canceled and updates order activity.
365
333
  *
@@ -10,7 +10,6 @@ use Fleetbase\FleetOps\Models\Place;
10
10
  use Fleetbase\FleetOps\Support\Geocoding;
11
11
  use Fleetbase\Http\Requests\ExportRequest;
12
12
  use Fleetbase\Http\Requests\ImportRequest;
13
- use Fleetbase\Http\Requests\Internal\BulkDeleteRequest;
14
13
  use Fleetbase\LaravelMysqlSpatial\Types\Point;
15
14
  use Illuminate\Http\Request;
16
15
  use Illuminate\Support\Str;
@@ -150,38 +149,6 @@ class PlaceController extends FleetOpsController
150
149
  return Excel::download(new PlaceExport($selections), $fileName);
151
150
  }
152
151
 
153
- /**
154
- * Bulk deletes resources.
155
- *
156
- * @return \Illuminate\Http\Response
157
- */
158
- public function bulkDelete(BulkDeleteRequest $request)
159
- {
160
- $ids = $request->input('ids', []);
161
-
162
- if (!$ids) {
163
- return response()->error('Nothing to delete.');
164
- }
165
-
166
- /**
167
- * @var \Fleetbase\Models\Place
168
- */
169
- $count = Place::whereIn('uuid', $ids)->applyDirectivesForPermissions('fleet-ops list place')->count();
170
- $deleted = Place::whereIn('uuid', $ids)->applyDirectivesForPermissions('fleet-ops list place')->delete();
171
-
172
- if (!$deleted) {
173
- return response()->error('Failed to bulk delete places.');
174
- }
175
-
176
- return response()->json(
177
- [
178
- 'status' => 'OK',
179
- 'message' => 'Deleted ' . $count . ' places',
180
- ],
181
- 200
182
- );
183
- }
184
-
185
152
  /**
186
153
  * Get all avatar options for an vehicle.
187
154
  *
@@ -9,7 +9,6 @@ use Fleetbase\FleetOps\Models\Driver;
9
9
  use Fleetbase\FleetOps\Models\Vendor;
10
10
  use Fleetbase\Http\Requests\ExportRequest;
11
11
  use Fleetbase\Http\Requests\ImportRequest;
12
- use Fleetbase\Http\Requests\Internal\BulkDeleteRequest;
13
12
  use Illuminate\Http\Request;
14
13
  use Illuminate\Support\Facades\DB;
15
14
  use Illuminate\Support\Str;
@@ -85,36 +84,6 @@ class VendorController extends FleetOpsController
85
84
  return Excel::download(new VendorExport($selections), $fileName);
86
85
  }
87
86
 
88
- /**
89
- * Bulk delete resources.
90
- *
91
- * @return \Illuminate\Http\Response
92
- */
93
- public function bulkDelete(BulkDeleteRequest $request)
94
- {
95
- $ids = $request->input('ids', []);
96
-
97
- if (!$ids) {
98
- return response()->error('Nothing to delete.');
99
- }
100
-
101
- /** @var \Fleetbase\Models\Vendor */
102
- $count = Vendor::whereIn('uuid', $ids)->count();
103
- $deleted = Vendor::whereIn('uuid', $ids)->delete();
104
-
105
- if (!$deleted) {
106
- return response()->error('Failed to bulk delete vendors.');
107
- }
108
-
109
- return response()->json(
110
- [
111
- 'status' => 'OK',
112
- 'message' => 'Deleted ' . $count . ' vendors',
113
- ],
114
- 200
115
- );
116
- }
117
-
118
87
  /**
119
88
  * Get all status options for an vehicle.
120
89
  *
@@ -36,8 +36,8 @@ class CreateServiceRateRequest extends FleetbaseRequest
36
36
  'base_fee' => ['numeric'],
37
37
  'per_meter_unit' => ['required_if:rate_calculation_method,per_meter', 'string', 'in:km,m'],
38
38
  'per_meter_flat_rate_fee' => ['required_if:rate_calculation_method,per_meter', 'numeric'],
39
- 'meter_fees' => [Rule::requiredIf(function ($input) {
40
- return in_array($input->rate_calculation_method, ['fixed_meter', 'fixed_rate']);
39
+ 'meter_fees' => [Rule::requiredIf(function () {
40
+ return in_array($this->input('rate_calculation_method'), ['fixed_meter', 'fixed_rate']);
41
41
  }), 'array'],
42
42
  'meter_fees.*.distance' => ['numeric'],
43
43
  'meter_fees.*.fee' => ['numeric'],
@@ -3,7 +3,9 @@
3
3
  namespace Fleetbase\FleetOps\Http\Requests\Internal;
4
4
 
5
5
  use Fleetbase\FleetOps\Http\Requests\CreateDriverRequest as CreateDriverApiRequest;
6
+ use Fleetbase\FleetOps\Rules\ResolvablePoint;
6
7
  use Fleetbase\Support\Auth;
8
+ use Illuminate\Validation\Rule;
7
9
 
8
10
  class CreateDriverRequest extends CreateDriverApiRequest
9
11
  {
@@ -24,14 +26,76 @@ class CreateDriverRequest extends CreateDriverApiRequest
24
26
  */
25
27
  public function rules()
26
28
  {
27
- $isCreating = $this->isMethod('POST');
29
+ $isCreating = $this->isMethod('POST');
30
+ $isCreatingWithUser = $this->filled('driver.user_uuid');
31
+ $shouldValidateUserAttributes = $isCreating && !$isCreatingWithUser;
28
32
 
29
33
  return [
30
- 'password' => 'nullable|string',
31
- 'country' => 'nullable|size:2',
32
- 'city' => 'nullable|string',
33
- 'status' => 'nullable|string|in:active,inactive',
34
- 'job' => 'nullable|exists:orders,public_id',
34
+ // Required fields for driver creation
35
+ 'name' => [Rule::requiredIf($shouldValidateUserAttributes), 'nullable', 'string', 'max:255'],
36
+ 'email' => [
37
+ Rule::requiredIf($shouldValidateUserAttributes),
38
+ Rule::when($this->filled('email'), ['email']),
39
+ Rule::when($shouldValidateUserAttributes, [Rule::unique('users')->whereNull('deleted_at')]),
40
+ ],
41
+ 'phone' => [
42
+ Rule::requiredIf($shouldValidateUserAttributes),
43
+ Rule::when($shouldValidateUserAttributes, [Rule::unique('users')->whereNull('deleted_at')]),
44
+ ],
45
+
46
+ // Optional fields
47
+ 'password' => 'nullable|string|min:8',
48
+ 'drivers_license_number' => 'nullable|string|max:255',
49
+ 'internal_id' => 'nullable|string|max:255',
50
+ 'country' => 'nullable|string|size:2',
51
+ 'city' => 'nullable|string|max:255',
52
+ 'vehicle' => 'nullable|string|starts_with:vehicle_|exists:vehicles,public_id',
53
+ 'status' => 'nullable|string|in:active,inactive',
54
+ 'vendor' => 'nullable|exists:vendors,public_id',
55
+ 'job' => 'nullable|exists:orders,public_id',
56
+ 'location' => ['nullable', new ResolvablePoint()],
57
+ 'latitude' => ['nullable', 'required_with:longitude', 'numeric'],
58
+ 'longitude' => ['nullable', 'required_with:latitude', 'numeric'],
59
+
60
+ // Photo/avatar
61
+ 'photo_uuid' => 'nullable|exists:files,uuid',
62
+ 'avatar_uuid' => 'nullable|exists:files,uuid',
63
+ ];
64
+ }
65
+
66
+ /**
67
+ * Get custom attributes for validator errors.
68
+ *
69
+ * @return array
70
+ */
71
+ public function attributes()
72
+ {
73
+ return [
74
+ 'name' => 'driver name',
75
+ 'email' => 'email address',
76
+ 'phone' => 'phone number',
77
+ 'drivers_license_number' => 'driver\'s license number',
78
+ 'internal_id' => 'internal ID',
79
+ 'photo_uuid' => 'photo',
80
+ 'avatar_uuid' => 'avatar',
81
+ ];
82
+ }
83
+
84
+ /**
85
+ * Get custom messages for validator errors.
86
+ *
87
+ * @return array
88
+ */
89
+ public function messages()
90
+ {
91
+ return [
92
+ 'name.required' => 'Driver name is required.',
93
+ 'email.required' => 'Email address is required.',
94
+ 'email.email' => 'Please provide a valid email address.',
95
+ 'email.unique' => 'This email address is already registered.',
96
+ 'phone.required' => 'Phone number is required.',
97
+ 'phone.unique' => 'This phone number is already registered.',
98
+ 'password.min' => 'Password must be at least 8 characters.',
35
99
  ];
36
100
  }
37
101
  }
@@ -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
 
@@ -333,6 +333,28 @@ class Place extends Model
333
333
  {
334
334
  $instance = (new static())->fillWithGoogleAddress($address);
335
335
 
336
+ // Before saving or returning this instance check the database for a duplicate address
337
+ // it cannot have any owner, and must belong to this session
338
+ if ($companyUuid = session('company')) {
339
+ $duplicate = static::where([
340
+ 'company_uuid' => $companyUuid,
341
+ 'street1' => $instance->street1,
342
+ 'city' => $instance->city,
343
+ 'country' => $instance->country,
344
+ ])
345
+ ->when(
346
+ $instance->postal_code !== null,
347
+ fn ($q) => $q->where('postal_code', $instance->postal_code),
348
+ fn ($q) => $q->whereNull('postal_code')
349
+ )
350
+ ->whereNull('owner_uuid')
351
+ ->first();
352
+
353
+ if ($duplicate) {
354
+ return $duplicate;
355
+ }
356
+ }
357
+
336
358
  if ($saveInstance) {
337
359
  $instance->save();
338
360
  }
@@ -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
  /**
@@ -60,6 +60,7 @@ class FleetOpsServiceProvider extends CoreServiceProvider
60
60
  \Fleetbase\FleetOps\Console\Commands\PurgeUnpurchasedServiceQuotes::class,
61
61
  \Fleetbase\FleetOps\Console\Commands\SendDriverNotification::class,
62
62
  \Fleetbase\FleetOps\Console\Commands\ReplayVehicleLocations::class,
63
+ \Fleetbase\FleetOps\Console\Commands\TestEmail::class,
63
64
  ];
64
65
 
65
66
  /**
@@ -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.