@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.
Files changed (109) hide show
  1. package/RELEASE.md +21 -17
  2. package/addon/components/ai/route-preview-map.js +1 -5
  3. package/addon/components/customer/orders.js +2 -1
  4. package/addon/components/driver/card.hbs +49 -0
  5. package/addon/components/driver/card.js +30 -0
  6. package/addon/components/map/leaflet-live-map.js +4 -19
  7. package/addon/components/modals/place-details.hbs +1 -1
  8. package/addon/components/modals/point-map.hbs +1 -1
  9. package/addon/components/orchestrator-workbench.js +2 -1
  10. package/addon/components/order/form/route.js +3 -0
  11. package/addon/components/order-tracking-lookup.js +2 -1
  12. package/addon/components/place/details.hbs +1 -1
  13. package/addon/components/positions-replay.js +4 -1
  14. package/addon/components/service-area/details.hbs +1 -1
  15. package/addon/components/widget/live-fleet.js +7 -7
  16. package/addon/components/zone/details.hbs +1 -1
  17. package/addon/controllers/management/drivers/index.js +22 -1
  18. package/addon/controllers/settings/map.js +16 -0
  19. package/addon/helpers/leaflet-tile-url.js +19 -0
  20. package/addon/services/map-settings.js +26 -1
  21. package/addon/templates/management/drivers/index.hbs +67 -33
  22. package/addon/templates/settings/map.hbs +10 -0
  23. package/addon/utils/leaflet-tile-url.js +31 -0
  24. package/addon/utils/prepare-place-for-save.js +13 -0
  25. package/app/components/driver/card.js +1 -0
  26. package/app/helpers/leaflet-tile-url.js +1 -0
  27. package/app/utils/leaflet-tile-url.js +1 -0
  28. package/composer.json +1 -1
  29. package/extension.json +1 -1
  30. package/index.js +11 -0
  31. package/package.json +2 -1
  32. package/server/migrations/2026_09_05_000001_add_unique_indexes_to_fleet_membership_pivots.php +129 -0
  33. package/server/src/Exceptions/PublicRelationNotFoundException.php +47 -0
  34. package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesFleetOpsApiResources.php +42 -7
  35. package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesPublicExpansions.php +187 -0
  36. package/server/src/Http/Controllers/Api/v1/DriverController.php +312 -59
  37. package/server/src/Http/Controllers/Api/v1/EntityController.php +1 -1
  38. package/server/src/Http/Controllers/Api/v1/FleetController.php +370 -33
  39. package/server/src/Http/Controllers/Api/v1/FuelReportController.php +1 -1
  40. package/server/src/Http/Controllers/Api/v1/IssueController.php +1 -1
  41. package/server/src/Http/Controllers/Api/v1/ManifestController.php +250 -0
  42. package/server/src/Http/Controllers/Api/v1/OrderController.php +1 -1
  43. package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +1 -1
  44. package/server/src/Http/Controllers/Api/v1/TrackingStatusController.php +1 -1
  45. package/server/src/Http/Controllers/Api/v1/VehicleController.php +139 -34
  46. package/server/src/Http/Controllers/Api/v1/ZoneController.php +1 -1
  47. package/server/src/Http/Controllers/Internal/v1/SettingController.php +22 -0
  48. package/server/src/Http/Filter/Concerns/ResolvesPublicRelationUuids.php +54 -0
  49. package/server/src/Http/Filter/DriverFilter.php +71 -15
  50. package/server/src/Http/Filter/FleetFilter.php +32 -33
  51. package/server/src/Http/Filter/FuelReportFilter.php +29 -0
  52. package/server/src/Http/Filter/IssueFilter.php +29 -0
  53. package/server/src/Http/Filter/VehicleFilter.php +57 -19
  54. package/server/src/Http/Requests/Concerns/ScopesPublicRelationRules.php +48 -0
  55. package/server/src/Http/Requests/CreateDriverRequest.php +100 -9
  56. package/server/src/Http/Requests/CreateFleetRequest.php +30 -2
  57. package/server/src/Http/Requests/CreateServiceRateRequest.php +7 -2
  58. package/server/src/Http/Requests/CreateVehicleRequest.php +150 -26
  59. package/server/src/Http/Resources/v1/Concerns/ResolvesPublicRelationFields.php +110 -0
  60. package/server/src/Http/Resources/v1/Driver.php +22 -2
  61. package/server/src/Http/Resources/v1/Fleet.php +38 -25
  62. package/server/src/Http/Resources/v1/Manifest.php +52 -0
  63. package/server/src/Http/Resources/v1/ManifestStop.php +47 -0
  64. package/server/src/Http/Resources/v1/OrderConfig.php +51 -4
  65. package/server/src/Http/Resources/v1/Vehicle.php +46 -4
  66. package/server/src/Models/Driver.php +2 -2
  67. package/server/src/Models/Place.php +1 -1
  68. package/server/src/routes.php +29 -0
  69. package/server/tests/ApiDriverControllerContractsTest.php +317 -13
  70. package/server/tests/ApiDriverPasswordContractsTest.php +457 -0
  71. package/server/tests/ApiEntityControllerContractsTest.php +1 -1
  72. package/server/tests/ApiEquipmentControllerContractsTest.php +2 -2
  73. package/server/tests/ApiFleetControllerContractsTest.php +354 -21
  74. package/server/tests/ApiFuelReportControllerContractsTest.php +1 -1
  75. package/server/tests/ApiIssueControllerContractsTest.php +1 -1
  76. package/server/tests/ApiManifestControllerContractsTest.php +599 -0
  77. package/server/tests/ApiOrderControllerContractsTest.php +1 -1
  78. package/server/tests/ApiPartControllerContractsTest.php +2 -2
  79. package/server/tests/ApiServiceAreaZoneControllerContractsTest.php +2 -2
  80. package/server/tests/ApiTrackingStatusControllerContractsTest.php +1 -1
  81. package/server/tests/ApiVehicleControllerContractsTest.php +210 -8
  82. package/server/tests/CompactResourceSerializationTest.php +13 -7
  83. package/server/tests/ControllerFilterContractsTest.php +132 -13
  84. package/server/tests/ControllerHelperContractsTest.php +6 -3
  85. package/server/tests/DriverFilterExecutionTest.php +130 -6
  86. package/server/tests/ExceptionContractsTest.php +15 -0
  87. package/server/tests/Feature/Http/Api/DeviceControllerContractsTest.php +1 -1
  88. package/server/tests/Feature/Http/Api/DriverControllerSessionHelpersTest.php +61 -0
  89. package/server/tests/Feature/Http/Api/FleetMembershipMigrationTest.php +140 -0
  90. package/server/tests/Feature/Http/Api/FleetMembershipTest.php +333 -0
  91. package/server/tests/Feature/Http/Api/FleetPublicContractTest.php +439 -0
  92. package/server/tests/Feature/Http/Api/FuelTransactionControllerContractsTest.php +2 -2
  93. package/server/tests/Feature/Http/Api/NullRelationshipInputTest.php +191 -0
  94. package/server/tests/Feature/Http/Api/PublicIdentifierFilterTest.php +293 -0
  95. package/server/tests/Feature/Http/Api/SmallApiControllerHelpersTest.php +7 -1
  96. package/server/tests/Feature/Http/Api/VehicleControllerTrackingTest.php +3 -1
  97. package/server/tests/Feature/Http/Api/VehiclePublicContractTest.php +454 -0
  98. package/server/tests/Feature/Http/Api/WorkOrderControllerContractsTest.php +1 -1
  99. package/server/tests/Feature/Http/Internal/PlaceControllerTest.php +45 -0
  100. package/server/tests/FleetHierarchyResourceTest.php +56 -8
  101. package/server/tests/RequestContractsTest.php +105 -7
  102. package/server/tests/SettingControllerContractsTest.php +33 -0
  103. package/server/tests/Unit/Http/Filter/FuelReportFilterTest.php +135 -0
  104. package/server/tests/Unit/Http/Filter/IssueFilterTest.php +43 -0
  105. package/server/tests/Unit/Http/Resources/FleetResourceTest.php +50 -6
  106. package/server/tests/Unit/Http/Resources/ManifestResourceTest.php +129 -0
  107. package/server/tests/Unit/Http/Resources/OrderConfigResourceTest.php +91 -0
  108. package/server/tests/VehicleControllerHelperContractsTest.php +1 -1
  109. package/translations/en-us.yaml +4 -0
@@ -2,11 +2,14 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Http\Requests;
4
4
 
5
+ use Fleetbase\FleetOps\Http\Requests\Concerns\ScopesPublicRelationRules;
5
6
  use Fleetbase\Http\Requests\FleetbaseRequest;
6
7
  use Illuminate\Validation\Rule;
7
8
 
8
9
  class CreateFleetRequest extends FleetbaseRequest
9
10
  {
11
+ use ScopesPublicRelationRules;
12
+
10
13
  /**
11
14
  * Determine if the user is authorized to make this request.
12
15
  */
@@ -21,8 +24,33 @@ class CreateFleetRequest extends FleetbaseRequest
21
24
  public function rules(): array
22
25
  {
23
26
  return [
24
- 'name' => [Rule::requiredIf($this->isMethod('POST'))],
25
- 'service_area' => 'exists:service_areas,public_id',
27
+ 'name' => [Rule::requiredIf($this->isMethod('POST')), 'nullable', 'string', 'max:191'],
28
+ 'color' => 'nullable|string|max:64',
29
+ 'task' => 'nullable|string|max:191',
30
+ /*
31
+ * Fleet status is not a closed set on this model: the console, the
32
+ * spreadsheet importer and existing integrations all write free-form
33
+ * values, and `active`/`disabled`/`decommissioned` are the console's
34
+ * options rather than a schema constraint. Restricting the public API
35
+ * to a subset would leave it unable to express a state the console can.
36
+ */
37
+ 'status' => 'nullable|string|max:64',
38
+ 'service_area' => $this->publicRelationRules('service_areas'),
39
+ 'zone' => $this->publicRelationRules('zones'),
40
+ 'vendor' => $this->publicRelationRules('vendors'),
41
+ 'parent_fleet' => $this->publicRelationRules('fleets'),
42
+ 'photo' => 'nullable|string',
43
+ ];
44
+ }
45
+
46
+ /**
47
+ * Get custom attributes for validator errors.
48
+ */
49
+ public function attributes(): array
50
+ {
51
+ return [
52
+ 'service_area' => 'service area',
53
+ 'parent_fleet' => 'parent fleet',
26
54
  ];
27
55
  }
28
56
  }
@@ -25,8 +25,13 @@ class CreateServiceRateRequest extends FleetbaseRequest
25
25
  return [
26
26
  'service_name' => [Rule::requiredIf($this->isMethod('POST')), 'string'],
27
27
  'service_type' => [Rule::requiredIf($this->isMethod('POST')), 'string'],
28
- 'service_area' => [Rule::exists('service_areas', 'public_id')->whereNull('deleted_at')],
29
- 'zone' => [Rule::exists('zones', 'public_id')->whereNull('deleted_at')],
28
+ // `nullable` so an empty relationship is treated as absent rather than
29
+ // as an invalid one. Both are optional, but without it `exists` runs
30
+ // against the null that ConvertEmptyStringsToNull produced and answers
31
+ // "The selected service area is invalid" for a field the caller simply
32
+ // did not fill in.
33
+ 'service_area' => ['nullable', Rule::exists('service_areas', 'public_id')->whereNull('deleted_at')],
34
+ 'zone' => ['nullable', Rule::exists('zones', 'public_id')->whereNull('deleted_at')],
30
35
  'rate_calculation_method' => [Rule::requiredIf($this->isMethod('POST')), 'string', 'in:fixed_meter,fixed_rate,per_meter,per_drop,algo,parcel'],
31
36
  'currency' => ['required', 'size:3'],
32
37
  'base_fee' => ['numeric'],
@@ -2,12 +2,45 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Http\Requests;
4
4
 
5
+ use Fleetbase\FleetOps\Http\Requests\Concerns\ScopesPublicRelationRules;
5
6
  use Fleetbase\FleetOps\Rules\ResolvablePoint;
6
7
  use Fleetbase\Http\Requests\FleetbaseRequest;
7
8
  use Illuminate\Validation\Rule;
8
9
 
9
10
  class CreateVehicleRequest extends FleetbaseRequest
10
11
  {
12
+ use ScopesPublicRelationRules;
13
+
14
+ /**
15
+ * The statuses the public API accepts for a vehicle.
16
+ *
17
+ * `active` is retained even though the model rewrites it to `available`, so
18
+ * clients written against the original contract keep working.
19
+ *
20
+ * @var array<int, string>
21
+ */
22
+ public const STATUSES = [
23
+ 'active',
24
+ 'available',
25
+ 'in_use',
26
+ 'maintenance',
27
+ 'out_of_service',
28
+ 'reserved',
29
+ 'retired',
30
+ 'staging',
31
+ 'on_route',
32
+ 'idle',
33
+ 'cleaning',
34
+ 'awaiting_parts',
35
+ 'inspection_due',
36
+ 'inspection_failed',
37
+ 'accident',
38
+ 'compliance_hold',
39
+ 'stolen',
40
+ 'operational',
41
+ 'decommissioned',
42
+ ];
43
+
11
44
  /**
12
45
  * Determine if the user is authorized to make this request.
13
46
  */
@@ -22,35 +55,126 @@ class CreateVehicleRequest extends FleetbaseRequest
22
55
  public function rules(): array
23
56
  {
24
57
  return [
25
- 'status' => [
26
- 'nullable',
27
- Rule::in([
28
- 'active',
29
- 'available',
30
- 'in_use',
31
- 'maintenance',
32
- 'out_of_service',
33
- 'reserved',
34
- 'retired',
35
- 'staging',
36
- 'on_route',
37
- 'idle',
38
- 'cleaning',
39
- 'awaiting_parts',
40
- 'inspection_due',
41
- 'inspection_failed',
42
- 'accident',
43
- 'compliance_hold',
44
- 'stolen',
45
- 'operational',
46
- 'decommissioned',
47
- ]),
48
- ],
49
- 'vendor' => 'nullable|exists:vendors,public_id',
50
- 'driver' => 'nullable|exists:drivers,public_id',
58
+ 'status' => ['nullable', Rule::in(static::STATUSES)],
59
+
60
+ // Identity and description
61
+ 'internal_id' => 'nullable|string|max:191',
62
+ 'name' => 'nullable|string|max:191',
63
+ 'description' => 'nullable|string',
64
+ 'make' => 'nullable|string|max:191',
65
+ 'model' => 'nullable|string|max:191',
66
+ 'model_type' => 'nullable|string|max:191',
67
+ 'year' => 'nullable|integer|min:1900|max:2100',
68
+ 'trim' => 'nullable|string|max:191',
69
+ 'color' => 'nullable|string|max:64',
70
+ 'type' => 'nullable|string|max:191',
71
+ 'class' => 'nullable|string|max:191',
72
+ 'plate_number' => 'nullable|string|max:191',
73
+ 'vin' => 'nullable|string|max:64',
74
+ 'serial_number' => 'nullable|string|max:191',
75
+ 'call_sign' => 'nullable|string|max:191',
76
+ 'fuel_card_number' => 'nullable|string|max:191',
77
+
78
+ // Measurement and operation.
79
+ //
80
+ // Validated rather than merely accepted: the model casts odometer
81
+ // to an integer, so an unchecked string would be stored as 0 — a
82
+ // vehicle reporting zero miles rather than an error.
83
+ 'odometer' => 'nullable|numeric|min:0',
84
+ 'odometer_unit' => 'nullable|string|max:12',
85
+ 'odometer_at_purchase' => 'nullable|numeric|min:0',
86
+ 'measurement_system' => 'nullable|string|max:32',
87
+ 'fuel_type' => 'nullable|string|max:64',
88
+ 'fuel_volume_unit' => 'nullable|string|max:12',
89
+ 'online' => 'nullable|boolean',
90
+
91
+ // Body, capacity and dimensions
92
+ 'transmission' => 'nullable|string|max:64',
93
+ 'body_type' => 'nullable|string|max:191',
94
+ 'body_sub_type' => 'nullable|string|max:191',
95
+ 'usage_type' => 'nullable|string|max:64',
96
+ 'ownership_type' => 'nullable|string|max:64',
97
+ 'cargo_volume' => 'nullable|numeric|min:0',
98
+ 'passenger_volume' => 'nullable|numeric|min:0',
99
+ 'interior_volume' => 'nullable|numeric|min:0',
100
+ 'weight' => 'nullable|numeric|min:0',
101
+ 'width' => 'nullable|numeric|min:0',
102
+ 'length' => 'nullable|numeric|min:0',
103
+ 'height' => 'nullable|numeric|min:0',
104
+ 'towing_capacity' => 'nullable|numeric|min:0',
105
+ 'payload_capacity' => 'nullable|numeric|min:0',
106
+ 'seating_capacity' => 'nullable|integer|min:0',
107
+ 'ground_clearance' => 'nullable|numeric|min:0',
108
+ 'bed_length' => 'nullable|numeric|min:0',
109
+ 'fuel_capacity' => 'nullable|numeric|min:0',
110
+
111
+ // Lifecycle and financing
112
+ 'financing_status' => 'nullable|string|max:64',
113
+ 'loan_number_of_payments' => 'nullable|integer|min:0',
114
+ 'loan_first_payment' => 'nullable|date',
115
+ 'loan_amount' => 'nullable|numeric|min:0',
116
+ 'estimated_service_life_distance_unit' => 'nullable|string|max:12',
117
+ 'estimated_service_life_distance' => 'nullable|integer|min:0',
118
+ 'estimated_service_life_months' => 'nullable|integer|min:0',
119
+ 'insurance_value' => 'nullable|numeric|min:0',
120
+ 'depreciation_rate' => 'nullable|numeric',
121
+ 'current_value' => 'nullable|numeric|min:0',
122
+ 'acquisition_cost' => 'nullable|numeric|min:0',
123
+ 'currency' => 'nullable|string|size:3',
124
+ 'purchased_at' => 'nullable|date',
125
+ 'lease_expires_at' => 'nullable|date',
126
+
127
+ // Regulatory and engine specifications
128
+ 'emission_standard' => 'nullable|string|max:64',
129
+ 'dpf_equipped' => 'nullable|boolean',
130
+ 'scr_equipped' => 'nullable|boolean',
131
+ 'gvwr' => 'nullable|numeric|min:0',
132
+ 'gcwr' => 'nullable|numeric|min:0',
133
+ 'engine_number' => 'nullable|string|max:191',
134
+ 'engine_model' => 'nullable|string|max:191',
135
+ 'engine_make' => 'nullable|string|max:191',
136
+ 'engine_family' => 'nullable|string|max:191',
137
+ 'engine_configuration' => 'nullable|string|max:191',
138
+ 'engine_displacement' => 'nullable|numeric|min:0',
139
+ 'engine_size' => 'nullable|numeric|min:0',
140
+ 'horsepower' => 'nullable|numeric|min:0',
141
+ 'horsepower_rpm' => 'nullable|integer|min:0',
142
+ 'torque' => 'nullable|numeric|min:0',
143
+ 'torque_rpm' => 'nullable|integer|min:0',
144
+ 'number_of_cylinders' => 'nullable|integer|min:0',
145
+ 'cylinder_arrangement' => 'nullable|string|max:64',
146
+
147
+ // Structured and descriptive fields
148
+ 'specs' => 'nullable|array',
149
+ 'details' => 'nullable|array',
150
+ 'notes' => 'nullable|string',
151
+ 'meta' => 'nullable|array',
152
+
153
+ // Orchestrator
154
+ 'skills' => 'nullable|array',
155
+ 'skills.*' => 'string',
156
+ 'payload_capacity_volume' => 'nullable|numeric|min:0',
157
+ 'payload_capacity_pallets' => 'nullable|integer|min:0',
158
+ 'payload_capacity_parcels' => 'nullable|integer|min:0',
159
+ 'max_tasks' => 'nullable|integer|min:0',
160
+ 'time_window_start' => 'nullable|date_format:H:i,H:i:s',
161
+ 'time_window_end' => 'nullable|date_format:H:i,H:i:s',
162
+ 'return_to_depot' => 'nullable|boolean',
163
+
164
+ // Relationships, resolved from public ids inside the caller's company
165
+ 'vendor' => $this->publicRelationRules('vendors'),
166
+ 'driver' => $this->publicRelationRules('drivers'),
167
+ 'category' => $this->publicRelationRules('categories'),
168
+ 'warranty' => $this->publicRelationRules('warranties'),
169
+ 'photo' => 'nullable|string',
170
+
171
+ // Location
51
172
  'location' => ['nullable', new ResolvablePoint()],
52
173
  'latitude' => ['nullable', 'required_with:longitude'],
53
174
  'longitude' => ['nullable', 'required_with:latitude'],
175
+ 'altitude' => 'nullable|numeric',
176
+ 'heading' => 'nullable|numeric',
177
+ 'speed' => 'nullable|numeric',
54
178
  ];
55
179
  }
56
180
  }
@@ -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
  ];
@@ -0,0 +1,52 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Http\Resources\v1;
4
+
5
+ use Fleetbase\Http\Resources\FleetbaseResource;
6
+ use Fleetbase\Support\Http;
7
+
8
+ /**
9
+ * Public projection of a Manifest — a driver's route for a day.
10
+ *
11
+ * A manifest is order-agnostic: it is a sequence of stops, which may belong to
12
+ * several orders or to none the driver has ever seen as an order. That is why
13
+ * this exposes the stops directly rather than making a consumer assemble them
14
+ * from orders.
15
+ *
16
+ * Totals ride along because a driver wants to know what the day looks like
17
+ * before starting it, and computing them client-side from the stop list gives a
18
+ * different answer than the server's own.
19
+ */
20
+ class Manifest extends FleetbaseResource
21
+ {
22
+ /**
23
+ * Transform the resource into an array.
24
+ *
25
+ * @param \Illuminate\Http\Request $request
26
+ */
27
+ public function toArray($request): array
28
+ {
29
+ return [
30
+ 'id' => $this->when(Http::isInternalRequest(), $this->id, $this->public_id),
31
+ 'uuid' => $this->when(Http::isInternalRequest(), $this->uuid),
32
+ 'public_id' => $this->when(Http::isInternalRequest(), $this->public_id),
33
+ 'status' => $this->status,
34
+ 'scheduled_date' => $this->scheduled_date,
35
+ 'started_at' => $this->started_at,
36
+ 'completed_at' => $this->completed_at,
37
+ 'total_distance_m' => $this->total_distance_m,
38
+ 'total_duration_s' => $this->total_duration_s,
39
+ 'stop_count' => $this->stop_count,
40
+ 'completed_stops' => $this->completed_stops,
41
+ 'pending_stops' => $this->pending_stops,
42
+ 'driver_name' => $this->driver_name,
43
+ 'vehicle_name' => $this->vehicle_name,
44
+ 'notes' => $this->notes,
45
+ // Present only when the manifest was loaded with them, so a list
46
+ // endpoint stays a list.
47
+ 'stops' => ManifestStop::collection($this->whenLoaded('stops')),
48
+ 'updated_at' => $this->updated_at,
49
+ 'created_at' => $this->created_at,
50
+ ];
51
+ }
52
+ }
@@ -0,0 +1,47 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Http\Resources\v1;
4
+
5
+ use Fleetbase\Http\Resources\FleetbaseResource;
6
+ use Fleetbase\Support\Http;
7
+
8
+ /**
9
+ * Public projection of a ManifestStop.
10
+ *
11
+ * Carries the place inline rather than as an id. A driver app rendering a route
12
+ * needs a name and a coordinate for every stop, and making it resolve each one
13
+ * separately turns a route of twenty stops into twenty-one requests.
14
+ *
15
+ * `sequence` is the order to drive them in, and is the field a re-sequence
16
+ * rewrites.
17
+ */
18
+ class ManifestStop extends FleetbaseResource
19
+ {
20
+ /**
21
+ * Transform the resource into an array.
22
+ *
23
+ * @param \Illuminate\Http\Request $request
24
+ */
25
+ public function toArray($request): array
26
+ {
27
+ return [
28
+ 'id' => $this->when(Http::isInternalRequest(), $this->id, $this->public_id),
29
+ 'uuid' => $this->when(Http::isInternalRequest(), $this->uuid),
30
+ 'public_id' => $this->when(Http::isInternalRequest(), $this->public_id),
31
+ 'status' => $this->status,
32
+ 'sequence' => $this->sequence,
33
+ 'estimated_arrival' => $this->estimated_arrival,
34
+ 'actual_arrival' => $this->actual_arrival,
35
+ 'distance_from_prev_m' => $this->distance_from_prev_m,
36
+ 'duration_from_prev_s' => $this->duration_from_prev_s,
37
+ 'place' => new Place($this->whenLoaded('place')),
38
+ 'order' => $this->when($this->relationLoaded('order') && $this->order, fn () => [
39
+ 'id' => data_get($this->order, 'public_id'),
40
+ 'tracking_number' => data_get($this->order, 'trackingNumber.tracking_number'),
41
+ 'status' => data_get($this->order, 'status'),
42
+ ]),
43
+ 'updated_at' => $this->updated_at,
44
+ 'created_at' => $this->created_at,
45
+ ];
46
+ }
47
+ }