@fleetbase/fleetops-engine 0.6.61 → 0.6.63

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 (68) hide show
  1. package/RELEASE.md +5 -14
  2. package/addon/controllers/management/drivers/index.js +4 -1
  3. package/addon/routes/management/fleets/index/details.js +1 -1
  4. package/addon/routes/management/fleets/index/edit.js +1 -1
  5. package/addon/routes/management/fleets/index.js +3 -1
  6. package/addon/services/map-settings.js +4 -1
  7. package/composer.json +1 -1
  8. package/extension.json +1 -1
  9. package/package.json +1 -1
  10. package/server/migrations/2026_09_05_000001_add_unique_indexes_to_fleet_membership_pivots.php +129 -0
  11. package/server/src/Exceptions/PublicRelationNotFoundException.php +47 -0
  12. package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesFleetOpsApiResources.php +42 -7
  13. package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesPublicExpansions.php +187 -0
  14. package/server/src/Http/Controllers/Api/v1/DriverController.php +93 -57
  15. package/server/src/Http/Controllers/Api/v1/EntityController.php +1 -1
  16. package/server/src/Http/Controllers/Api/v1/FleetController.php +370 -33
  17. package/server/src/Http/Controllers/Api/v1/FuelReportController.php +1 -1
  18. package/server/src/Http/Controllers/Api/v1/IssueController.php +1 -1
  19. package/server/src/Http/Controllers/Api/v1/OrderController.php +1 -1
  20. package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +1 -1
  21. package/server/src/Http/Controllers/Api/v1/TrackingStatusController.php +1 -1
  22. package/server/src/Http/Controllers/Api/v1/VehicleController.php +134 -35
  23. package/server/src/Http/Controllers/Api/v1/ZoneController.php +1 -1
  24. package/server/src/Http/Filter/Concerns/ResolvesPublicRelationUuids.php +54 -0
  25. package/server/src/Http/Filter/DriverFilter.php +71 -15
  26. package/server/src/Http/Filter/FleetFilter.php +32 -33
  27. package/server/src/Http/Filter/VehicleFilter.php +57 -19
  28. package/server/src/Http/Requests/Concerns/ScopesPublicRelationRules.php +48 -0
  29. package/server/src/Http/Requests/CreateDriverRequest.php +100 -9
  30. package/server/src/Http/Requests/CreateFleetRequest.php +30 -2
  31. package/server/src/Http/Requests/CreateServiceRateRequest.php +7 -2
  32. package/server/src/Http/Requests/CreateVehicleRequest.php +150 -31
  33. package/server/src/Http/Resources/v1/Concerns/ResolvesPublicRelationFields.php +110 -0
  34. package/server/src/Http/Resources/v1/Driver.php +22 -2
  35. package/server/src/Http/Resources/v1/Fleet.php +38 -25
  36. package/server/src/Http/Resources/v1/Vehicle.php +46 -4
  37. package/server/src/Models/Driver.php +2 -2
  38. package/server/src/routes.php +8 -0
  39. package/server/tests/ApiDriverControllerContractsTest.php +284 -13
  40. package/server/tests/ApiEntityControllerContractsTest.php +1 -1
  41. package/server/tests/ApiEquipmentControllerContractsTest.php +2 -2
  42. package/server/tests/ApiFleetControllerContractsTest.php +354 -21
  43. package/server/tests/ApiFuelReportControllerContractsTest.php +1 -1
  44. package/server/tests/ApiIssueControllerContractsTest.php +1 -1
  45. package/server/tests/ApiOrderControllerContractsTest.php +1 -1
  46. package/server/tests/ApiPartControllerContractsTest.php +2 -2
  47. package/server/tests/ApiServiceAreaZoneControllerContractsTest.php +2 -2
  48. package/server/tests/ApiTrackingStatusControllerContractsTest.php +1 -1
  49. package/server/tests/ApiVehicleControllerContractsTest.php +175 -8
  50. package/server/tests/ControllerFilterContractsTest.php +132 -13
  51. package/server/tests/ControllerHelperContractsTest.php +6 -3
  52. package/server/tests/DriverFilterExecutionTest.php +130 -6
  53. package/server/tests/ExceptionContractsTest.php +15 -0
  54. package/server/tests/Feature/Http/Api/DeviceControllerContractsTest.php +1 -1
  55. package/server/tests/Feature/Http/Api/FleetMembershipMigrationTest.php +140 -0
  56. package/server/tests/Feature/Http/Api/FleetMembershipTest.php +333 -0
  57. package/server/tests/Feature/Http/Api/FleetPublicContractTest.php +439 -0
  58. package/server/tests/Feature/Http/Api/FuelTransactionControllerContractsTest.php +2 -2
  59. package/server/tests/Feature/Http/Api/NullRelationshipInputTest.php +191 -0
  60. package/server/tests/Feature/Http/Api/PublicIdentifierFilterTest.php +293 -0
  61. package/server/tests/Feature/Http/Api/SmallApiControllerHelpersTest.php +7 -1
  62. package/server/tests/Feature/Http/Api/VehicleControllerTrackingTest.php +3 -1
  63. package/server/tests/Feature/Http/Api/VehiclePublicContractTest.php +454 -0
  64. package/server/tests/Feature/Http/Api/WorkOrderControllerContractsTest.php +1 -1
  65. package/server/tests/FleetHierarchyResourceTest.php +56 -8
  66. package/server/tests/RequestContractsTest.php +100 -7
  67. package/server/tests/Unit/Http/Resources/FleetResourceTest.php +50 -6
  68. package/server/tests/VehicleControllerHelperContractsTest.php +1 -1
@@ -0,0 +1,110 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Http\Resources\v1\Concerns;
4
+
5
+ use Fleetbase\Support\Http;
6
+ use Illuminate\Http\Resources\MissingValue;
7
+
8
+ /**
9
+ * Renders a relationship twice: as an additive `<name>_id` public identifier, and
10
+ * as the nested object under the name the endpoint has always used.
11
+ *
12
+ * The two are deliberately separate keys. The SDK stores whatever the API returns
13
+ * verbatim — `Resource::$attributes = $attributes`, no normalisation — so a
14
+ * property that is an object on one call and a string on another silently breaks
15
+ * every consumer that dereferences it. Navigator interpolates `driver.user`
16
+ * straight into a socket channel name; an object there would subscribe it to
17
+ * `user.[object Object]` and simply stop delivering messages. So an existing
18
+ * object key stays an object key, always, and the identifier arrives beside it.
19
+ */
20
+ trait ResolvesPublicRelationFields
21
+ {
22
+ /**
23
+ * The relations the caller asked for, as real Eloquent relation names.
24
+ *
25
+ * The controller has already normalised, mapped and allowlisted `with` /
26
+ * `expand` by the time a resource runs, so this is a plain read.
27
+ *
28
+ * @return array<int, string>
29
+ */
30
+ protected function requestedRelations($request): array
31
+ {
32
+ $with = $request->input('with');
33
+
34
+ if (is_string($with)) {
35
+ $with = explode(',', $with);
36
+ }
37
+
38
+ if (!is_array($with)) {
39
+ return [];
40
+ }
41
+
42
+ return array_values(array_filter(array_map('strval', $with), 'strlen'));
43
+ }
44
+
45
+ /**
46
+ * The nested object for a relationship.
47
+ *
48
+ * Internal console requests keep the exact `whenLoaded` behaviour they have
49
+ * always had. Public requests get the object only when they asked for it,
50
+ * which is also what they got before: these relations were never eager
51
+ * loaded on the public endpoints, so the key was absent unless `with` named
52
+ * it. Absent stays absent; it never becomes a string.
53
+ *
54
+ * @param array<int, string> $with camelCased relations the caller asked for
55
+ */
56
+ protected function publicRelationObject(string $relation, array $with, \Closure $resource): mixed
57
+ {
58
+ if (Http::isInternalRequest()) {
59
+ return $this->whenLoaded($relation, $resource);
60
+ }
61
+
62
+ if (!in_array($relation, $with, true)) {
63
+ return new MissingValue();
64
+ }
65
+
66
+ $this->loadRelationIfPossible($relation);
67
+
68
+ return $this->{$relation} ? $resource() : null;
69
+ }
70
+
71
+ /**
72
+ * The public id behind a relationship, or null when nothing is assigned.
73
+ *
74
+ * Additive and unconditional: it is present whether or not the object is,
75
+ * and it does not change when the object is expanded.
76
+ *
77
+ * @param string|null $foreignKey the column holding the relation for a belongsTo;
78
+ * null for the inverse side, which has no local column
79
+ */
80
+ protected function publicIdForRelation(string $relation, ?string $foreignKey): ?string
81
+ {
82
+ if ($foreignKey !== null && empty($this->{$foreignKey})) {
83
+ return null;
84
+ }
85
+
86
+ $resource = $this->resource;
87
+
88
+ if (is_object($resource) && method_exists($resource, 'relationLoaded') && $resource->relationLoaded($relation)) {
89
+ return data_get($this, $relation . '.public_id');
90
+ }
91
+
92
+ // Not every resource wraps an Eloquent model — webhook payload fixtures and
93
+ // compact serializers pass plain objects — so read the already-materialised
94
+ // value rather than calling a relation method that is not there.
95
+ if (!is_object($resource) || !method_exists($resource, $relation)) {
96
+ return data_get($this, $relation . '.public_id');
97
+ }
98
+
99
+ return $this->{$relation}()->value('public_id');
100
+ }
101
+
102
+ private function loadRelationIfPossible(string $relation): void
103
+ {
104
+ $resource = $this->resource;
105
+
106
+ if (is_object($resource) && method_exists($resource, 'loadMissing') && method_exists($resource, $relation)) {
107
+ $this->loadMissing($relation);
108
+ }
109
+ }
110
+ }
@@ -37,6 +37,7 @@ class Driver extends FleetbaseResource
37
37
  'name' => $this->name,
38
38
  'email' => $this->email,
39
39
  'phone' => $this->phone,
40
+ 'timezone' => data_get($this, 'user.timezone'),
40
41
  'drivers_license_number' => $this->drivers_license_number,
41
42
  'license_expiry' => $this->formatDateOnly($this->license_expiry),
42
43
  'photo_url' => $this->photo_url,
@@ -45,25 +46,38 @@ class Driver extends FleetbaseResource
45
46
  'vehicle_name' => $this->when(Http::isInternalRequest(), $this->vehicle_name),
46
47
  'vehicle_avatar' => $this->vehicle_avatar,
47
48
  'vendor_name' => $this->when(Http::isInternalRequest(), $this->vendor_name),
48
- 'assigned_orders_count' => $this->when(Http::isInternalRequest(), $this->assignedOrdersCount()),
49
- 'current_order_reference' => $this->when(Http::isInternalRequest(), $this->currentOrderReference()),
49
+ 'assigned_orders_count' => $this->when(Http::isInternalRequest(), fn () => $this->assignedOrdersCount()),
50
+ 'current_order_reference' => $this->when(Http::isInternalRequest(), fn () => $this->currentOrderReference()),
50
51
  'vehicle' => $this->whenLoaded('vehicle', fn () => new VehicleWithoutDriver($this->vehicle)),
51
52
  'current_job' => $this->whenLoaded('currentJob', fn () => new Order($this->currentJob)),
52
53
  'current_job_id' => $this->when(Http::isInternalRequest(), data_get($this, 'currentJob.tracking')),
53
54
  'jobs' => $this->whenLoaded('jobs', fn () => $this->getJobs()),
54
55
  'vendor' => $this->whenLoaded('vendor', fn () => new Vendor($this->vendor)),
56
+ // Public callers receive each assignment as a public id so a write
57
+ // can be read back; the nested objects above are unchanged.
58
+ 'vehicle_id' => $this->when(Http::isPublicRequest(), fn () => $this->vehicle_id),
59
+ 'vendor_id' => $this->when(Http::isPublicRequest(), fn () => $this->vendor_id),
60
+ 'job_id' => $this->when(Http::isPublicRequest(), fn () => data_get($this, 'currentJob.public_id')),
55
61
  'fleets' => $this->whenLoaded('fleets', fn () => Fleet::collection($this->fleets()->without('drivers')->get())),
56
62
  'current_shift' => $this->whenLoaded('currentShift', fn () => $this->currentShift),
57
63
  'location' => $this->wasRecentlyCreated ? new Point(0, 0) : Utils::castPoint($this->location),
58
64
  'heading' => (int) data_get($this, 'heading', 0),
65
+ 'bearing' => data_get($this, 'bearing'),
59
66
  'altitude' => (int) data_get($this, 'altitude', 0),
60
67
  'speed' => (int) data_get($this, 'speed', 0),
61
68
  'country' => data_get($this, 'country'),
62
69
  'currency' => data_get($this, 'currency', Utils::getCurrenyFromCountryCode($this->country)),
63
70
  'city' => data_get($this, 'city', Utils::getCapitalCityFromCountryCode($this->country)),
64
71
  'online' => data_get($this, 'online', false),
72
+ 'current_status' => data_get($this, 'current_status'),
65
73
  'status' => $this->status,
66
74
  'token' => $this->token,
75
+ // Orchestrator constraints
76
+ 'skills' => data_get($this, 'skills'),
77
+ 'max_travel_time' => $this->max_travel_time,
78
+ 'max_distance' => $this->max_distance,
79
+ 'time_window_start' => $this->time_window_start,
80
+ 'time_window_end' => $this->time_window_end,
67
81
  'meta' => data_get($this, 'meta', Utils::createObject()),
68
82
  'updated_at' => $this->updated_at,
69
83
  'created_at' => $this->created_at,
@@ -131,7 +145,13 @@ class Driver extends FleetbaseResource
131
145
  'currency' => data_get($this, 'currency', Utils::getCurrenyFromCountryCode($this->country)),
132
146
  'city' => data_get($this, 'city', Utils::getCapitalCityFromCountryCode($this->country)),
133
147
  'online' => data_get($this, 'online', false),
148
+ 'current_status' => data_get($this, 'current_status'),
134
149
  'status' => $this->status,
150
+ 'skills' => data_get($this, 'skills'),
151
+ 'max_travel_time' => $this->max_travel_time,
152
+ 'max_distance' => $this->max_distance,
153
+ 'time_window_start' => $this->time_window_start,
154
+ 'time_window_end' => $this->time_window_end,
135
155
  'meta' => data_get($this, 'meta', Utils::createObject()),
136
156
  'updated_at' => $this->updated_at,
137
157
  'created_at' => $this->created_at,
@@ -2,12 +2,15 @@
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\Http\Resources\FleetbaseResource;
6
7
  use Fleetbase\Support\Http;
7
- use Illuminate\Support\Str;
8
+ use Fleetbase\Support\Resolve;
8
9
 
9
10
  class Fleet extends FleetbaseResource
10
11
  {
12
+ use ResolvesPublicRelationFields;
13
+
11
14
  /**
12
15
  * Transform the resource into an array.
13
16
  *
@@ -17,22 +20,15 @@ class Fleet extends FleetbaseResource
17
20
  */
18
21
  public function toArray($request)
19
22
  {
20
- if ($request->isArray('with')) {
21
- $with = array_map(function ($relation) {
22
- return Str::camel($relation);
23
- }, $request->array('with'));
24
-
25
- $this->load($with);
26
-
27
- if (in_array('subfleets', $with, true)) {
28
- if (in_array('drivers', $with, true)) {
29
- $this->loadMissing('subFleets.drivers');
30
- }
23
+ // The controller has already mapped these onto real relation names and
24
+ // dropped anything outside the public allowlist, so they are safe to
25
+ // hand to the loader. `subfleets` used to arrive here spelled exactly
26
+ // that and reach `load('subfleets')`, which is not the relation —
27
+ // the documented expansion raised instead of resolving.
28
+ $with = $this->requestedRelations($request);
31
29
 
32
- if (in_array('vehicles', $with, true)) {
33
- $this->loadMissing('subFleets.vehicles');
34
- }
35
- }
30
+ if ($with !== []) {
31
+ $this->loadMissing($with);
36
32
  }
37
33
 
38
34
  return $this->withCustomFields([
@@ -40,16 +36,31 @@ class Fleet extends FleetbaseResource
40
36
  'uuid' => $this->when(Http::isInternalRequest(), $this->uuid),
41
37
  'public_id' => $this->when(Http::isInternalRequest(), $this->public_id),
42
38
  'name' => $this->name,
39
+ 'color' => $this->color ?? null,
43
40
  'task' => $this->task ?? null,
44
41
  'status' => $this->status ?? null,
45
- 'drivers_count' => $this->when(Http::isInternalRequest(), $this->drivers_count),
46
- 'drivers_online_count' => $this->when(Http::isInternalRequest(), $this->drivers_online_count),
47
- 'vehicles_count' => $this->when(Http::isInternalRequest(), $this->vehicles_count),
48
- 'vehicles_online_count' => $this->when(Http::isInternalRequest(), $this->vehicles_online_count),
49
- 'service_area' => $this->whenLoaded('serviceArea', fn () => new ServiceArea($this->serviceArea)),
50
- 'zone' => $this->whenLoaded('zone', fn () => new Zone($this->zone)),
51
- 'vendor' => $this->whenLoaded('vendor', fn () => new Vendor($this->vendor)),
52
- 'parent_fleet' => $this->whenLoaded('parentFleet', fn () => new ParentFleet($this->parentFleet)),
42
+ 'photo_url' => $this->photo_url,
43
+ // Closures, not values: `when()` evaluates a plain second argument
44
+ // eagerly, so each of these counts ran a query on every public
45
+ // request that then discarded the result.
46
+ 'drivers_count' => $this->when(Http::isInternalRequest(), fn () => $this->drivers_count),
47
+ 'drivers_online_count' => $this->when(Http::isInternalRequest(), fn () => $this->drivers_online_count),
48
+ 'vehicles_count' => $this->when(Http::isInternalRequest(), fn () => $this->vehicles_count),
49
+ 'vehicles_online_count' => $this->when(Http::isInternalRequest(), fn () => $this->vehicles_online_count),
50
+ // Additive public identifiers. Always present, unchanged when the
51
+ // object beside them is expanded, and never a substitute for it.
52
+ 'service_area_id' => $this->publicIdForRelation('serviceArea', 'service_area_uuid'),
53
+ 'zone_id' => $this->publicIdForRelation('zone', 'zone_uuid'),
54
+ 'vendor_id' => $this->publicIdForRelation('vendor', 'vendor_uuid'),
55
+ 'parent_fleet_id' => $this->publicIdForRelation('parentFleet', 'parent_fleet_uuid'),
56
+ 'photo_id' => $this->publicIdForRelation('photo', 'image_uuid'),
57
+ // The objects keep the shape they have always had: absent unless the
58
+ // relation was loaded, an object when it was, never a string.
59
+ 'service_area' => $this->publicRelationObject('serviceArea', $with, fn () => new ServiceArea($this->serviceArea)),
60
+ 'zone' => $this->publicRelationObject('zone', $with, fn () => new Zone($this->zone)),
61
+ 'vendor' => $this->publicRelationObject('vendor', $with, fn () => new Vendor($this->vendor)),
62
+ 'parent_fleet' => $this->publicRelationObject('parentFleet', $with, fn () => new ParentFleet($this->parentFleet)),
63
+ 'photo' => $this->publicRelationObject('photo', $with, fn () => Resolve::httpResourceForModel($this->photo)),
53
64
  'subfleets' => $this->whenLoaded('subFleets', fn () => SubFleet::collection($this->subFleets)),
54
65
  'drivers' => $this->whenLoaded('drivers', fn () => Driver::collection($this->drivers()->with(Http::isInternalRequest() || $request->has('with.jobs') ? ['jobs'] : [])->get())),
55
66
  'vehicles' => $this->whenLoaded('vehicles', fn () => Vehicle::collection($this->vehicles)),
@@ -68,11 +79,13 @@ class Fleet extends FleetbaseResource
68
79
  return [
69
80
  'id' => $this->public_id,
70
81
  'name' => $this->name,
82
+ 'color' => $this->color ?? null,
71
83
  'task' => $this->task ?? null,
72
84
  'status' => $this->status ?? null,
73
- 'parent_fleet' => $this->when($this->serviceArea, data_get($this, 'parentFleet.public_id')),
85
+ 'parent_fleet' => $this->when($this->parentFleet, data_get($this, 'parentFleet.public_id')),
74
86
  'service_area' => $this->when($this->serviceArea, data_get($this, 'serviceArea.public_id')),
75
87
  'zone' => $this->when($this->zone, data_get($this, 'zone.public_id')),
88
+ 'vendor' => $this->when($this->vendor, data_get($this, 'vendor.public_id')),
76
89
  'updated_at' => $this->updated_at,
77
90
  'created_at' => $this->created_at,
78
91
  ];
@@ -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
- 'driver' => $this->whenLoaded('driver', fn () => new Driver($this->driver)),
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
  /**
@@ -302,6 +302,14 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
302
302
  $router->group(['prefix' => 'fleets'], function () use ($router) {
303
303
  $router->post('/', 'FleetController@create');
304
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');
305
313
  $router->get('{id}', 'FleetController@find');
306
314
  $router->put('{id}', 'FleetController@update');
307
315
  $router->delete('{id}', 'FleetController@delete');