@fleetbase/fleetops-engine 0.6.60 → 0.6.62
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.
- package/RELEASE.md +21 -17
- package/addon/components/ai/route-preview-map.js +1 -5
- package/addon/components/customer/orders.js +2 -1
- package/addon/components/driver/card.hbs +49 -0
- package/addon/components/driver/card.js +30 -0
- package/addon/components/map/leaflet-live-map.js +4 -19
- package/addon/components/modals/place-details.hbs +1 -1
- package/addon/components/modals/point-map.hbs +1 -1
- package/addon/components/orchestrator-workbench.js +2 -1
- package/addon/components/order/form/route.js +3 -0
- package/addon/components/order-tracking-lookup.js +2 -1
- package/addon/components/place/details.hbs +1 -1
- package/addon/components/positions-replay.js +4 -1
- package/addon/components/service-area/details.hbs +1 -1
- package/addon/components/widget/live-fleet.js +7 -7
- package/addon/components/zone/details.hbs +1 -1
- package/addon/controllers/management/drivers/index.js +22 -1
- package/addon/controllers/settings/map.js +16 -0
- package/addon/helpers/leaflet-tile-url.js +19 -0
- package/addon/services/map-settings.js +26 -1
- package/addon/templates/management/drivers/index.hbs +67 -33
- package/addon/templates/settings/map.hbs +10 -0
- package/addon/utils/leaflet-tile-url.js +31 -0
- package/addon/utils/prepare-place-for-save.js +13 -0
- package/app/components/driver/card.js +1 -0
- package/app/helpers/leaflet-tile-url.js +1 -0
- package/app/utils/leaflet-tile-url.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/index.js +11 -0
- package/package.json +2 -1
- package/server/migrations/2026_09_05_000001_add_unique_indexes_to_fleet_membership_pivots.php +129 -0
- package/server/src/Exceptions/PublicRelationNotFoundException.php +47 -0
- package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesFleetOpsApiResources.php +42 -7
- package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesPublicExpansions.php +187 -0
- package/server/src/Http/Controllers/Api/v1/DriverController.php +312 -59
- package/server/src/Http/Controllers/Api/v1/EntityController.php +1 -1
- package/server/src/Http/Controllers/Api/v1/FleetController.php +370 -33
- package/server/src/Http/Controllers/Api/v1/FuelReportController.php +1 -1
- package/server/src/Http/Controllers/Api/v1/IssueController.php +1 -1
- package/server/src/Http/Controllers/Api/v1/ManifestController.php +250 -0
- package/server/src/Http/Controllers/Api/v1/OrderController.php +1 -1
- package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +1 -1
- package/server/src/Http/Controllers/Api/v1/TrackingStatusController.php +1 -1
- package/server/src/Http/Controllers/Api/v1/VehicleController.php +139 -34
- package/server/src/Http/Controllers/Api/v1/ZoneController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/SettingController.php +22 -0
- package/server/src/Http/Filter/Concerns/ResolvesPublicRelationUuids.php +54 -0
- package/server/src/Http/Filter/DriverFilter.php +71 -15
- package/server/src/Http/Filter/FleetFilter.php +32 -33
- package/server/src/Http/Filter/FuelReportFilter.php +29 -0
- package/server/src/Http/Filter/IssueFilter.php +29 -0
- package/server/src/Http/Filter/VehicleFilter.php +57 -19
- package/server/src/Http/Requests/Concerns/ScopesPublicRelationRules.php +48 -0
- package/server/src/Http/Requests/CreateDriverRequest.php +100 -9
- package/server/src/Http/Requests/CreateFleetRequest.php +30 -2
- package/server/src/Http/Requests/CreateServiceRateRequest.php +7 -2
- package/server/src/Http/Requests/CreateVehicleRequest.php +150 -26
- package/server/src/Http/Resources/v1/Concerns/ResolvesPublicRelationFields.php +110 -0
- package/server/src/Http/Resources/v1/Driver.php +22 -2
- package/server/src/Http/Resources/v1/Fleet.php +38 -25
- package/server/src/Http/Resources/v1/Manifest.php +52 -0
- package/server/src/Http/Resources/v1/ManifestStop.php +47 -0
- package/server/src/Http/Resources/v1/OrderConfig.php +51 -4
- package/server/src/Http/Resources/v1/Vehicle.php +46 -4
- package/server/src/Models/Driver.php +2 -2
- package/server/src/Models/Place.php +1 -1
- package/server/src/routes.php +29 -0
- package/server/tests/ApiDriverControllerContractsTest.php +317 -13
- package/server/tests/ApiDriverPasswordContractsTest.php +457 -0
- package/server/tests/ApiEntityControllerContractsTest.php +1 -1
- package/server/tests/ApiEquipmentControllerContractsTest.php +2 -2
- package/server/tests/ApiFleetControllerContractsTest.php +354 -21
- package/server/tests/ApiFuelReportControllerContractsTest.php +1 -1
- package/server/tests/ApiIssueControllerContractsTest.php +1 -1
- package/server/tests/ApiManifestControllerContractsTest.php +599 -0
- package/server/tests/ApiOrderControllerContractsTest.php +1 -1
- package/server/tests/ApiPartControllerContractsTest.php +2 -2
- package/server/tests/ApiServiceAreaZoneControllerContractsTest.php +2 -2
- package/server/tests/ApiTrackingStatusControllerContractsTest.php +1 -1
- package/server/tests/ApiVehicleControllerContractsTest.php +210 -8
- package/server/tests/CompactResourceSerializationTest.php +13 -7
- package/server/tests/ControllerFilterContractsTest.php +132 -13
- package/server/tests/ControllerHelperContractsTest.php +6 -3
- package/server/tests/DriverFilterExecutionTest.php +130 -6
- package/server/tests/ExceptionContractsTest.php +15 -0
- package/server/tests/Feature/Http/Api/DeviceControllerContractsTest.php +1 -1
- package/server/tests/Feature/Http/Api/DriverControllerSessionHelpersTest.php +61 -0
- package/server/tests/Feature/Http/Api/FleetMembershipMigrationTest.php +140 -0
- package/server/tests/Feature/Http/Api/FleetMembershipTest.php +333 -0
- package/server/tests/Feature/Http/Api/FleetPublicContractTest.php +439 -0
- package/server/tests/Feature/Http/Api/FuelTransactionControllerContractsTest.php +2 -2
- package/server/tests/Feature/Http/Api/NullRelationshipInputTest.php +191 -0
- package/server/tests/Feature/Http/Api/PublicIdentifierFilterTest.php +293 -0
- package/server/tests/Feature/Http/Api/SmallApiControllerHelpersTest.php +7 -1
- package/server/tests/Feature/Http/Api/VehicleControllerTrackingTest.php +3 -1
- package/server/tests/Feature/Http/Api/VehiclePublicContractTest.php +454 -0
- package/server/tests/Feature/Http/Api/WorkOrderControllerContractsTest.php +1 -1
- package/server/tests/Feature/Http/Internal/PlaceControllerTest.php +45 -0
- package/server/tests/FleetHierarchyResourceTest.php +56 -8
- package/server/tests/RequestContractsTest.php +105 -7
- package/server/tests/SettingControllerContractsTest.php +33 -0
- package/server/tests/Unit/Http/Filter/FuelReportFilterTest.php +135 -0
- package/server/tests/Unit/Http/Filter/IssueFilterTest.php +43 -0
- package/server/tests/Unit/Http/Resources/FleetResourceTest.php +50 -6
- package/server/tests/Unit/Http/Resources/ManifestResourceTest.php +129 -0
- package/server/tests/Unit/Http/Resources/OrderConfigResourceTest.php +91 -0
- package/server/tests/VehicleControllerHelperContractsTest.php +1 -1
- package/translations/en-us.yaml +4 -0
|
@@ -41,10 +41,25 @@ class OrderConfig extends FleetbaseResource
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
* Project the flow JSON into
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* `
|
|
44
|
+
* Project the flow JSON into a list of activities, keeping the public-safe
|
|
45
|
+
* per-activity fields.
|
|
46
|
+
*
|
|
47
|
+
* `code` and `status` (the label) are the contract; `complete`, `color`,
|
|
48
|
+
* `details`, `pod_method` and `require_pod` are useful UI hints that ride
|
|
49
|
+
* along.
|
|
50
|
+
*
|
|
51
|
+
* `activities`, `sequence` and `logic` describe the flow's *shape*, and are
|
|
52
|
+
* published because without them the list cannot be sequenced. The stored
|
|
53
|
+
* flow is a directed graph — `activities` names the codes an activity can
|
|
54
|
+
* transition to, `sequence` orders activities reachable from the same
|
|
55
|
+
* parent, and `logic` gates availability — which is precisely what
|
|
56
|
+
* `OrderConfig::nextActivity()` walks server-side. Projecting them away
|
|
57
|
+
* left consumers with an unordered set: a client rendering progress from
|
|
58
|
+
* array position marks a dispatched order complete whenever `completed`
|
|
59
|
+
* happens to be declared earlier, and can offer no next step at all.
|
|
60
|
+
*
|
|
61
|
+
* These are descriptions of the configured workflow, not internal state, so
|
|
62
|
+
* there is nothing here a consumer of the config should not see.
|
|
48
63
|
*/
|
|
49
64
|
protected function projectFlow(): array
|
|
50
65
|
{
|
|
@@ -66,9 +81,41 @@ class OrderConfig extends FleetbaseResource
|
|
|
66
81
|
'complete' => (bool) ($activity['complete'] ?? false),
|
|
67
82
|
'pod_method' => $activity['pod_method'] ?? null,
|
|
68
83
|
'require_pod' => (bool) ($activity['require_pod'] ?? false),
|
|
84
|
+
'sequence' => isset($activity['sequence']) ? (int) $activity['sequence'] : null,
|
|
85
|
+
'activities' => static::projectTransitions($activity),
|
|
86
|
+
'logic' => $activity['logic'] ?? null,
|
|
69
87
|
];
|
|
70
88
|
}
|
|
71
89
|
|
|
72
90
|
return $activities;
|
|
73
91
|
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The codes an activity can transition to, as a list of strings.
|
|
95
|
+
*
|
|
96
|
+
* Stored flows have been written both ways over time: a bare list of codes,
|
|
97
|
+
* and a list of objects carrying a `code`. Normalising here means consumers
|
|
98
|
+
* do not each have to guess which one they were handed.
|
|
99
|
+
*/
|
|
100
|
+
protected static function projectTransitions(array $activity): array
|
|
101
|
+
{
|
|
102
|
+
$transitions = $activity['activities'] ?? [];
|
|
103
|
+
if (!is_array($transitions)) {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
$codes = [];
|
|
108
|
+
foreach ($transitions as $transition) {
|
|
109
|
+
if (is_string($transition)) {
|
|
110
|
+
$codes[] = $transition;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (is_array($transition) && isset($transition['code']) && is_string($transition['code'])) {
|
|
115
|
+
$codes[] = $transition['code'];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return $codes;
|
|
120
|
+
}
|
|
74
121
|
}
|
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
namespace Fleetbase\FleetOps\Http\Resources\v1;
|
|
4
4
|
|
|
5
|
+
use Fleetbase\FleetOps\Http\Resources\v1\Concerns\ResolvesPublicRelationFields;
|
|
5
6
|
use Fleetbase\FleetOps\Support\Utils;
|
|
6
7
|
use Fleetbase\Http\Resources\FleetbaseResource;
|
|
7
8
|
use Fleetbase\Support\Http;
|
|
9
|
+
use Fleetbase\Support\Resolve;
|
|
8
10
|
|
|
9
11
|
class Vehicle extends FleetbaseResource
|
|
10
12
|
{
|
|
13
|
+
use ResolvesPublicRelationFields;
|
|
14
|
+
|
|
11
15
|
/**
|
|
12
16
|
* Transform the resource into an array.
|
|
13
17
|
*
|
|
@@ -17,6 +21,12 @@ class Vehicle extends FleetbaseResource
|
|
|
17
21
|
*/
|
|
18
22
|
public function toArray($request)
|
|
19
23
|
{
|
|
24
|
+
$with = $this->requestedRelations($request);
|
|
25
|
+
|
|
26
|
+
if ($with !== []) {
|
|
27
|
+
$this->loadMissing($with);
|
|
28
|
+
}
|
|
29
|
+
|
|
20
30
|
return $this->withCustomFields([
|
|
21
31
|
// Identity
|
|
22
32
|
'id' => $this->when(Http::isInternalRequest(), $this->id, $this->public_id),
|
|
@@ -39,10 +49,24 @@ class Vehicle extends FleetbaseResource
|
|
|
39
49
|
'description' => $this->description,
|
|
40
50
|
'driver_name' => $this->when(Http::isInternalRequest(), $this->driver_name),
|
|
41
51
|
'vendor_name' => $this->when(Http::isInternalRequest(), $this->vendor_name),
|
|
42
|
-
'assigned_orders_count' => $this->when(Http::isInternalRequest(), $this->assignedOrdersCount()),
|
|
43
|
-
'current_order_reference'=> $this->when(Http::isInternalRequest(), $this->currentOrderReference()),
|
|
44
|
-
// Relationships
|
|
45
|
-
|
|
52
|
+
'assigned_orders_count' => $this->when(Http::isInternalRequest(), fn () => $this->assignedOrdersCount()),
|
|
53
|
+
'current_order_reference'=> $this->when(Http::isInternalRequest(), fn () => $this->currentOrderReference()),
|
|
54
|
+
// Relationships. The identifier is additive and always present so a
|
|
55
|
+
// write can be read back; the object beside it keeps the shape it
|
|
56
|
+
// has always had — absent unless loaded, an object when it is.
|
|
57
|
+
'driver_id' => $this->publicIdForRelation('driver', null),
|
|
58
|
+
'vendor_id' => $this->publicIdForRelation('vendor', 'vendor_uuid'),
|
|
59
|
+
'category_id' => $this->publicIdForRelation('category', 'category_uuid'),
|
|
60
|
+
'warranty_id' => $this->publicIdForRelation('warranty', 'warranty_uuid'),
|
|
61
|
+
'photo_id' => $this->publicIdForRelation('photo', 'photo_uuid'),
|
|
62
|
+
'driver' => $this->publicRelationObject('driver', $with, fn () => new Driver($this->driver)),
|
|
63
|
+
'vendor' => $this->publicRelationObject('vendor', $with, fn () => new Vendor($this->vendor)),
|
|
64
|
+
// Resolved through the repository's own resource resolver rather than
|
|
65
|
+
// a hardcoded class: Category and File live in core, and Warranty has
|
|
66
|
+
// no v1 resource of its own.
|
|
67
|
+
'category' => $this->publicRelationObject('category', $with, fn () => Resolve::httpResourceForModel($this->category)),
|
|
68
|
+
'warranty' => $this->publicRelationObject('warranty', $with, fn () => Resolve::httpResourceForModel($this->warranty)),
|
|
69
|
+
'photo' => $this->publicRelationObject('photo', $with, fn () => Resolve::httpResourceForModel($this->photo)),
|
|
46
70
|
'devices' => $this->whenLoaded('devices', fn () => $this->devices),
|
|
47
71
|
// Vehicle identification
|
|
48
72
|
'make' => $this->make,
|
|
@@ -139,6 +163,15 @@ class Vehicle extends FleetbaseResource
|
|
|
139
163
|
'altitude' => (int) data_get($this, 'altitude', 0),
|
|
140
164
|
'speed' => (int) data_get($this, 'speed', 0),
|
|
141
165
|
'telematics' => data_get($this, 'telematics'),
|
|
166
|
+
// Orchestrator constraints
|
|
167
|
+
'skills' => data_get($this, 'skills'),
|
|
168
|
+
'payload_capacity_volume' => $this->payload_capacity_volume,
|
|
169
|
+
'payload_capacity_pallets' => $this->payload_capacity_pallets,
|
|
170
|
+
'payload_capacity_parcels' => $this->payload_capacity_parcels,
|
|
171
|
+
'max_tasks' => $this->max_tasks,
|
|
172
|
+
'time_window_start' => $this->time_window_start,
|
|
173
|
+
'time_window_end' => $this->time_window_end,
|
|
174
|
+
'return_to_depot' => $this->return_to_depot,
|
|
142
175
|
// Notes & meta
|
|
143
176
|
'notes' => $this->notes,
|
|
144
177
|
'meta' => data_get($this, 'meta', Utils::createObject()),
|
|
@@ -273,6 +306,15 @@ class Vehicle extends FleetbaseResource
|
|
|
273
306
|
'vin_data' => data_get($this, 'vin_data', Utils::createObject()),
|
|
274
307
|
'specs' => data_get($this, 'specs', Utils::createObject()),
|
|
275
308
|
'details' => data_get($this, 'details', Utils::createObject()),
|
|
309
|
+
// Orchestrator constraints
|
|
310
|
+
'skills' => data_get($this, 'skills'),
|
|
311
|
+
'payload_capacity_volume' => $this->payload_capacity_volume,
|
|
312
|
+
'payload_capacity_pallets' => $this->payload_capacity_pallets,
|
|
313
|
+
'payload_capacity_parcels' => $this->payload_capacity_parcels,
|
|
314
|
+
'max_tasks' => $this->max_tasks,
|
|
315
|
+
'time_window_start' => $this->time_window_start,
|
|
316
|
+
'time_window_end' => $this->time_window_end,
|
|
317
|
+
'return_to_depot' => $this->return_to_depot,
|
|
276
318
|
// Notes & meta
|
|
277
319
|
'notes' => $this->notes,
|
|
278
320
|
'meta' => data_get($this, 'meta', Utils::createObject()),
|
|
@@ -110,7 +110,7 @@ class Driver extends Model
|
|
|
110
110
|
'current_status',
|
|
111
111
|
'slug',
|
|
112
112
|
'status',
|
|
113
|
-
'meta
|
|
113
|
+
'meta',
|
|
114
114
|
// Orchestrator
|
|
115
115
|
'skills',
|
|
116
116
|
'max_travel_time',
|
|
@@ -255,7 +255,7 @@ class Driver extends Model
|
|
|
255
255
|
*/
|
|
256
256
|
public function user()
|
|
257
257
|
{
|
|
258
|
-
return $this->belongsTo(User::class)->select(['uuid', 'company_uuid', 'public_id', 'avatar_uuid', 'name', 'phone', 'email', 'type', 'status', 'last_login'])->without(['driver'])->withTrashed();
|
|
258
|
+
return $this->belongsTo(User::class)->select(['uuid', 'company_uuid', 'public_id', 'avatar_uuid', 'name', 'phone', 'email', 'timezone', 'type', 'status', 'last_login'])->without(['driver'])->withTrashed();
|
|
259
259
|
}
|
|
260
260
|
|
|
261
261
|
/**
|
|
@@ -129,7 +129,7 @@ class Place extends Model
|
|
|
129
129
|
*
|
|
130
130
|
* @var array
|
|
131
131
|
*/
|
|
132
|
-
protected $filterParams = ['vendor', 'contact', 'vendor_uuid', 'vendor_name'];
|
|
132
|
+
protected $filterParams = ['vendor', 'contact', 'vendor_uuid', 'vendor_name', 'avatar_value', 'eta'];
|
|
133
133
|
|
|
134
134
|
/**
|
|
135
135
|
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
|
package/server/src/routes.php
CHANGED
|
@@ -28,6 +28,13 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
|
|
|
28
28
|
$router->post('login-with-sms', 'DriverController@loginWithPhone');
|
|
29
29
|
$router->post('verify-code', 'DriverController@verifyCode');
|
|
30
30
|
$router->post('login', 'DriverController@login');
|
|
31
|
+
// Password management. Deliberately separate from the driver
|
|
32
|
+
// update: changing a password requires proving the old one, and
|
|
33
|
+
// resetting it requires a code, neither of which a general PUT
|
|
34
|
+
// can express.
|
|
35
|
+
$router->post('forgot-password', 'DriverController@forgotPassword');
|
|
36
|
+
$router->post('reset-password', 'DriverController@resetPassword');
|
|
37
|
+
$router->post('{id}/change-password', 'DriverController@changePassword');
|
|
31
38
|
$router->post('{id}/simulate', 'DriverController@simulate');
|
|
32
39
|
$router->match(['put', 'patch', 'post'], '{id}/track', 'DriverController@track');
|
|
33
40
|
$router->post('{id}/register-device', 'DriverController@registerDevice');
|
|
@@ -37,6 +44,8 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
|
|
|
37
44
|
$router->get('/', 'DriverController@query');
|
|
38
45
|
$router->get('{id}', 'DriverController@find');
|
|
39
46
|
$router->get('{id}/organizations', 'DriverController@listOrganizations');
|
|
47
|
+
// A driver's own routes for the day.
|
|
48
|
+
$router->get('{id}/manifests', 'ManifestController@forDriver');
|
|
40
49
|
$router->get('{id}/current-organization', 'DriverController@currentOrganization');
|
|
41
50
|
$router->put('{id}', 'DriverController@update');
|
|
42
51
|
$router->delete('{id}', 'DriverController@delete');
|
|
@@ -185,6 +194,18 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
|
|
|
185
194
|
$router->delete('{id}', 'OrderController@delete');
|
|
186
195
|
$router->get('{id}/editable-entity-fields', 'OrderController@getEditableEntityFields');
|
|
187
196
|
});
|
|
197
|
+
// manifests — a driver's route. Read and run only: creating,
|
|
198
|
+
// cancelling and deleting a manifest is dispatch work and stays on
|
|
199
|
+
// the internal namespace.
|
|
200
|
+
$router->group(['prefix' => 'manifests'], function () use ($router) {
|
|
201
|
+
$router->get('{id}', 'ManifestController@show');
|
|
202
|
+
$router->post('{id}/optimize', 'ManifestController@optimize');
|
|
203
|
+
});
|
|
204
|
+
$router->group(['prefix' => 'manifest-stops'], function () use ($router) {
|
|
205
|
+
$router->patch('{id}', 'ManifestController@updateStop');
|
|
206
|
+
$router->post('{id}', 'ManifestController@updateStop');
|
|
207
|
+
});
|
|
208
|
+
|
|
188
209
|
// entities routes
|
|
189
210
|
$router->group(['prefix' => 'entities'], function () use ($router) {
|
|
190
211
|
$router->post('/', 'EntityController@create');
|
|
@@ -281,6 +302,14 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
|
|
|
281
302
|
$router->group(['prefix' => 'fleets'], function () use ($router) {
|
|
282
303
|
$router->post('/', 'FleetController@create');
|
|
283
304
|
$router->get('/', 'FleetController@query');
|
|
305
|
+
// Membership routes are declared ahead of the `{id}` routes so a
|
|
306
|
+
// literal `vehicles`/`drivers` segment can never be swallowed by
|
|
307
|
+
// the single-parameter patterns below. Both parameters are public
|
|
308
|
+
// ids; no internal uuid is ever accepted or returned here.
|
|
309
|
+
$router->post('{id}/vehicles/{vehicle}', 'FleetController@assignVehicle');
|
|
310
|
+
$router->delete('{id}/vehicles/{vehicle}', 'FleetController@removeVehicle');
|
|
311
|
+
$router->post('{id}/drivers/{driver}', 'FleetController@assignDriver');
|
|
312
|
+
$router->delete('{id}/drivers/{driver}', 'FleetController@removeDriver');
|
|
284
313
|
$router->get('{id}', 'FleetController@find');
|
|
285
314
|
$router->put('{id}', 'FleetController@update');
|
|
286
315
|
$router->delete('{id}', 'FleetController@delete');
|
|
@@ -4,7 +4,9 @@ use Fleetbase\FleetOps\Http\Controllers\Api\v1\DriverController;
|
|
|
4
4
|
use Fleetbase\FleetOps\Http\Requests\CreateDriverRequest;
|
|
5
5
|
use Fleetbase\FleetOps\Http\Requests\UpdateDriverRequest;
|
|
6
6
|
use Fleetbase\FleetOps\Models\Driver;
|
|
7
|
+
use Fleetbase\FleetOps\Models\Order;
|
|
7
8
|
use Fleetbase\FleetOps\Models\Vehicle;
|
|
9
|
+
use Fleetbase\FleetOps\Models\Vendor;
|
|
8
10
|
use Fleetbase\LaravelMysqlSpatial\Types\Point;
|
|
9
11
|
use Fleetbase\Models\Company;
|
|
10
12
|
use Fleetbase\Models\User;
|
|
@@ -29,6 +31,9 @@ class FleetOpsApiDriverControllerProbe extends DriverController
|
|
|
29
31
|
public array $companyCalls = [];
|
|
30
32
|
public array $createdUsers = [];
|
|
31
33
|
public array $uuidLookups = [];
|
|
34
|
+
public array $relationLookups = [];
|
|
35
|
+
public array $relationCompanyScopes = [];
|
|
36
|
+
public array $unresolvable = [];
|
|
32
37
|
public array $pointInputs = [];
|
|
33
38
|
public array $createdDrivers = [];
|
|
34
39
|
public array $resolvedFiles = [];
|
|
@@ -76,6 +81,33 @@ class FleetOpsApiDriverControllerProbe extends DriverController
|
|
|
76
81
|
return $table . '-uuid';
|
|
77
82
|
}
|
|
78
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Stand in for the company-scoped public-id lookup.
|
|
86
|
+
*
|
|
87
|
+
* Anything listed in `$unresolvable` behaves as a cross-company or missing
|
|
88
|
+
* identifier does in production.
|
|
89
|
+
*/
|
|
90
|
+
protected function resolveUuid(string $modelClass, ?string $id, ?string $companyUuid = null): ?string
|
|
91
|
+
{
|
|
92
|
+
if (empty($id)) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
$this->relationLookups[] = [$modelClass, $id];
|
|
97
|
+
$this->relationCompanyScopes[] = $companyUuid;
|
|
98
|
+
|
|
99
|
+
if (in_array($id, $this->unresolvable, true)) {
|
|
100
|
+
throw (new ModelNotFoundException())->setModel($modelClass, $id);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return strtolower(class_basename($modelClass)) . '-uuid';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
public function inputForTest(Request $request): array
|
|
107
|
+
{
|
|
108
|
+
return $this->driverInputFromRequest($request);
|
|
109
|
+
}
|
|
110
|
+
|
|
79
111
|
protected function pointFromCoordinates(array $coordinates): Point
|
|
80
112
|
{
|
|
81
113
|
$this->pointInputs[] = $coordinates;
|
|
@@ -343,21 +375,23 @@ test('api driver controller creates drivers with user company assignment and rel
|
|
|
343
375
|
->and($controller->user->assignedRoles)->toBe(['Driver'])
|
|
344
376
|
->and($controller->createdDrivers[0])->toMatchArray([
|
|
345
377
|
'status' => 'available',
|
|
346
|
-
'vehicle_uuid' => '
|
|
347
|
-
'vendor_uuid' => '
|
|
348
|
-
'current_job_uuid' => '
|
|
378
|
+
'vehicle_uuid' => 'vehicle-uuid',
|
|
379
|
+
'vendor_uuid' => 'vendor-uuid',
|
|
380
|
+
'current_job_uuid' => 'order-uuid',
|
|
349
381
|
'online' => 0,
|
|
350
382
|
'user_uuid' => 'user-uuid',
|
|
351
383
|
'company_uuid' => 'company-uuid',
|
|
352
384
|
])
|
|
353
385
|
->and($controller->createdDrivers[0]['location'])->toBeInstanceOf(Point::class)
|
|
354
|
-
->and($controller->
|
|
355
|
-
[
|
|
356
|
-
[
|
|
357
|
-
[
|
|
358
|
-
)
|
|
386
|
+
->and($controller->relationLookups)->toBe([
|
|
387
|
+
[Vehicle::class, 'vehicle_public'],
|
|
388
|
+
[Vendor::class, 'vendor_public'],
|
|
389
|
+
[Order::class, 'order_public'],
|
|
390
|
+
])
|
|
359
391
|
->and($controller->resolvedFiles)->toBe([['file_public', 'uploads/company-uuid/drivers']])
|
|
360
|
-
|
|
392
|
+
// `photo_uuid` is not a column on users; the avatar lives in `avatar_uuid`
|
|
393
|
+
// and the old key was silently dropped by mass assignment.
|
|
394
|
+
->and($controller->user->updates)->toContain(['avatar_uuid' => 'photo-file-uuid'])
|
|
361
395
|
->and($controller->driver->loaded)->toContain(['user', 'vehicle', 'vendor', 'currentJob']);
|
|
362
396
|
});
|
|
363
397
|
|
|
@@ -411,14 +445,14 @@ test('api driver controller updates drivers user details assignments location an
|
|
|
411
445
|
])
|
|
412
446
|
->and($driver->updates[0])->toMatchArray([
|
|
413
447
|
'status' => 'busy',
|
|
414
|
-
'vehicle_uuid' => '
|
|
415
|
-
'vendor_uuid' => '
|
|
416
|
-
'current_job_uuid' => '
|
|
448
|
+
'vehicle_uuid' => 'vehicle-uuid',
|
|
449
|
+
'vendor_uuid' => 'vendor-uuid',
|
|
450
|
+
'current_job_uuid' => 'order-uuid',
|
|
417
451
|
])
|
|
418
452
|
->and($driver->updates[0]['location'])->toBeInstanceOf(Point::class)
|
|
419
453
|
->and($driver->flushedForTest)->toBeTrue()
|
|
420
454
|
->and($controller->resolvedFiles)->toBe([['photo-input', 'uploads/session-company-uuid/drivers']])
|
|
421
|
-
->and($user->updates)->toContain(['
|
|
455
|
+
->and($user->updates)->toContain(['avatar_uuid' => 'photo-file-uuid'])
|
|
422
456
|
->and($driver->loaded)->toContain(['user', 'vehicle', 'vendor', 'currentJob']);
|
|
423
457
|
});
|
|
424
458
|
|
|
@@ -601,3 +635,273 @@ test('api driver controller reports missing driver branches', function () {
|
|
|
601
635
|
'status' => 404,
|
|
602
636
|
]);
|
|
603
637
|
});
|
|
638
|
+
|
|
639
|
+
test('api driver controller refuses to set a password through a general update', function () {
|
|
640
|
+
/*
|
|
641
|
+
* Regression for a takeover path: `update()` passed `password` straight
|
|
642
|
+
* through to the user, so anyone holding a driver's token — or an unlocked
|
|
643
|
+
* handset — could set a new one without proving they knew the old one, and
|
|
644
|
+
* the account was theirs. A password change is an authorisation decision
|
|
645
|
+
* and now has its own endpoint that demands the current password.
|
|
646
|
+
*/
|
|
647
|
+
$user = new FleetOpsApiDriverUserFake();
|
|
648
|
+
$user->setRawAttributes(['uuid' => 'user-uuid'], true);
|
|
649
|
+
$driver = new FleetOpsApiDriverFake();
|
|
650
|
+
$driver->setRawAttributes([
|
|
651
|
+
'uuid' => 'driver-uuid',
|
|
652
|
+
'public_id' => 'driver_public',
|
|
653
|
+
'user_uuid' => 'user-uuid',
|
|
654
|
+
], true);
|
|
655
|
+
$driver->userForTest = $user;
|
|
656
|
+
$driver->setRelation('user', $user);
|
|
657
|
+
|
|
658
|
+
$controller = new FleetOpsApiDriverControllerProbe();
|
|
659
|
+
$controller->driver = $driver;
|
|
660
|
+
$controller->sessionCompanyUuid = 'session-company-uuid';
|
|
661
|
+
|
|
662
|
+
$controller->update('driver_public', new UpdateDriverRequest([
|
|
663
|
+
'name' => 'Driver Updated',
|
|
664
|
+
'password' => 'attacker-chosen-password',
|
|
665
|
+
]));
|
|
666
|
+
|
|
667
|
+
expect($user->updates)->not->toBeEmpty()
|
|
668
|
+
->and($user->updates[0])->not->toHaveKey('password')
|
|
669
|
+
->and($user->updates[0])->toHaveKey('name');
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
test('api driver controller accepts every safe driver field the model exposes', function () {
|
|
673
|
+
$payload = [
|
|
674
|
+
// Identity
|
|
675
|
+
'internal_id' => 'DRV-9001', 'drivers_license_number' => 'S1234567A',
|
|
676
|
+
'license_expiry' => '2030-06-30',
|
|
677
|
+
// Operational
|
|
678
|
+
'country' => 'SG', 'currency' => 'SGD', 'city' => 'Singapore', 'online' => true,
|
|
679
|
+
'current_status' => 'on_break', 'status' => 'available',
|
|
680
|
+
'heading' => 180, 'bearing' => 90, 'altitude' => 15, 'speed' => 42,
|
|
681
|
+
// Structured / orchestrator
|
|
682
|
+
'meta' => ['depot' => 'north'], 'skills' => ['hazmat'],
|
|
683
|
+
'max_travel_time' => 28800, 'max_distance' => 250000,
|
|
684
|
+
'time_window_start' => '08:00', 'time_window_end' => '18:00',
|
|
685
|
+
];
|
|
686
|
+
|
|
687
|
+
$controller = new FleetOpsApiDriverControllerProbe();
|
|
688
|
+
$input = $controller->inputForTest(new Request($payload));
|
|
689
|
+
|
|
690
|
+
$missing = array_values(array_diff(array_keys($payload), array_keys($input)));
|
|
691
|
+
|
|
692
|
+
expect($missing)->toBe([])
|
|
693
|
+
->and($input)->toMatchArray($payload);
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
test('api driver controller input excludes authentication tenancy and generated columns', function () {
|
|
697
|
+
// The projection used to be an `except()` blocklist, so anything nobody had
|
|
698
|
+
// thought to name reached Driver::create() intact — including the auth token.
|
|
699
|
+
$controller = new FleetOpsApiDriverControllerProbe();
|
|
700
|
+
|
|
701
|
+
$input = $controller->inputForTest(new Request([
|
|
702
|
+
'internal_id' => 'DRV-1',
|
|
703
|
+
'auth_token' => 'forged',
|
|
704
|
+
'signup_token_used' => true,
|
|
705
|
+
'user_uuid' => 'forged',
|
|
706
|
+
'company_uuid' => 'someone-elses-company',
|
|
707
|
+
'vehicle_uuid' => 'forged',
|
|
708
|
+
'vendor_uuid' => 'forged',
|
|
709
|
+
'current_job_uuid' => 'forged',
|
|
710
|
+
'_key' => 'forged',
|
|
711
|
+
'uuid' => 'forged',
|
|
712
|
+
'public_id' => 'driver_forged',
|
|
713
|
+
'slug' => 'forged',
|
|
714
|
+
'avatar_url' => 'forged',
|
|
715
|
+
]));
|
|
716
|
+
|
|
717
|
+
expect(array_keys($input))->toBe(['internal_id']);
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
test('api driver controller persists meta location and telemetry that the blocklist used to drop', function () {
|
|
721
|
+
// `location`, `heading`, `altitude`, `speed` and `meta` were all on the old
|
|
722
|
+
// `except()` list, so every write silently discarded them — and `meta` could
|
|
723
|
+
// not be mass assigned anyway because the model spelled it `meta,`.
|
|
724
|
+
$controller = new FleetOpsApiDriverControllerProbe();
|
|
725
|
+
|
|
726
|
+
$input = $controller->inputForTest(new Request([
|
|
727
|
+
'meta' => ['badge' => 'A12'],
|
|
728
|
+
'heading' => 270,
|
|
729
|
+
'altitude' => 8,
|
|
730
|
+
'speed' => 55,
|
|
731
|
+
]));
|
|
732
|
+
|
|
733
|
+
expect($input)->toMatchArray([
|
|
734
|
+
'meta' => ['badge' => 'A12'],
|
|
735
|
+
'heading' => 270,
|
|
736
|
+
'altitude' => 8,
|
|
737
|
+
'speed' => 55,
|
|
738
|
+
])->and(in_array('meta', (new Driver())->getFillable(), true))->toBeTrue();
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
test('api driver controller creates an operational driver with no email or phone', function () {
|
|
742
|
+
$controller = new FleetOpsApiDriverControllerProbe();
|
|
743
|
+
|
|
744
|
+
$response = $controller->create(new CreateDriverRequest([
|
|
745
|
+
'name' => 'Yard Driver',
|
|
746
|
+
'internal_id' => 'DRV-7788',
|
|
747
|
+
]));
|
|
748
|
+
|
|
749
|
+
// No invented address, no invented number: the user record simply carries
|
|
750
|
+
// neither, which the users table permits. The driver cannot sign in to
|
|
751
|
+
// Navigator until credentials are supplied.
|
|
752
|
+
expect($response)->toBe(['resource' => 'driver', 'driver' => $controller->driver])
|
|
753
|
+
->and($controller->createdUsers[0])->toMatchArray(['name' => 'Yard Driver'])
|
|
754
|
+
->and($controller->createdUsers[0]['email'] ?? null)->toBeNull()
|
|
755
|
+
->and($controller->createdUsers[0]['phone'] ?? null)->toBeNull()
|
|
756
|
+
->and($controller->createdDrivers[0])->toMatchArray([
|
|
757
|
+
'internal_id' => 'DRV-7788',
|
|
758
|
+
'user_uuid' => 'user-uuid',
|
|
759
|
+
'company_uuid' => 'company-uuid',
|
|
760
|
+
'status' => 'available',
|
|
761
|
+
])
|
|
762
|
+
// The Driver-to-User relationship, organization membership, user type
|
|
763
|
+
// and role are all preserved for a credential-less driver.
|
|
764
|
+
->and($controller->user->assignedCompanies)->toBe(['company-uuid'])
|
|
765
|
+
->and($controller->user->assignedTypes)->toBe(['driver'])
|
|
766
|
+
->and($controller->user->assignedRoles)->toBe(['Driver']);
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
test('api driver controller creates a driver with only one contact method', function () {
|
|
770
|
+
$emailOnly = new FleetOpsApiDriverControllerProbe();
|
|
771
|
+
$emailOnly->create(new CreateDriverRequest([
|
|
772
|
+
'name' => 'Email Only',
|
|
773
|
+
'email' => 'email.only@example.test',
|
|
774
|
+
]));
|
|
775
|
+
|
|
776
|
+
$phoneOnly = new FleetOpsApiDriverControllerProbe();
|
|
777
|
+
$phoneOnly->create(new CreateDriverRequest([
|
|
778
|
+
'name' => 'Phone Only',
|
|
779
|
+
'phone' => '+15550001111',
|
|
780
|
+
]));
|
|
781
|
+
|
|
782
|
+
expect($emailOnly->createdUsers[0])->toMatchArray(['email' => 'email.only@example.test'])
|
|
783
|
+
->and($emailOnly->createdUsers[0]['phone'] ?? null)->toBeNull()
|
|
784
|
+
->and($phoneOnly->createdUsers[0])->toMatchArray(['phone' => '+15550001111'])
|
|
785
|
+
->and($phoneOnly->createdUsers[0]['email'] ?? null)->toBeNull();
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
test('api driver controller rejects a relationship that belongs to another company', function () {
|
|
789
|
+
$controller = new FleetOpsApiDriverControllerProbe();
|
|
790
|
+
$controller->unresolvable = ['vehicle_other_company'];
|
|
791
|
+
|
|
792
|
+
$created = $controller->create(new CreateDriverRequest([
|
|
793
|
+
'name' => 'Driver One',
|
|
794
|
+
'vehicle' => 'vehicle_other_company',
|
|
795
|
+
]));
|
|
796
|
+
|
|
797
|
+
$controller = new FleetOpsApiDriverControllerProbe();
|
|
798
|
+
$controller->unresolvable = ['vendor_other_company'];
|
|
799
|
+
|
|
800
|
+
$updated = $controller->update('driver_public', new UpdateDriverRequest([
|
|
801
|
+
'vendor' => 'vendor_other_company',
|
|
802
|
+
]));
|
|
803
|
+
|
|
804
|
+
expect($created)->toBe([
|
|
805
|
+
'json' => ['error' => 'No vehicle resource found for the identifier provided.'],
|
|
806
|
+
'status' => 404,
|
|
807
|
+
])->and($updated)->toBe([
|
|
808
|
+
'json' => ['error' => 'No vendor resource found for the identifier provided.'],
|
|
809
|
+
'status' => 404,
|
|
810
|
+
]);
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
test('api driver controller clears a relationship when the input is sent empty', function () {
|
|
814
|
+
$controller = new FleetOpsApiDriverControllerProbe();
|
|
815
|
+
|
|
816
|
+
$input = $controller->inputForTest(new Request(['vehicle' => null, 'vendor' => '', 'job' => null]));
|
|
817
|
+
|
|
818
|
+
expect($input)->toMatchArray([
|
|
819
|
+
'vehicle_uuid' => null,
|
|
820
|
+
'vendor_uuid' => null,
|
|
821
|
+
'current_job_uuid' => null,
|
|
822
|
+
])->and($controller->relationLookups)->toBe([]);
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
test('api driver controller resolves driver relationships inside the company the driver is created in', function () {
|
|
826
|
+
// A create request may name a company explicitly, and the relationships must
|
|
827
|
+
// be looked up in that company rather than in whatever the session holds.
|
|
828
|
+
$controller = new FleetOpsApiDriverControllerProbe();
|
|
829
|
+
|
|
830
|
+
$controller->create(new CreateDriverRequest([
|
|
831
|
+
'company' => 'company_public',
|
|
832
|
+
'name' => 'Driver One',
|
|
833
|
+
'vehicle' => 'vehicle_public',
|
|
834
|
+
'vendor' => 'vendor_public',
|
|
835
|
+
]));
|
|
836
|
+
|
|
837
|
+
expect($controller->relationCompanyScopes)->toBe(['company-uuid', 'company-uuid']);
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
test('api driver controller keeps every relationship the base contract returned', function () {
|
|
841
|
+
// The SDK stores what the API returns verbatim, and Navigator interpolates
|
|
842
|
+
// `driver.user` straight into a socket channel name — an object there would
|
|
843
|
+
// subscribe it to `user.[object Object]` and quietly stop delivering
|
|
844
|
+
// messages. These endpoints have always loaded user, vehicle, vendor and
|
|
845
|
+
// currentJob, so they must keep doing it without a `with` parameter.
|
|
846
|
+
$controller = new FleetOpsApiDriverControllerProbe();
|
|
847
|
+
|
|
848
|
+
$controller->create(new CreateDriverRequest([
|
|
849
|
+
'name' => 'Driver One',
|
|
850
|
+
'vehicle' => 'vehicle_public',
|
|
851
|
+
'vendor' => 'vendor_public',
|
|
852
|
+
'job' => 'order_public',
|
|
853
|
+
]));
|
|
854
|
+
|
|
855
|
+
expect($controller->driver->loaded)->toContain(['user', 'vehicle', 'vendor', 'currentJob']);
|
|
856
|
+
|
|
857
|
+
$updateController = new FleetOpsApiDriverControllerProbe();
|
|
858
|
+
$updateController->update('driver_public', new UpdateDriverRequest(['name' => 'Renamed']));
|
|
859
|
+
|
|
860
|
+
expect($updateController->driver->loaded)->toContain(['user', 'vehicle', 'vendor', 'currentJob'])
|
|
861
|
+
->and($updateController->findCalls)->toContain(['driver_public', ['user']]);
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
test('api driver controller copies timezone through to the linked user account', function () {
|
|
865
|
+
// `timezone` is documented and accepted on both create and update, but the
|
|
866
|
+
// update only ever copied name, email and phone — so the request validated,
|
|
867
|
+
// answered 200, and dropped it. The driver has no timezone column; the
|
|
868
|
+
// linked user does.
|
|
869
|
+
$create = new FleetOpsApiDriverControllerProbe();
|
|
870
|
+
$create->create(new CreateDriverRequest([
|
|
871
|
+
'name' => 'Driver One',
|
|
872
|
+
'timezone' => 'Asia/Singapore',
|
|
873
|
+
]));
|
|
874
|
+
|
|
875
|
+
$user = new FleetOpsApiDriverUserFake();
|
|
876
|
+
$user->setRawAttributes(['uuid' => 'user-uuid'], true);
|
|
877
|
+
|
|
878
|
+
$driver = new FleetOpsApiDriverFake();
|
|
879
|
+
$driver->setRawAttributes(['uuid' => 'driver-uuid', 'public_id' => 'driver_public', 'user_uuid' => 'user-uuid'], true);
|
|
880
|
+
$driver->userForTest = $user;
|
|
881
|
+
$driver->setRelation('user', $user);
|
|
882
|
+
|
|
883
|
+
$update = new FleetOpsApiDriverControllerProbe();
|
|
884
|
+
$update->driver = $driver;
|
|
885
|
+
$update->update('driver_public', new UpdateDriverRequest([
|
|
886
|
+
'name' => 'Driver One',
|
|
887
|
+
'timezone' => 'Europe/Amsterdam',
|
|
888
|
+
]));
|
|
889
|
+
|
|
890
|
+
expect($create->createdUsers[0])->toMatchArray(['timezone' => 'Asia/Singapore'])
|
|
891
|
+
->and($user->updates)->toContain([
|
|
892
|
+
'name' => 'Driver One',
|
|
893
|
+
'timezone' => 'Europe/Amsterdam',
|
|
894
|
+
]);
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
test('api driver controller only expands relationships the public contract allows', function () {
|
|
898
|
+
$controller = new FleetOpsApiDriverControllerProbe();
|
|
899
|
+
$request = new CreateDriverRequest(['name' => 'Driver One', 'with' => ['vehicle', 'current_job', 'user', 'company', 'nope']]);
|
|
900
|
+
|
|
901
|
+
$controller->create($request);
|
|
902
|
+
|
|
903
|
+
// `user` and `company` are published as public-id strings. Expanding either
|
|
904
|
+
// would retype a released field, so neither is expandable at all; an unknown
|
|
905
|
+
// name is dropped rather than reaching Eloquent, where it would be a 500.
|
|
906
|
+
expect($request->input('with'))->toBe(['vehicle', 'currentJob']);
|
|
907
|
+
});
|