@fleetbase/fleetops-engine 0.6.64 → 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 (284) hide show
  1. package/RELEASE.md +7 -5
  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/Device.php +3 -1
  238. package/server/src/Http/Resources/v1/Equipment.php +3 -1
  239. package/server/src/Http/Resources/v1/Index/Vehicle.php +10 -0
  240. package/server/src/Http/Resources/v1/Trailer.php +199 -0
  241. package/server/src/Http/Resources/v1/Vehicle.php +2 -0
  242. package/server/src/Http/Resources/v1/VehicleWithoutDriver.php +3 -0
  243. package/server/src/Imports/TrailerImport.php +33 -0
  244. package/server/src/Models/Asset.php +39 -10
  245. package/server/src/Models/AssetConnection.php +43 -0
  246. package/server/src/Models/Device.php +39 -8
  247. package/server/src/Models/DeviceInstallation.php +27 -0
  248. package/server/src/Models/Equipment.php +2 -0
  249. package/server/src/Models/Trailer.php +181 -0
  250. package/server/src/Models/Vehicle.php +13 -0
  251. package/server/src/Providers/FleetOpsServiceProvider.php +4 -2
  252. package/server/src/Support/Ai/FleetOpsAiQueryResources.php +22 -0
  253. package/server/src/Support/Reporting/FleetOpsReportSchema.php +38 -0
  254. package/server/src/Support/Telematics/TelematicService.php +35 -0
  255. package/server/src/Support/TrailerSummary.php +35 -0
  256. package/server/src/routes.php +29 -0
  257. package/server/tests/CompactResourceSerializationTest.php +36 -0
  258. package/server/tests/ControllerFilterContractsTest.php +29 -0
  259. package/server/tests/Feature/Http/Api/DeviceControllerContractsTest.php +30 -13
  260. package/server/tests/Feature/Http/Api/TrailerControllerLifecycleTest.php +640 -0
  261. package/server/tests/Feature/Http/Internal/DeviceControllerContractsTest.php +20 -5
  262. package/server/tests/Feature/Http/Internal/LiveControllerEndpointsTest.php +19 -0
  263. package/server/tests/ModelAccessorContractsTest.php +10 -0
  264. package/server/tests/OrderFilterExecutionTest.php +5 -0
  265. package/server/tests/PublicApiControllerInputContractsTest.php +11 -0
  266. package/server/tests/PublicFleetOpsRestEndpointsTest.php +15 -3
  267. package/server/tests/ReportSchemaContractsTest.php +4 -0
  268. package/server/tests/TelematicsHardeningTest.php +4 -2
  269. package/server/tests/TrailerSupportCoverageTest.php +524 -0
  270. package/server/tests/Unit/Http/Resources/IndexVehicleResourceTest.php +35 -0
  271. package/server/tests/Unit/Models/EquipmentTest.php +22 -0
  272. package/server/tests/Unit/Models/ModelLookupFallbackSeamsTest.php +7 -3
  273. package/server/tests/Unit/Models/TrailerTest.php +204 -0
  274. package/server/tests/Unit/Support/Telematics/TelematicServiceTelemetryTest.php +93 -0
  275. package/server/tests/VehicleControllerHelperContractsTest.php +14 -0
  276. package/translations/ar-ae.yml +396 -0
  277. package/translations/bg-bg.yaml +424 -0
  278. package/translations/en-us.yaml +423 -0
  279. package/translations/es-pa.yaml +425 -0
  280. package/translations/fr-fr.yaml +424 -0
  281. package/translations/mn-mn.yaml +424 -0
  282. package/translations/pt-br.yaml +424 -0
  283. package/translations/ru-ru.yaml +424 -0
  284. package/translations/vi-vn.yaml +435 -0
@@ -0,0 +1,204 @@
1
+ <?php
2
+
3
+ use Fleetbase\FleetOps\Events\TrailerLocationChanged;
4
+ use Fleetbase\FleetOps\Models\AssetConnection;
5
+ use Fleetbase\FleetOps\Models\Device;
6
+ use Fleetbase\FleetOps\Models\Equipment;
7
+ use Fleetbase\FleetOps\Models\Maintenance;
8
+ use Fleetbase\FleetOps\Models\MaintenanceSchedule;
9
+ use Fleetbase\FleetOps\Models\Position;
10
+ use Fleetbase\FleetOps\Models\Trailer;
11
+ use Fleetbase\FleetOps\Models\Vehicle;
12
+ use Fleetbase\FleetOps\Models\WorkOrder;
13
+ use Illuminate\Database\ConnectionResolver;
14
+ use Illuminate\Database\Eloquent\Model as EloquentModel;
15
+ use Illuminate\Database\Eloquent\Relations\BelongsTo;
16
+ use Illuminate\Database\Eloquent\Relations\HasMany;
17
+ use Illuminate\Database\Eloquent\Relations\HasManyThrough;
18
+ use Illuminate\Database\Eloquent\Relations\HasOne;
19
+ use Illuminate\Database\SQLiteConnection;
20
+ use Illuminate\Support\Carbon;
21
+
22
+ // Model identifier hooks bind to the dispatcher present when the class boots.
23
+ if (!EloquentModel::getEventDispatcher()) {
24
+ EloquentModel::setEventDispatcher(new Illuminate\Events\Dispatcher());
25
+ }
26
+
27
+ function fleetOpsTrailerUseInMemoryConnection(): SQLiteConnection
28
+ {
29
+ $pdo = new PDO('sqlite::memory:');
30
+ $pdo->sqliteCreateFunction('ST_PointFromText', fn ($wkt, $srid = 0, $axisOrder = null) => $wkt);
31
+ $pdo->sqliteCreateFunction('ST_GeomFromText', fn ($wkt, $srid = 0, $axisOrder = null) => $wkt);
32
+ $connection = new SQLiteConnection($pdo);
33
+ $connection->statement('create table assets (
34
+ id integer primary key autoincrement,
35
+ uuid varchar(64),
36
+ public_id varchar(64) null unique,
37
+ company_uuid varchar(64),
38
+ name varchar(255),
39
+ code varchar(64) null,
40
+ type varchar(64) null,
41
+ asset_class varchar(64) null,
42
+ status varchar(64) null,
43
+ slug varchar(255) null,
44
+ last_online_at datetime null,
45
+ created_at datetime null,
46
+ updated_at datetime null,
47
+ deleted_at datetime null
48
+ )');
49
+ $connection->statement('create table positions (
50
+ id integer primary key autoincrement,
51
+ uuid varchar(64), public_id varchar(64), company_uuid varchar(64),
52
+ subject_uuid varchar(64), subject_type varchar(255), destination_uuid varchar(64),
53
+ coordinates text, heading varchar(64), bearing varchar(64), speed varchar(64), altitude varchar(64),
54
+ created_at datetime null, updated_at datetime null, deleted_at datetime null
55
+ )');
56
+
57
+ $resolver = new ConnectionResolver(['default' => $connection, 'mysql' => $connection]);
58
+ $resolver->setDefaultConnection('mysql');
59
+ EloquentModel::setConnectionResolver($resolver);
60
+ app()->instance('responsecache', new class {
61
+ public function __call($method, $arguments)
62
+ {
63
+ return null;
64
+ }
65
+ });
66
+
67
+ return $connection;
68
+ }
69
+
70
+ test('trailer creation enforces its discriminator defaults and public id namespace', function () {
71
+ fleetOpsTrailerUseInMemoryConnection();
72
+
73
+ $trailer = new Trailer([
74
+ 'company_uuid' => 'company-a',
75
+ 'name' => 'Reefer 12',
76
+ 'asset_class' => 'vehicle',
77
+ ]);
78
+ Trailer::getEventDispatcher()->until('eloquent.creating: ' . Trailer::class, $trailer);
79
+
80
+ expect($trailer->asset_class)->toBe(Trailer::ASSET_CLASS)
81
+ ->and($trailer->status)->toBe('available')
82
+ ->and($trailer->public_id)->toStartWith('trailer_')
83
+ ->and($trailer->uuid)->not->toBeEmpty();
84
+
85
+ EloquentModel::unsetEventDispatcher();
86
+ });
87
+
88
+ test('trailer global scope never returns another asset class', function () {
89
+ $connection = fleetOpsTrailerUseInMemoryConnection();
90
+ $connection->table('assets')->insert([
91
+ ['uuid' => 'trailer-uuid', 'public_id' => 'trailer_public', 'company_uuid' => 'company-a', 'name' => 'Trailer', 'asset_class' => 'trailer', 'status' => 'available'],
92
+ ['uuid' => 'vehicle-uuid', 'public_id' => 'asset_public', 'company_uuid' => 'company-a', 'name' => 'Vehicle-like asset', 'asset_class' => 'vehicle', 'status' => 'available'],
93
+ ]);
94
+
95
+ expect(Trailer::query()->pluck('uuid')->all())->toBe(['trailer-uuid'])
96
+ ->and(Trailer::query()->toSql())->toContain('asset_class');
97
+ });
98
+
99
+ test('trailer exposes first class connection device equipment maintenance and position relations', function () {
100
+ fleetOpsTrailerUseInMemoryConnection();
101
+ $trailer = new Trailer();
102
+
103
+ expect($trailer->currentConnection())->toBeInstanceOf(HasOne::class)
104
+ ->and($trailer->currentConnection()->getRelated())->toBeInstanceOf(AssetConnection::class)
105
+ ->and($trailer->connections())->toBeInstanceOf(HasMany::class)
106
+ ->and($trailer->devices()->getRelated())->toBeInstanceOf(Device::class)
107
+ ->and($trailer->equipments()->getRelated())->toBeInstanceOf(Equipment::class)
108
+ ->and($trailer->positions()->getRelated())->toBeInstanceOf(Position::class)
109
+ ->and($trailer->maintenances()->getRelated())->toBeInstanceOf(Maintenance::class)
110
+ ->and($trailer->maintenanceSchedules()->getRelated())->toBeInstanceOf(MaintenanceSchedule::class)
111
+ ->and($trailer->workOrders()->getRelated())->toBeInstanceOf(WorkOrder::class);
112
+ });
113
+
114
+ test('asset connections preserve effective dated vehicle and trailer identities', function () {
115
+ fleetOpsTrailerUseInMemoryConnection();
116
+ $connection = new AssetConnection([
117
+ 'relationship_type' => 'towing',
118
+ 'position' => 2,
119
+ 'connected_at' => '2026-09-03 12:00:00',
120
+ 'disconnected_at' => null,
121
+ ]);
122
+
123
+ expect($connection->vehicle())->toBeInstanceOf(BelongsTo::class)
124
+ ->and($connection->vehicle()->getRelated())->toBeInstanceOf(Vehicle::class)
125
+ ->and($connection->trailer())->toBeInstanceOf(BelongsTo::class)
126
+ ->and($connection->trailer()->getRelated())->toBeInstanceOf(Trailer::class)
127
+ ->and($connection->relationship_type)->toBe('towing')
128
+ ->and($connection->position)->toBe(2)
129
+ ->and($connection->disconnected_at)->toBeNull();
130
+ });
131
+
132
+ test('vehicles expose current and historical trailer relationships', function () {
133
+ fleetOpsTrailerUseInMemoryConnection();
134
+ $vehicle = new Vehicle();
135
+
136
+ expect($vehicle->trailerConnections())->toBeInstanceOf(HasMany::class)
137
+ ->and($vehicle->trailerConnections()->getRelated())->toBeInstanceOf(AssetConnection::class)
138
+ ->and($vehicle->currentTrailers())->toBeInstanceOf(HasManyThrough::class)
139
+ ->and($vehicle->currentTrailers()->getRelated())->toBeInstanceOf(Trailer::class);
140
+ });
141
+
142
+ test('trailer connectivity is distinct from lifecycle and attachment state', function () {
143
+ Carbon::setTestNow('2026-09-03 12:00:00');
144
+
145
+ $trailer = new Trailer(['status' => 'maintenance']);
146
+ expect($trailer->connectivity_status)->toBe('never_connected')
147
+ ->and($trailer->status)->toBe('maintenance')
148
+ ->and($trailer->attachment_state)->toBe('detached');
149
+
150
+ $vehicle = new Vehicle();
151
+ $connection = new AssetConnection();
152
+ $connection->setRelation('vehicle', $vehicle);
153
+ $trailer->setRelation('currentConnection', $connection);
154
+ expect($trailer->current_vehicle)->toBe($vehicle)
155
+ ->and($trailer->attachment_state)->toBe('attached');
156
+
157
+ $trailer->last_online_at = Carbon::now()->subMinutes(5);
158
+ expect($trailer->connectivity_status)->toBe('online');
159
+
160
+ $trailer->last_online_at = Carbon::now()->subHours(2);
161
+ expect($trailer->connectivity_status)->toBe('recently_offline');
162
+
163
+ $trailer->last_online_at = Carbon::now()->subDays(2);
164
+ expect($trailer->connectivity_status)->toBe('offline');
165
+
166
+ Carbon::setTestNow();
167
+ });
168
+
169
+ test('trailer positions require coordinates and preserve model or scalar destinations', function () {
170
+ fleetOpsTrailerUseInMemoryConnection();
171
+ $trailer = new Trailer();
172
+ $trailer->forceFill(['uuid' => 'trailer-position', 'company_uuid' => 'company-a']);
173
+ expect($trailer->createPosition())->toBeNull();
174
+
175
+ $vehicle = new Vehicle();
176
+ $vehicle->forceFill(['uuid' => 'vehicle-destination']);
177
+ $fromModel = $trailer->createPosition(['latitude' => 1.3, 'longitude' => 103.8, 'heading' => 90, 'bearing' => 91, 'speed' => 40, 'altitude' => 12], $vehicle);
178
+ $fromScalar = $trailer->createPosition(['latitude' => 1.4, 'longitude' => 103.9], 'place-destination');
179
+ expect($fromModel)->toBeInstanceOf(Position::class)
180
+ ->and($fromModel->destination_uuid)->toBe('vehicle-destination')
181
+ ->and($fromScalar->destination_uuid)->toBe('place-destination');
182
+ });
183
+
184
+ test('trailer location broadcasts use tenant and trailer channels without exposing internal ids', function () {
185
+ Carbon::setTestNow('2026-09-03 12:00:00');
186
+
187
+ $trailer = new Trailer();
188
+ $trailer->forceFill([
189
+ 'uuid' => 'internal-trailer-uuid',
190
+ 'public_id' => 'trailer_public',
191
+ 'company_uuid' => 'company-a',
192
+ 'speed' => 42,
193
+ 'heading' => 180,
194
+ ]);
195
+ $event = new TrailerLocationChanged($trailer, ['source' => 'telematics']);
196
+
197
+ expect(array_map(fn ($channel) => $channel->name, $event->broadcastOn()))
198
+ ->toBe(['company.company-a', 'trailer.trailer_public', 'trailer.internal-trailer-uuid'])
199
+ ->and($event->broadcastWith()['data']['id'])->toBe('trailer_public')
200
+ ->and($event->broadcastWith()['data'])->not->toHaveKey('uuid')
201
+ ->and($event->broadcastWith()['data']['additionalData'])->toBe(['source' => 'telematics']);
202
+
203
+ Carbon::setTestNow();
204
+ });
@@ -6,6 +6,7 @@ use Fleetbase\FleetOps\Models\DeviceEvent;
6
6
  use Fleetbase\FleetOps\Models\Position;
7
7
  use Fleetbase\FleetOps\Models\Sensor;
8
8
  use Fleetbase\FleetOps\Models\Telematic;
9
+ use Fleetbase\FleetOps\Models\Trailer;
9
10
  use Fleetbase\FleetOps\Models\Vehicle;
10
11
  use Fleetbase\FleetOps\Support\Telematics\TelematicProviderRegistry;
11
12
  use Fleetbase\FleetOps\Support\Telematics\TelematicService;
@@ -105,6 +106,36 @@ class FleetOpsTelematicServiceTelemetryVehicleFake extends Vehicle
105
106
  }
106
107
  }
107
108
 
109
+ class FleetOpsTelematicServiceTelemetryTrailerFake extends Trailer
110
+ {
111
+ public int $saveCount = 0;
112
+
113
+ public function __construct(array $attributes = [])
114
+ {
115
+ $this->attributes = $attributes;
116
+ }
117
+
118
+ public function getAttribute($key)
119
+ {
120
+ return $this->attributes[$key] ?? null;
121
+ }
122
+
123
+ public function setAttribute($key, $value)
124
+ {
125
+ $this->attributes[$key] = $value;
126
+
127
+ return $this;
128
+ }
129
+
130
+ public function save(array $options = [])
131
+ {
132
+ $this->saveCount++;
133
+ $this->exists = true;
134
+
135
+ return true;
136
+ }
137
+ }
138
+
108
139
  class FleetOpsTelematicServiceTelemetryProviderFake implements TelematicProviderInterface
109
140
  {
110
141
  public array $normalizedSensors = [];
@@ -317,6 +348,68 @@ test('telematic service skips event positions and vehicle updates when event loc
317
348
  ->and($GLOBALS['fleetops_telematic_service_broadcasts'])->toBe([]);
318
349
  });
319
350
 
351
+ test('telematic service updates trailer telemetry and rejects stale trailer snapshots', function () {
352
+ Carbon::setTestNow(Carbon::parse('2026-09-03 12:00:00'));
353
+ $GLOBALS['fleetops_telematic_service_broadcasts'] = [];
354
+
355
+ $service = new FleetOpsTelematicServiceTelemetryServiceFake();
356
+ $trailer = new FleetOpsTelematicServiceTelemetryTrailerFake([
357
+ 'uuid' => 'trailer-uuid',
358
+ 'public_id' => 'trailer_public',
359
+ 'company_uuid' => 'company-uuid',
360
+ 'telematics' => ['last_event_at' => '2026-09-03T11:00:00.000000Z'],
361
+ ]);
362
+ $device = new FleetOpsTelematicServiceTelemetryDeviceFake(['uuid' => 'device-uuid', 'device_id' => 'device-1']);
363
+ $device->setRelation('attachable', $trailer);
364
+ $event = new FleetOpsTelematicServiceTelemetryEventFake([
365
+ 'uuid' => 'event-new',
366
+ 'public_id' => 'event_new',
367
+ 'device_uuid' => 'device-uuid',
368
+ 'event_type' => 'telemetry_update',
369
+ 'provider' => 'samsara',
370
+ 'occurred_at' => Carbon::parse('2026-09-03 11:30:00'),
371
+ ]);
372
+
373
+ fleetOpsTelematicServiceTelemetryInvoke($service, 'applyDeviceEventTelemetry', [$event, [
374
+ 'location' => ['latitude' => 47.918, 'longitude' => 106.917],
375
+ 'speed' => 42,
376
+ 'heading' => 180,
377
+ 'odometer' => 32000,
378
+ 'reefer_engine_hours' => 800,
379
+ ], $device, true, null]);
380
+
381
+ expect($trailer->saveCount)->toBe(1)
382
+ ->and($trailer->location)->toBeInstanceOf(SpatialPoint::class)
383
+ ->and($trailer->speed)->toBe(42)
384
+ ->and($trailer->heading)->toBe(180)
385
+ ->and($trailer->odometer)->toBe(32000)
386
+ ->and($trailer->reefer_engine_hours)->toBe(800)
387
+ ->and($trailer->telematics['last_event_uuid'])->toBe('event-new')
388
+ ->and($trailer->telematics['last_provider'])->toBe('samsara')
389
+ ->and($GLOBALS['fleetops_telematic_service_broadcasts'])->toHaveCount(1);
390
+
391
+ $staleEvent = new FleetOpsTelematicServiceTelemetryEventFake([
392
+ 'uuid' => 'event-stale',
393
+ 'public_id' => 'event_stale',
394
+ 'device_uuid' => 'device-uuid',
395
+ 'event_type' => 'telemetry_update',
396
+ 'provider' => 'samsara',
397
+ 'occurred_at' => Carbon::parse('2026-09-03 10:30:00'),
398
+ ]);
399
+
400
+ fleetOpsTelematicServiceTelemetryInvoke($service, 'applyDeviceEventTelemetry', [$staleEvent, [
401
+ 'location' => ['latitude' => 1, 'longitude' => 1],
402
+ 'speed' => 5,
403
+ ], $device, true, null]);
404
+
405
+ expect($trailer->saveCount)->toBe(1)
406
+ ->and($trailer->speed)->toBe(42)
407
+ ->and($trailer->telematics['last_event_uuid'])->toBe('event-new')
408
+ ->and($GLOBALS['fleetops_telematic_service_broadcasts'])->toHaveCount(1);
409
+
410
+ Carbon::setTestNow();
411
+ });
412
+
320
413
  test('telematic service stores list snapshot sensors and skips provider failures', function () {
321
414
  $service = new FleetOpsTelematicServiceTelemetryServiceFake();
322
415
  $provider = new FleetOpsTelematicServiceTelemetryProviderFake();
@@ -1,5 +1,6 @@
1
1
  <?php
2
2
 
3
+ use Fleetbase\FleetOps\Exceptions\DeviceAlreadyAttachedException;
3
4
  use Fleetbase\FleetOps\Http\Controllers\Internal\v1\VehicleController;
4
5
  use Fleetbase\FleetOps\Models\Device;
5
6
  use Fleetbase\FleetOps\Models\Driver;
@@ -485,4 +486,17 @@ test('vehicle controller reports detach lookup failures and attach exceptions',
485
486
  $response = $controller->attachDevice(new Request(['device' => 'device-public']), 'vehicle-public');
486
487
  expect($response->getData(true))->toBe(['error' => 'Unable to attach device to vehicle. Please try again or contact support.'])
487
488
  ->and($response->getStatusCode())->toBe(500);
489
+
490
+ // A device installed on another asset is a conflict the operator must resolve by detaching first
491
+ $controller = new FleetOpsVehicleControllerProbe();
492
+ $controller->vehicle = new FleetOpsVehicleEndpointFake();
493
+ $controller->device = new class extends FleetOpsVehicleDeviceFake {
494
+ public function attachTo(Fleetbase\Models\Model $attachable): bool
495
+ {
496
+ throw DeviceAlreadyAttachedException::for('Truck 2');
497
+ }
498
+ };
499
+ $response = $controller->attachDevice(new Request(['device' => 'device-public']), 'vehicle-public');
500
+ expect($response->getData(true))->toBe(['error' => 'Device is already attached to Truck 2. Detach it before attaching it elsewhere.'])
501
+ ->and($response->getStatusCode())->toBe(409);
488
502
  });