@fleetbase/fleetops-engine 0.6.63 → 0.6.65

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 (289) hide show
  1. package/RELEASE.md +7 -6
  2. package/addon/components/attachment-list.hbs +65 -0
  3. package/addon/components/attachment-list.js +8 -0
  4. package/addon/components/cell/attached-vehicle.hbs +1 -1
  5. package/addon/components/cell/attached-vehicle.js +3 -3
  6. package/addon/components/cell/driver-identity.js +4 -4
  7. package/addon/components/cell/driver-name.hbs +1 -1
  8. package/addon/components/cell/translated-value.hbs +9 -0
  9. package/addon/components/cell/translated-value.js +57 -0
  10. package/addon/components/contact/card.hbs +58 -0
  11. package/addon/components/contact/card.js +19 -0
  12. package/addon/components/contact/form.hbs +1 -1
  13. package/addon/components/contact/panel-header.hbs +1 -1
  14. package/addon/components/customer/form.hbs +2 -2
  15. package/addon/components/device/manager.hbs +26 -28
  16. package/addon/components/device/manager.js +28 -2
  17. package/addon/components/device/panel-tabs/vehicle.hbs +13 -13
  18. package/addon/components/device/panel-tabs/vehicle.js +8 -0
  19. package/addon/components/driver/card.hbs +24 -13
  20. package/addon/components/driver/card.js +20 -6
  21. package/addon/components/driver/form.hbs +3 -3
  22. package/addon/components/driver/panel-header.hbs +2 -2
  23. package/addon/components/driver/pill.hbs +2 -2
  24. package/addon/components/equipment/form.js +11 -4
  25. package/addon/components/equipment/pill.hbs +20 -0
  26. package/addon/components/equipment/pill.js +3 -0
  27. package/addon/components/filter/multi-model.hbs +19 -0
  28. package/addon/components/filter/multi-model.js +90 -0
  29. package/addon/components/fleet/driver-listing.hbs +1 -1
  30. package/addon/components/fleet/vehicle-listing.hbs +1 -1
  31. package/addon/components/fleet-panel/driver-listing.hbs +1 -1
  32. package/addon/components/fleet-panel/vehicle-listing.hbs +1 -1
  33. package/addon/components/layout/fleet-ops-sidebar.js +9 -7
  34. package/addon/components/maintenance/form.js +12 -4
  35. package/addon/components/maintenance-schedule/form.js +13 -7
  36. package/addon/components/map/leaflet-live-map.hbs +52 -0
  37. package/addon/components/map/order-list-overlay/order.hbs +4 -4
  38. package/addon/components/modals/attach-equipment.hbs +22 -0
  39. package/addon/components/modals/attach-equipment.js +3 -0
  40. package/addon/components/modals/attach-telematic-device.hbs +13 -8
  41. package/addon/components/modals/attach-telematic-device.js +11 -1
  42. package/addon/components/modals/attach-trailer.hbs +30 -0
  43. package/addon/components/modals/attach-trailer.js +3 -0
  44. package/addon/components/modals/contact-form.hbs +2 -2
  45. package/addon/components/modals/convert-customer-to-vendor.hbs +1 -1
  46. package/addon/components/modals/driver-form.hbs +2 -2
  47. package/addon/components/modals/select-trailer.hbs +35 -0
  48. package/addon/components/modals/select-trailer.js +3 -0
  49. package/addon/components/modals/vehicle-form.hbs +2 -2
  50. package/addon/components/orchestrator/resource-panel.hbs +2 -2
  51. package/addon/components/order/customer-avatar-stack.hbs +2 -2
  52. package/addon/components/order/details/detail.hbs +4 -4
  53. package/addon/components/order/details/payload.hbs +2 -2
  54. package/addon/components/order/kanban-card.hbs +4 -4
  55. package/addon/components/order-list-overlay/order.hbs +4 -4
  56. package/addon/components/trailer/card.hbs +68 -0
  57. package/addon/components/trailer/card.js +22 -0
  58. package/addon/components/trailer/details/connections.hbs +57 -0
  59. package/addon/components/trailer/details/connections.js +59 -0
  60. package/addon/components/trailer/details/equipment.hbs +25 -0
  61. package/addon/components/trailer/details/equipment.js +52 -0
  62. package/addon/components/trailer/details/maintenance-history.hbs +47 -0
  63. package/addon/components/trailer/details/maintenance-history.js +40 -0
  64. package/addon/components/trailer/details/schedules.hbs +45 -0
  65. package/addon/components/trailer/details/schedules.js +40 -0
  66. package/addon/components/trailer/details/work-orders.hbs +47 -0
  67. package/addon/components/trailer/details/work-orders.js +40 -0
  68. package/addon/components/trailer/details.hbs +399 -0
  69. package/addon/components/trailer/details.js +118 -0
  70. package/addon/components/trailer/form.hbs +447 -0
  71. package/addon/components/trailer/form.js +165 -0
  72. package/addon/components/trailer/panel-header.hbs +45 -0
  73. package/addon/components/trailer/panel-header.js +3 -0
  74. package/addon/components/trailer/pill.hbs +21 -0
  75. package/addon/components/trailer/pill.js +3 -0
  76. package/addon/components/vehicle/card.hbs +42 -11
  77. package/addon/components/vehicle/card.js +31 -0
  78. package/addon/components/vehicle/details/equipment.hbs +25 -0
  79. package/addon/components/vehicle/details/equipment.js +55 -0
  80. package/addon/components/vehicle/details/trailers.hbs +35 -0
  81. package/addon/components/vehicle/details/trailers.js +55 -0
  82. package/addon/components/vehicle/form.hbs +2 -2
  83. package/addon/components/vehicle/panel-header.hbs +2 -2
  84. package/addon/components/vehicle/pill.hbs +2 -2
  85. package/addon/components/vendor/card.hbs +58 -0
  86. package/addon/components/vendor/card.js +6 -0
  87. package/addon/components/vendor/panel-header.hbs +2 -2
  88. package/addon/components/vendor/personnel-manager.hbs +2 -2
  89. package/addon/components/work-order/form.js +12 -4
  90. package/addon/controllers/connectivity/devices/index/details/vehicle.js +11 -0
  91. package/addon/controllers/connectivity/devices/index/details.js +3 -3
  92. package/addon/controllers/connectivity/devices/index.js +1 -0
  93. package/addon/controllers/connectivity/telematics/details/attachments.js +28 -47
  94. package/addon/controllers/connectivity/telematics/details/devices.js +1 -0
  95. package/addon/controllers/management/contacts/customers.js +4 -0
  96. package/addon/controllers/management/contacts/index.js +32 -0
  97. package/addon/controllers/management/trailers/index/details.js +119 -0
  98. package/addon/controllers/management/trailers/index/edit.js +67 -0
  99. package/addon/controllers/management/trailers/index/new.js +43 -0
  100. package/addon/controllers/management/trailers/index.js +599 -0
  101. package/addon/controllers/management/vehicles/index/details.js +16 -0
  102. package/addon/controllers/management/vehicles/index.js +78 -4
  103. package/addon/controllers/management/vendors/index.js +32 -0
  104. package/addon/controllers/operations/orders/index.js +3 -1
  105. package/addon/controllers/settings/custom-fields.js +8 -0
  106. package/addon/extension.js +7 -0
  107. package/addon/helpers/placeholder-image.js +6 -0
  108. package/addon/helpers/resource-image.js +10 -0
  109. package/addon/helpers/trailer-placeholder-image.js +6 -0
  110. package/addon/routes/management/trailers/index/details/connections.js +2 -0
  111. package/addon/routes/management/trailers/index/details/devices.js +2 -0
  112. package/addon/routes/management/trailers/index/details/equipment.js +2 -0
  113. package/addon/routes/management/trailers/index/details/index.js +2 -0
  114. package/addon/routes/management/trailers/index/details/maintenance-history.js +2 -0
  115. package/addon/routes/management/trailers/index/details/positions.js +2 -0
  116. package/addon/routes/management/trailers/index/details/schedules.js +2 -0
  117. package/addon/routes/management/trailers/index/details/work-orders.js +2 -0
  118. package/addon/routes/management/trailers/index/details.js +30 -0
  119. package/addon/routes/management/trailers/index/edit.js +23 -0
  120. package/addon/routes/management/trailers/index/new.js +14 -0
  121. package/addon/routes/management/trailers/index.js +42 -0
  122. package/addon/routes/management/vehicles/index/details/trailers.js +2 -0
  123. package/addon/routes/management/vehicles/index.js +7 -1
  124. package/addon/routes.js +17 -0
  125. package/addon/services/device-actions.js +26 -16
  126. package/addon/services/trailer-actions.js +394 -0
  127. package/addon/services/vehicle-actions.js +111 -0
  128. package/addon/styles/fleetops-engine.css +349 -0
  129. package/addon/templates/connectivity/devices/index/details/vehicle.hbs +19 -19
  130. package/addon/templates/connectivity/telematics/details/attachments.hbs +15 -15
  131. package/addon/templates/management/contacts/customers.hbs +48 -29
  132. package/addon/templates/management/contacts/index.hbs +52 -33
  133. package/addon/templates/management/trailers/index/details/connections.hbs +1 -0
  134. package/addon/templates/management/trailers/index/details/devices.hbs +1 -0
  135. package/addon/templates/management/trailers/index/details/equipment.hbs +1 -0
  136. package/addon/templates/management/trailers/index/details/index.hbs +1 -0
  137. package/addon/templates/management/trailers/index/details/maintenance-history.hbs +1 -0
  138. package/addon/templates/management/trailers/index/details/positions.hbs +1 -0
  139. package/addon/templates/management/trailers/index/details/schedules.hbs +1 -0
  140. package/addon/templates/management/trailers/index/details/work-orders.hbs +1 -0
  141. package/addon/templates/management/trailers/index/details.hbs +15 -0
  142. package/addon/templates/management/trailers/index/edit.hbs +12 -0
  143. package/addon/templates/management/trailers/index/new.hbs +11 -0
  144. package/addon/templates/management/trailers/index.hbs +67 -0
  145. package/addon/templates/management/vehicles/index/details/equipment.hbs +1 -1
  146. package/addon/templates/management/vehicles/index/details/trailers.hbs +1 -0
  147. package/addon/templates/management/vehicles/index/details.hbs +1 -1
  148. package/addon/templates/management/vendors/index.hbs +66 -32
  149. package/addon/utils/device-table-columns.js +10 -6
  150. package/addon/utils/live-map-card-content.js +56 -0
  151. package/addon/utils/placeholder-images.js +71 -0
  152. package/addon/utils/trailer-placeholder-image.js +11 -0
  153. package/app/components/attachment-list.js +1 -0
  154. package/app/components/cell/translated-value.js +1 -0
  155. package/app/components/contact/card.js +1 -0
  156. package/app/components/equipment/pill.js +1 -0
  157. package/app/components/filter/multi-model.js +1 -0
  158. package/app/components/modals/attach-equipment.js +1 -0
  159. package/app/components/modals/attach-trailer.js +1 -0
  160. package/app/components/modals/select-trailer.js +1 -0
  161. package/app/components/trailer/card.js +1 -0
  162. package/app/components/trailer/details/connections.js +1 -0
  163. package/app/components/trailer/details/equipment.js +1 -0
  164. package/app/components/trailer/details/maintenance-history.js +1 -0
  165. package/app/components/trailer/details/schedules.js +1 -0
  166. package/app/components/trailer/details/work-orders.js +1 -0
  167. package/app/components/trailer/details.js +1 -0
  168. package/app/components/trailer/form.js +1 -0
  169. package/app/components/trailer/panel-header.js +1 -0
  170. package/app/components/trailer/pill.js +1 -0
  171. package/app/components/vehicle/details/equipment.js +1 -0
  172. package/app/components/vehicle/details/trailers.js +1 -0
  173. package/app/components/vendor/card.js +1 -0
  174. package/app/controllers/management/trailers/index/details.js +1 -0
  175. package/app/controllers/management/trailers/index/edit.js +1 -0
  176. package/app/controllers/management/trailers/index/new.js +1 -0
  177. package/app/controllers/management/trailers/index.js +1 -0
  178. package/app/helpers/placeholder-image.js +1 -0
  179. package/app/helpers/resource-image.js +1 -0
  180. package/app/helpers/trailer-placeholder-image.js +1 -0
  181. package/app/routes/management/trailers/index/details/connections.js +1 -0
  182. package/app/routes/management/trailers/index/details/devices.js +1 -0
  183. package/app/routes/management/trailers/index/details/equipment.js +1 -0
  184. package/app/routes/management/trailers/index/details/index.js +1 -0
  185. package/app/routes/management/trailers/index/details/maintenance-history.js +1 -0
  186. package/app/routes/management/trailers/index/details/positions.js +1 -0
  187. package/app/routes/management/trailers/index/details/schedules.js +1 -0
  188. package/app/routes/management/trailers/index/details/work-orders.js +1 -0
  189. package/app/routes/management/trailers/index/details.js +1 -0
  190. package/app/routes/management/trailers/index/edit.js +1 -0
  191. package/app/routes/management/trailers/index/new.js +1 -0
  192. package/app/routes/management/trailers/index.js +1 -0
  193. package/app/routes/management/vehicles/index/details/trailers.js +1 -0
  194. package/app/services/trailer-actions.js +1 -0
  195. package/app/templates/management/trailers/index/details/connections.js +1 -0
  196. package/app/templates/management/trailers/index/details/devices.js +1 -0
  197. package/app/templates/management/trailers/index/details/equipment.js +1 -0
  198. package/app/templates/management/trailers/index/details/index.js +1 -0
  199. package/app/templates/management/trailers/index/details/maintenance-history.js +1 -0
  200. package/app/templates/management/trailers/index/details/positions.js +1 -0
  201. package/app/templates/management/trailers/index/details/schedules.js +1 -0
  202. package/app/templates/management/trailers/index/details/work-orders.js +1 -0
  203. package/app/templates/management/trailers/index/details.js +1 -0
  204. package/app/templates/management/trailers/index/edit.js +1 -0
  205. package/app/templates/management/trailers/index/new.js +1 -0
  206. package/app/templates/management/trailers/index.js +1 -0
  207. package/app/templates/management/vehicles/index/details/trailers.js +1 -0
  208. package/composer.json +1 -1
  209. package/extension.json +1 -1
  210. package/package.json +1 -1
  211. package/server/migrations/2026_09_03_000001_add_first_class_trailer_columns_to_assets_table.php +58 -0
  212. package/server/migrations/2026_09_03_000002_create_asset_connections_table.php +43 -0
  213. package/server/migrations/2026_09_03_000003_create_device_installations_table.php +33 -0
  214. package/server/src/Auth/Schemas/FleetOps.php +7 -0
  215. package/server/src/Events/TrailerLocationChanged.php +70 -0
  216. package/server/src/Exceptions/DeviceAlreadyAttachedException.php +18 -0
  217. package/server/src/Exports/TrailerExport.php +31 -0
  218. package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesFleetOpsApiResources.php +8 -2
  219. package/server/src/Http/Controllers/Api/v1/DeviceController.php +22 -15
  220. package/server/src/Http/Controllers/Api/v1/EquipmentController.php +47 -0
  221. package/server/src/Http/Controllers/Api/v1/TrailerController.php +362 -0
  222. package/server/src/Http/Controllers/Api/v1/VehicleController.php +1 -0
  223. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +3 -0
  224. package/server/src/Http/Controllers/Internal/v1/LiveController.php +1 -1
  225. package/server/src/Http/Controllers/Internal/v1/SearchController.php +16 -0
  226. package/server/src/Http/Controllers/Internal/v1/TrailerController.php +360 -0
  227. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +74 -0
  228. package/server/src/Http/Filter/EquipmentFilter.php +51 -0
  229. package/server/src/Http/Filter/OrderFilter.php +15 -0
  230. package/server/src/Http/Filter/TrailerFilter.php +269 -0
  231. package/server/src/Http/Filter/VehicleFilter.php +70 -3
  232. package/server/src/Http/Requests/CreateDeviceRequest.php +1 -1
  233. package/server/src/Http/Requests/CreateEquipmentRequest.php +1 -1
  234. package/server/src/Http/Requests/CreateTrailerRequest.php +54 -0
  235. package/server/src/Http/Requests/UpdateTrailerRequest.php +7 -0
  236. package/server/src/Http/Resources/v1/AssetConnection.php +90 -0
  237. package/server/src/Http/Resources/v1/Concerns/ResolvesPublicRelationFields.php +29 -3
  238. package/server/src/Http/Resources/v1/Device.php +3 -1
  239. package/server/src/Http/Resources/v1/Equipment.php +3 -1
  240. package/server/src/Http/Resources/v1/Fleet.php +3 -1
  241. package/server/src/Http/Resources/v1/Index/Vehicle.php +10 -0
  242. package/server/src/Http/Resources/v1/Trailer.php +199 -0
  243. package/server/src/Http/Resources/v1/Vehicle.php +2 -0
  244. package/server/src/Http/Resources/v1/VehicleWithoutDriver.php +3 -0
  245. package/server/src/Imports/TrailerImport.php +33 -0
  246. package/server/src/Models/Asset.php +39 -10
  247. package/server/src/Models/AssetConnection.php +43 -0
  248. package/server/src/Models/Device.php +39 -8
  249. package/server/src/Models/DeviceInstallation.php +27 -0
  250. package/server/src/Models/Equipment.php +2 -0
  251. package/server/src/Models/Trailer.php +181 -0
  252. package/server/src/Models/Vehicle.php +13 -0
  253. package/server/src/Providers/FleetOpsServiceProvider.php +4 -2
  254. package/server/src/Support/Ai/FleetOpsAiQueryResources.php +22 -0
  255. package/server/src/Support/Reporting/FleetOpsReportSchema.php +38 -0
  256. package/server/src/Support/Telematics/TelematicService.php +35 -0
  257. package/server/src/Support/TrailerSummary.php +35 -0
  258. package/server/src/routes.php +29 -0
  259. package/server/tests/CompactResourceSerializationTest.php +36 -0
  260. package/server/tests/ControllerFilterContractsTest.php +29 -0
  261. package/server/tests/Feature/Http/Api/DeviceControllerContractsTest.php +30 -13
  262. package/server/tests/Feature/Http/Api/TrailerControllerLifecycleTest.php +640 -0
  263. package/server/tests/Feature/Http/Api/VehiclePublicContractTest.php +23 -0
  264. package/server/tests/Feature/Http/Internal/DeviceControllerContractsTest.php +20 -5
  265. package/server/tests/Feature/Http/Internal/LiveControllerEndpointsTest.php +19 -0
  266. package/server/tests/ModelAccessorContractsTest.php +10 -0
  267. package/server/tests/OrderFilterExecutionTest.php +5 -0
  268. package/server/tests/PublicApiControllerInputContractsTest.php +11 -0
  269. package/server/tests/PublicFleetOpsRestEndpointsTest.php +15 -3
  270. package/server/tests/ReportSchemaContractsTest.php +4 -0
  271. package/server/tests/TelematicsHardeningTest.php +4 -2
  272. package/server/tests/TrailerSupportCoverageTest.php +524 -0
  273. package/server/tests/Unit/Http/Resources/FleetResourceTest.php +17 -0
  274. package/server/tests/Unit/Http/Resources/IndexVehicleResourceTest.php +35 -0
  275. package/server/tests/Unit/Models/EquipmentTest.php +22 -0
  276. package/server/tests/Unit/Models/ModelLookupFallbackSeamsTest.php +7 -3
  277. package/server/tests/Unit/Models/TrailerTest.php +204 -0
  278. package/server/tests/Unit/Support/Telematics/TelematicServiceTelemetryTest.php +93 -0
  279. package/server/tests/VehicleControllerHelperContractsTest.php +14 -0
  280. package/server/tests/VehicleResourceContractsTest.php +39 -0
  281. package/translations/ar-ae.yml +396 -0
  282. package/translations/bg-bg.yaml +424 -0
  283. package/translations/en-us.yaml +423 -0
  284. package/translations/es-pa.yaml +425 -0
  285. package/translations/fr-fr.yaml +424 -0
  286. package/translations/mn-mn.yaml +424 -0
  287. package/translations/pt-br.yaml +424 -0
  288. package/translations/ru-ru.yaml +424 -0
  289. package/translations/vi-vn.yaml +435 -0
@@ -452,3 +452,26 @@ test('an unsupported expansion on retrieve cannot reach eloquent', function () {
452
452
  ->and($payload)->not->toHaveKey('not_a_relation')
453
453
  ->and($request->input('with'))->toBe(['vendor']);
454
454
  });
455
+
456
+ test('a vehicle nested in an order response ignores the order expansions', function () {
457
+ fleetopsVehicleContractBoot();
458
+
459
+ $controller = new VehicleController();
460
+ $createRequest = fleetopsVehicleContractRequest(CreateVehicleRequest::class, 'POST', ['make' => 'Ford', 'driver' => 'driver_contract1']);
461
+ $created = $controller->create($createRequest)->resolve($createRequest);
462
+ $vehicle = Vehicle::where('public_id', $created['id'])->firstOrFail();
463
+
464
+ // An order fetch expands the order's own relations. The Order resource nests
465
+ // the assigned vehicle as a full Vehicle resource, which re-reads the same
466
+ // request: `payload` is not a vehicle relation and used to reach load(),
467
+ // answering the whole order request with a RelationNotFoundException.
468
+ $orderRequest = fleetopsVehicleContractRequest(Request::class, 'GET', ['with' => ['payload', 'driverAssigned.vehicle', 'driver']], 'v1/orders');
469
+ $payload = (new Fleetbase\FleetOps\Http\Resources\v1\Vehicle($vehicle))->resolve($orderRequest);
470
+
471
+ expect($payload['id'])->toBe($created['id'])
472
+ ->and($payload)->not->toHaveKey('payload')
473
+ ->and($vehicle->relationLoaded('payload'))->toBeFalse()
474
+ // The vehicle's own expansion in the same list still resolves.
475
+ ->and($payload['driver'])->toBeObject()
476
+ ->and($payload['driver_id'])->toBe('driver_contract1');
477
+ });
@@ -1,5 +1,6 @@
1
1
  <?php
2
2
 
3
+ use Fleetbase\FleetOps\Exceptions\DeviceAlreadyAttachedException;
3
4
  use Fleetbase\FleetOps\Http\Controllers\Internal\v1\DeviceController;
4
5
  use Fleetbase\FleetOps\Models\Device;
5
6
  use Fleetbase\FleetOps\Models\Vehicle;
@@ -39,11 +40,12 @@ class FleetOpsInternalDeviceControllerProbe extends DeviceController
39
40
 
40
41
  class FleetOpsInternalDeviceFake extends Device
41
42
  {
42
- public array $attachedTo = [];
43
- public array $detaches = [];
44
- public array $loads = [];
45
- public bool $throwAttach = false;
46
- public bool $throwDetach = false;
43
+ public array $attachedTo = [];
44
+ public array $detaches = [];
45
+ public array $loads = [];
46
+ public bool $throwAttach = false;
47
+ public bool $throwDetach = false;
48
+ public bool $throwAlreadyAttached = false;
47
49
 
48
50
  public function attachTo(Model $attachable): bool
49
51
  {
@@ -51,6 +53,10 @@ class FleetOpsInternalDeviceFake extends Device
51
53
  throw new RuntimeException('attach failed');
52
54
  }
53
55
 
56
+ if ($this->throwAlreadyAttached) {
57
+ throw DeviceAlreadyAttachedException::for('Truck 1');
58
+ }
59
+
54
60
  $this->attachedTo[] = [$attachable::class, $attachable->uuid];
55
61
  $this->forceFill([
56
62
  'attachable_type' => $attachable::class,
@@ -350,6 +356,12 @@ test('internal device controller reports lookup and persistence failures', funct
350
356
  ->attach(new Request(['vehicle' => 'vehicle_public']), 'device_public')
351
357
  );
352
358
 
359
+ $conflictDevice = fleetopsInternalDevice();
360
+ $conflictDevice->throwAlreadyAttached = true;
361
+ $conflictResponse = fleetopsInternalDeviceController($conflictDevice, fleetopsInternalDeviceVehicle())
362
+ ->attach(new Request(['vehicle' => 'vehicle_public']), 'device_public');
363
+ $conflict = fleetopsInternalDeviceJson($conflictResponse);
364
+
353
365
  $detachFailureDevice = fleetopsInternalDevice();
354
366
  $detachFailureDevice->throwDetach = true;
355
367
  $detachFailure = fleetopsInternalDeviceJson(
@@ -365,6 +377,9 @@ test('internal device controller reports lookup and persistence failures', funct
365
377
  expect($missingDevice['error'])->toBe('Device not found or not available for this organization.')
366
378
  ->and($missingVehicle['error'])->toBe('Vehicle not found or not available for this organization.')
367
379
  ->and($attachFailure['error'])->toBe('Unable to attach device to vehicle. Please try again or contact support.')
380
+ // A device installed elsewhere is refused with a conflict, not re-homed or reported as a crash.
381
+ ->and($conflict['error'])->toBe('Device is already attached to Truck 1. Detach it before attaching it elsewhere.')
382
+ ->and($conflictResponse->getStatusCode())->toBe(409)
368
383
  ->and($detachFailure['error'])->toBe('Unable to detach device from vehicle. Please try again or contact support.')
369
384
  ->and($missingDetachDevice['error'])->toBe('Device not found or not available for this organization.');
370
385
  });
@@ -123,6 +123,8 @@ function fleetopsLiveEndpointsBoot(): SQLiteConnection
123
123
  'users' => ['uuid', 'public_id', 'company_uuid', 'name', 'phone', 'email', 'status', 'type', 'avatar_uuid'],
124
124
  'vehicles' => ['uuid', 'public_id', 'company_uuid', 'vendor_uuid', 'photo_uuid', 'name', 'display_name', 'plate_number', 'serial_number', 'fuel_card_number', 'vin', 'vin_data', 'make', 'model', 'year', 'trim', 'class', 'color', 'call_sign', 'specs', 'telematics', 'status', 'online', 'location', 'meta', 'avatar_url', 'internal_id', 'speed', 'heading', 'altitude'],
125
125
  'devices' => ['uuid', 'public_id', 'company_uuid', 'device_id', 'device_type', 'device_provider', 'owner_uuid', 'owner_type', 'attachable_uuid', 'attachable_type', 'status'],
126
+ 'assets' => ['uuid', 'public_id', 'company_uuid', 'asset_class', 'name', 'display_name', 'type', 'plate_number', 'status', 'last_online_at', 'online'],
127
+ 'asset_connections' => ['uuid', 'public_id', 'company_uuid', 'connector_type', 'connector_uuid', 'connected_type', 'connected_uuid', 'active_connected_uuid', 'active_connector_position', 'relationship_type', 'position', 'connected_at', 'disconnected_at', 'source'],
126
128
  'vendors' => ['uuid', 'public_id', 'company_uuid', 'name'],
127
129
  'files' => ['uuid', 'public_id', 'company_uuid', 'type', 'path', 'disk'],
128
130
  'entities' => ['uuid', 'public_id', 'company_uuid', 'payload_uuid', 'name', 'type'],
@@ -216,6 +218,16 @@ test('drivers and vehicles list located records within viewport bounds', functio
216
218
  ['uuid' => 'driver-2', 'company_uuid' => 'company-1', 'user_uuid' => 'user-1', 'online' => '1', 'location' => null],
217
219
  ]);
218
220
  $connection->table('vehicles')->insert(['uuid' => 'vehicle-1', 'company_uuid' => 'company-1', 'name' => 'Truck', 'location' => 'POINT(1 1)']);
221
+ $connection->table('assets')->insert([
222
+ ['uuid' => 'trailer-1', 'company_uuid' => 'company-1', 'asset_class' => 'trailer', 'name' => 'Reefer 12', 'type' => 'reefer', 'plate_number' => 'TRL-12', 'status' => 'in_use'],
223
+ ['uuid' => 'trailer-2', 'company_uuid' => 'company-1', 'asset_class' => 'trailer', 'name' => 'Flatbed 3', 'type' => 'flatbed', 'plate_number' => null, 'status' => 'available'],
224
+ ]);
225
+ $connection->table('asset_connections')->insert([
226
+ // Active towing connection: appears on the vehicle's map popover.
227
+ ['uuid' => 'connection-1', 'company_uuid' => 'company-1', 'connector_type' => 'Fleetbase\\FleetOps\\Models\\Vehicle', 'connector_uuid' => 'vehicle-1', 'connected_type' => 'Fleetbase\\FleetOps\\Models\\Trailer', 'connected_uuid' => 'trailer-1', 'active_connected_uuid' => 'trailer-1', 'relationship_type' => 'towing', 'position' => '1', 'connected_at' => '2026-09-01 08:00:00', 'source' => 'manual'],
228
+ // Ended connection: history only, never surfaced as a current trailer.
229
+ ['uuid' => 'connection-2', 'company_uuid' => 'company-1', 'connector_type' => 'Fleetbase\\FleetOps\\Models\\Vehicle', 'connector_uuid' => 'vehicle-1', 'connected_type' => 'Fleetbase\\FleetOps\\Models\\Trailer', 'connected_uuid' => 'trailer-2', 'relationship_type' => 'towing', 'position' => '1', 'connected_at' => '2026-08-01 08:00:00', 'disconnected_at' => '2026-08-02 08:00:00', 'source' => 'manual'],
230
+ ]);
219
231
 
220
232
  $drivers = (new LiveController())->drivers(Request::create('/x', 'GET'));
221
233
  expect($drivers->count())->toBe(1);
@@ -228,6 +240,13 @@ test('drivers and vehicles list located records within viewport bounds', functio
228
240
 
229
241
  $vehicles = (new LiveController())->vehicles(Request::create('/x', 'GET'));
230
242
  expect($vehicles->count())->toBe(1);
243
+
244
+ // The live feed eager-loads the trailers currently coupled to each vehicle so the
245
+ // map popover can list them without a second request per marker.
246
+ $vehicle = $vehicles->collection->first()->resource;
247
+ expect($vehicle->relationLoaded('currentTrailers'))->toBeTrue()
248
+ ->and($vehicle->relationLoaded('devices'))->toBeTrue()
249
+ ->and($vehicle->currentTrailers->pluck('uuid')->all())->toBe(['trailer-1']);
231
250
  expect((new LiveController())->vehicles(Request::create('/x', 'GET', ['bounds' => [0, 0, 1, 1]]))->count())->toBe(0);
232
251
  });
233
252
 
@@ -21,6 +21,7 @@ if (!function_exists('Fleetbase\FleetOps\Models\activity')) {
21
21
  }
22
22
 
23
23
  use Fleetbase\FleetOps\Exceptions\CustomerUserConflictException;
24
+ use Fleetbase\FleetOps\Exceptions\DeviceAlreadyAttachedException;
24
25
  use Fleetbase\FleetOps\Flow\Activity;
25
26
  use Fleetbase\FleetOps\Models\Contact;
26
27
  use Fleetbase\FleetOps\Models\Device;
@@ -990,6 +991,15 @@ test('device accessors connection state configuration and command guards are sta
990
991
  'attachable_uuid' => 'vehicle-uuid',
991
992
  ]);
992
993
 
994
+ // An installed device must be detached before it can move to another asset;
995
+ // attaching used to silently re-home it with no feedback to the operator.
996
+ $device->attachable_type = Vehicle::class;
997
+ $device->attachable_uuid = 'other-vehicle-uuid';
998
+ expect(fn () => $device->attachTo($vehicle))->toThrow(DeviceAlreadyAttachedException::class, 'Detach it before attaching it elsewhere.');
999
+ expect(DeviceAlreadyAttachedException::for('Truck 99')->getMessage())->toBe('Device is already attached to Truck 99. Detach it before attaching it elsewhere.')
1000
+ ->and(DeviceAlreadyAttachedException::for(null)->getMessage())->toBe('Device is already attached to another asset. Detach it before attaching it elsewhere.')
1001
+ ->and($device->updates)->toHaveCount(3);
1002
+
993
1003
  $device->attachable_type = Vehicle::class;
994
1004
  $device->attachable_uuid = 'vehicle-uuid';
995
1005
 
@@ -181,7 +181,12 @@ test('order filter identity and relation filters support uuid and public identif
181
181
  $filter->driver('driver_public');
182
182
  $filter->driverAssigned('driver_public');
183
183
 
184
+ // Batch label scans arrive as one comma-separated list of internal ids; blanks are ignored.
185
+ $filter->internalId('ORD-1, ORD-2,');
186
+ $filter->internalId(' ');
187
+
184
188
  expect($builder->called('whereIn'))->toBeTrue()
189
+ ->and($builder->calls)->toContain(['whereIn', 'internal_id', ['ORD-1', 'ORD-2']])
185
190
  ->and($builder->called('removeWhereFromQuery'))->toBeTrue()
186
191
  ->and($builder->called('whereHas'))->toBeTrue()
187
192
  ->and($builder->called('orWhereHas'))->toBeTrue();
@@ -11,6 +11,7 @@ use Fleetbase\FleetOps\Models\FuelReport;
11
11
  use Fleetbase\FleetOps\Models\Order;
12
12
  use Fleetbase\FleetOps\Models\Part;
13
13
  use Fleetbase\FleetOps\Models\Telematic;
14
+ use Fleetbase\FleetOps\Models\Trailer;
14
15
  use Fleetbase\FleetOps\Models\Vehicle;
15
16
  use Fleetbase\FleetOps\Models\Vendor;
16
17
  use Fleetbase\FleetOps\Models\Warranty;
@@ -246,6 +247,9 @@ test('public api resource resolver exposes every supported morph alias', functio
246
247
  'fleet-ops:vehicle' => Vehicle::class,
247
248
  'vehicle' => Vehicle::class,
248
249
  Vehicle::class => Vehicle::class,
250
+ 'fleet-ops:trailer' => Trailer::class,
251
+ 'trailer' => Trailer::class,
252
+ Trailer::class => Trailer::class,
249
253
  'fleet-ops:driver' => Driver::class,
250
254
  'driver' => Driver::class,
251
255
  Driver::class => Driver::class,
@@ -284,6 +288,13 @@ test('public api resource resolver exposes every supported morph alias', functio
284
288
  ]);
285
289
  });
286
290
 
291
+ test('public api resource resolver rejects unsupported morph aliases', function () {
292
+ $controller = new FleetOpsWorkOrderControllerProbe();
293
+
294
+ expect(fn () => $controller->callHelper('resolveMorph', 'unsupported-resource', 'resource_public'))
295
+ ->toThrow(ValidationException::class);
296
+ });
297
+
287
298
  test('public api resource resolver detects fillable columns and primary keys', function () {
288
299
  $controller = new FleetOpsWorkOrderControllerProbe();
289
300
  $model = new FleetOpsPublicApiResolverModelFake();
@@ -5,18 +5,20 @@ use Fleetbase\FleetOps\Http\Controllers\Api\v1\EquipmentController;
5
5
  use Fleetbase\FleetOps\Http\Controllers\Api\v1\FuelTransactionController;
6
6
  use Fleetbase\FleetOps\Http\Controllers\Api\v1\PartController;
7
7
  use Fleetbase\FleetOps\Http\Controllers\Api\v1\SensorController;
8
+ use Fleetbase\FleetOps\Http\Controllers\Api\v1\TrailerController;
8
9
  use Fleetbase\FleetOps\Http\Controllers\Api\v1\WorkOrderController;
9
10
  use Fleetbase\FleetOps\Http\Resources\v1\Device;
10
11
  use Fleetbase\FleetOps\Http\Resources\v1\Equipment;
11
12
  use Fleetbase\FleetOps\Http\Resources\v1\FuelTransaction;
12
13
  use Fleetbase\FleetOps\Http\Resources\v1\Part;
13
14
  use Fleetbase\FleetOps\Http\Resources\v1\Sensor;
15
+ use Fleetbase\FleetOps\Http\Resources\v1\Trailer;
14
16
  use Fleetbase\FleetOps\Http\Resources\v1\WorkOrder;
15
17
 
16
18
  test('new first-class fleetops rest routes are registered in the consumable v1 namespace', function () {
17
19
  $routes = file_get_contents(dirname(__DIR__) . '/src/routes.php');
18
20
 
19
- foreach (['equipment', 'parts', 'work-orders', 'devices', 'sensors', 'fuel-transactions'] as $prefix) {
21
+ foreach (['equipment', 'parts', 'work-orders', 'devices', 'sensors', 'fuel-transactions', 'trailers'] as $prefix) {
20
22
  expect($routes)->toContain("\$router->group(['prefix' => '{$prefix}']");
21
23
  }
22
24
 
@@ -30,7 +32,12 @@ test('new first-class fleetops rest routes are registered in the consumable v1 n
30
32
  ->toContain('FuelTransactionController@matchVehicle')
31
33
  ->toContain('FuelTransactionController@matchOrder')
32
34
  ->toContain('FuelTransactionController@reprocess')
33
- ->toContain('FuelTransactionController@review');
35
+ ->toContain('FuelTransactionController@review')
36
+ ->toContain('TrailerController@track')
37
+ ->toContain('TrailerController@attach')
38
+ ->toContain('TrailerController@detach')
39
+ ->toContain('TrailerController@connections')
40
+ ->toContain('TrailerController@vehicleTrailers');
34
41
  });
35
42
 
36
43
  test('public fuel transaction api keeps provider naming internal only', function () {
@@ -54,6 +61,7 @@ test('new public controllers expose the expected rest methods', function () {
54
61
  WorkOrderController::class => ['create', 'query', 'find', 'update', 'delete', 'send'],
55
62
  DeviceController::class => ['create', 'query', 'find', 'update', 'delete', 'attach', 'detach'],
56
63
  SensorController::class => ['create', 'query', 'find', 'update', 'delete'],
64
+ TrailerController::class => ['create', 'query', 'find', 'update', 'delete', 'track', 'attach', 'detach', 'connections', 'vehicleTrailers'],
57
65
  FuelTransactionController::class => ['create', 'query', 'find', 'update', 'delete', 'matchVehicle', 'matchOrder', 'reprocess', 'review'],
58
66
  ];
59
67
 
@@ -65,7 +73,7 @@ test('new public controllers expose the expected rest methods', function () {
65
73
  });
66
74
 
67
75
  test('new public resources preserve internal uuid fields behind internal request checks', function () {
68
- foreach ([Equipment::class, Part::class, WorkOrder::class, Device::class, Sensor::class, FuelTransaction::class] as $resource) {
76
+ foreach ([Equipment::class, Part::class, WorkOrder::class, Device::class, Sensor::class, FuelTransaction::class, Trailer::class] as $resource) {
69
77
  expect(class_exists($resource))->toBeTrue();
70
78
  }
71
79
 
@@ -76,6 +84,7 @@ test('new public resources preserve internal uuid fields behind internal request
76
84
  'Sensor.php' => ['uuid', 'company_uuid', 'device_uuid', 'warranty_uuid', 'telematic_uuid', 'photo_uuid', 'sensorable_uuid'],
77
85
  'FuelTransaction.php' => ['uuid', 'fuel_provider_connection_uuid', 'fuel_report_uuid', 'vehicle_uuid', 'driver_uuid', 'order_uuid'],
78
86
  'WorkOrder.php' => ['uuid', 'company_uuid', 'schedule_uuid', 'created_by_uuid', 'updated_by_uuid', 'target_uuid', 'assignee_uuid'],
87
+ 'Trailer.php' => ['uuid', 'company_uuid'],
79
88
  ];
80
89
 
81
90
  foreach ($internalFields as $resourceFile => $fields) {
@@ -99,6 +108,7 @@ test('new shared resources do not expose unguarded uuid fields', function () {
99
108
  'Sensor.php',
100
109
  'FuelTransaction.php',
101
110
  'WorkOrder.php',
111
+ 'Trailer.php',
102
112
  ] as $resourceFile) {
103
113
  $source = file_get_contents(dirname(__DIR__) . '/src/Http/Resources/v1/' . $resourceFile);
104
114
 
@@ -147,6 +157,7 @@ test('public controllers reject uuid shaped request keys', function () {
147
157
  'DeviceController.php',
148
158
  'SensorController.php',
149
159
  'FuelTransactionController.php',
160
+ 'TrailerController.php',
150
161
  ] as $controllerFile) {
151
162
  $source = file_get_contents(dirname(__DIR__) . '/src/Http/Controllers/Api/v1/' . $controllerFile);
152
163
 
@@ -162,6 +173,7 @@ test('new public list endpoints have company scoped filters', function () {
162
173
  'DeviceFilter.php',
163
174
  'SensorFilter.php',
164
175
  'FuelProviderTransactionFilter.php',
176
+ 'TrailerFilter.php',
165
177
  ] as $filterFile) {
166
178
  $source = file_get_contents(dirname(__DIR__) . '/src/Http/Filter/' . $filterFile);
167
179
 
@@ -214,6 +214,7 @@ test('fleetops report schema registers every table with columns computed columns
214
214
  'orders',
215
215
  'drivers',
216
216
  'vehicles',
217
+ 'assets',
217
218
  'places',
218
219
  'contacts',
219
220
  'vendors',
@@ -241,6 +242,9 @@ test('fleetops report schema registers every table with columns computed columns
241
242
  ->and(fleetOpsReportTableName(fleetOpsReportRelationship($tables['drivers'], 'current_vehicle')))->toBe('vehicles')
242
243
  ->and(fleetOpsReportTableMeta($tables['vehicles'], 'category'))->toBe('Fleet')
243
244
  ->and(fleetOpsReportTableName(fleetOpsReportRelationship($tables['vehicles'], 'current_driver')))->toBe('drivers')
245
+ ->and(fleetOpsReportTableMeta($tables['assets'], 'label'))->toBe('Trailers and Assets')
246
+ ->and(fleetOpsReportTableMeta($tables['assets'], 'category'))->toBe('Fleet')
247
+ ->and(fleetOpsReportColumnFlag(fleetOpsReportColumn($tables['assets'], 'asset_class'), 'filterable'))->toBeTrue()
244
248
  ->and(fleetOpsReportTableMeta($tables['places'], 'category'))->toBe('Geography')
245
249
  ->and(fleetOpsReportTableMeta($tables['contacts'], 'category'))->toBe('CRM')
246
250
  ->and(fleetOpsReportTableMeta($tables['vendors'], 'category'))->toBe('CRM')
@@ -2092,8 +2092,10 @@ test('device attachment morph types are normalized and legacy aliases are tolera
2092
2092
  expect($serviceProvider)
2093
2093
  ->toContain('use Illuminate\Database\Eloquent\Relations\Relation;')
2094
2094
  ->toContain('$this->registerMorphMap();')
2095
- ->toContain("'Fleetbase\\\\Models\\\\Vehicle' => \\Fleetbase\\FleetOps\\Models\\Vehicle::class")
2096
- ->toContain("'\\\\Fleetbase\\\\Models\\\\Vehicle' => \\Fleetbase\\FleetOps\\Models\\Vehicle::class");
2095
+ ->toContain("'Fleetbase\\\\Models\\\\Vehicle'")
2096
+ ->toContain("'\\\\Fleetbase\\\\Models\\\\Vehicle'")
2097
+ ->toContain("'fleet-ops:trailer'")
2098
+ ->toContain('\\Fleetbase\\FleetOps\\Models\\Trailer::class');
2097
2099
  });
2098
2100
 
2099
2101
  test('internal device attachment endpoints return specific api errors instead of raw model misses', function () {