@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.
- package/RELEASE.md +7 -6
- package/addon/components/attachment-list.hbs +65 -0
- package/addon/components/attachment-list.js +8 -0
- package/addon/components/cell/attached-vehicle.hbs +1 -1
- package/addon/components/cell/attached-vehicle.js +3 -3
- package/addon/components/cell/driver-identity.js +4 -4
- package/addon/components/cell/driver-name.hbs +1 -1
- package/addon/components/cell/translated-value.hbs +9 -0
- package/addon/components/cell/translated-value.js +57 -0
- package/addon/components/contact/card.hbs +58 -0
- package/addon/components/contact/card.js +19 -0
- package/addon/components/contact/form.hbs +1 -1
- package/addon/components/contact/panel-header.hbs +1 -1
- package/addon/components/customer/form.hbs +2 -2
- package/addon/components/device/manager.hbs +26 -28
- package/addon/components/device/manager.js +28 -2
- package/addon/components/device/panel-tabs/vehicle.hbs +13 -13
- package/addon/components/device/panel-tabs/vehicle.js +8 -0
- package/addon/components/driver/card.hbs +24 -13
- package/addon/components/driver/card.js +20 -6
- package/addon/components/driver/form.hbs +3 -3
- package/addon/components/driver/panel-header.hbs +2 -2
- package/addon/components/driver/pill.hbs +2 -2
- package/addon/components/equipment/form.js +11 -4
- package/addon/components/equipment/pill.hbs +20 -0
- package/addon/components/equipment/pill.js +3 -0
- package/addon/components/filter/multi-model.hbs +19 -0
- package/addon/components/filter/multi-model.js +90 -0
- package/addon/components/fleet/driver-listing.hbs +1 -1
- package/addon/components/fleet/vehicle-listing.hbs +1 -1
- package/addon/components/fleet-panel/driver-listing.hbs +1 -1
- package/addon/components/fleet-panel/vehicle-listing.hbs +1 -1
- package/addon/components/layout/fleet-ops-sidebar.js +9 -7
- package/addon/components/maintenance/form.js +12 -4
- package/addon/components/maintenance-schedule/form.js +13 -7
- package/addon/components/map/leaflet-live-map.hbs +52 -0
- package/addon/components/map/order-list-overlay/order.hbs +4 -4
- package/addon/components/modals/attach-equipment.hbs +22 -0
- package/addon/components/modals/attach-equipment.js +3 -0
- package/addon/components/modals/attach-telematic-device.hbs +13 -8
- package/addon/components/modals/attach-telematic-device.js +11 -1
- package/addon/components/modals/attach-trailer.hbs +30 -0
- package/addon/components/modals/attach-trailer.js +3 -0
- package/addon/components/modals/contact-form.hbs +2 -2
- package/addon/components/modals/convert-customer-to-vendor.hbs +1 -1
- package/addon/components/modals/driver-form.hbs +2 -2
- package/addon/components/modals/select-trailer.hbs +35 -0
- package/addon/components/modals/select-trailer.js +3 -0
- package/addon/components/modals/vehicle-form.hbs +2 -2
- package/addon/components/orchestrator/resource-panel.hbs +2 -2
- package/addon/components/order/customer-avatar-stack.hbs +2 -2
- package/addon/components/order/details/detail.hbs +4 -4
- package/addon/components/order/details/payload.hbs +2 -2
- package/addon/components/order/kanban-card.hbs +4 -4
- package/addon/components/order-list-overlay/order.hbs +4 -4
- package/addon/components/trailer/card.hbs +68 -0
- package/addon/components/trailer/card.js +22 -0
- package/addon/components/trailer/details/connections.hbs +57 -0
- package/addon/components/trailer/details/connections.js +59 -0
- package/addon/components/trailer/details/equipment.hbs +25 -0
- package/addon/components/trailer/details/equipment.js +52 -0
- package/addon/components/trailer/details/maintenance-history.hbs +47 -0
- package/addon/components/trailer/details/maintenance-history.js +40 -0
- package/addon/components/trailer/details/schedules.hbs +45 -0
- package/addon/components/trailer/details/schedules.js +40 -0
- package/addon/components/trailer/details/work-orders.hbs +47 -0
- package/addon/components/trailer/details/work-orders.js +40 -0
- package/addon/components/trailer/details.hbs +399 -0
- package/addon/components/trailer/details.js +118 -0
- package/addon/components/trailer/form.hbs +447 -0
- package/addon/components/trailer/form.js +165 -0
- package/addon/components/trailer/panel-header.hbs +45 -0
- package/addon/components/trailer/panel-header.js +3 -0
- package/addon/components/trailer/pill.hbs +21 -0
- package/addon/components/trailer/pill.js +3 -0
- package/addon/components/vehicle/card.hbs +42 -11
- package/addon/components/vehicle/card.js +31 -0
- package/addon/components/vehicle/details/equipment.hbs +25 -0
- package/addon/components/vehicle/details/equipment.js +55 -0
- package/addon/components/vehicle/details/trailers.hbs +35 -0
- package/addon/components/vehicle/details/trailers.js +55 -0
- package/addon/components/vehicle/form.hbs +2 -2
- package/addon/components/vehicle/panel-header.hbs +2 -2
- package/addon/components/vehicle/pill.hbs +2 -2
- package/addon/components/vendor/card.hbs +58 -0
- package/addon/components/vendor/card.js +6 -0
- package/addon/components/vendor/panel-header.hbs +2 -2
- package/addon/components/vendor/personnel-manager.hbs +2 -2
- package/addon/components/work-order/form.js +12 -4
- package/addon/controllers/connectivity/devices/index/details/vehicle.js +11 -0
- package/addon/controllers/connectivity/devices/index/details.js +3 -3
- package/addon/controllers/connectivity/devices/index.js +1 -0
- package/addon/controllers/connectivity/telematics/details/attachments.js +28 -47
- package/addon/controllers/connectivity/telematics/details/devices.js +1 -0
- package/addon/controllers/management/contacts/customers.js +4 -0
- package/addon/controllers/management/contacts/index.js +32 -0
- package/addon/controllers/management/trailers/index/details.js +119 -0
- package/addon/controllers/management/trailers/index/edit.js +67 -0
- package/addon/controllers/management/trailers/index/new.js +43 -0
- package/addon/controllers/management/trailers/index.js +599 -0
- package/addon/controllers/management/vehicles/index/details.js +16 -0
- package/addon/controllers/management/vehicles/index.js +78 -4
- package/addon/controllers/management/vendors/index.js +32 -0
- package/addon/controllers/operations/orders/index.js +3 -1
- package/addon/controllers/settings/custom-fields.js +8 -0
- package/addon/extension.js +7 -0
- package/addon/helpers/placeholder-image.js +6 -0
- package/addon/helpers/resource-image.js +10 -0
- package/addon/helpers/trailer-placeholder-image.js +6 -0
- package/addon/routes/management/trailers/index/details/connections.js +2 -0
- package/addon/routes/management/trailers/index/details/devices.js +2 -0
- package/addon/routes/management/trailers/index/details/equipment.js +2 -0
- package/addon/routes/management/trailers/index/details/index.js +2 -0
- package/addon/routes/management/trailers/index/details/maintenance-history.js +2 -0
- package/addon/routes/management/trailers/index/details/positions.js +2 -0
- package/addon/routes/management/trailers/index/details/schedules.js +2 -0
- package/addon/routes/management/trailers/index/details/work-orders.js +2 -0
- package/addon/routes/management/trailers/index/details.js +30 -0
- package/addon/routes/management/trailers/index/edit.js +23 -0
- package/addon/routes/management/trailers/index/new.js +14 -0
- package/addon/routes/management/trailers/index.js +42 -0
- package/addon/routes/management/vehicles/index/details/trailers.js +2 -0
- package/addon/routes/management/vehicles/index.js +7 -1
- package/addon/routes.js +17 -0
- package/addon/services/device-actions.js +26 -16
- package/addon/services/trailer-actions.js +394 -0
- package/addon/services/vehicle-actions.js +111 -0
- package/addon/styles/fleetops-engine.css +349 -0
- package/addon/templates/connectivity/devices/index/details/vehicle.hbs +19 -19
- package/addon/templates/connectivity/telematics/details/attachments.hbs +15 -15
- package/addon/templates/management/contacts/customers.hbs +48 -29
- package/addon/templates/management/contacts/index.hbs +52 -33
- package/addon/templates/management/trailers/index/details/connections.hbs +1 -0
- package/addon/templates/management/trailers/index/details/devices.hbs +1 -0
- package/addon/templates/management/trailers/index/details/equipment.hbs +1 -0
- package/addon/templates/management/trailers/index/details/index.hbs +1 -0
- package/addon/templates/management/trailers/index/details/maintenance-history.hbs +1 -0
- package/addon/templates/management/trailers/index/details/positions.hbs +1 -0
- package/addon/templates/management/trailers/index/details/schedules.hbs +1 -0
- package/addon/templates/management/trailers/index/details/work-orders.hbs +1 -0
- package/addon/templates/management/trailers/index/details.hbs +15 -0
- package/addon/templates/management/trailers/index/edit.hbs +12 -0
- package/addon/templates/management/trailers/index/new.hbs +11 -0
- package/addon/templates/management/trailers/index.hbs +67 -0
- package/addon/templates/management/vehicles/index/details/equipment.hbs +1 -1
- package/addon/templates/management/vehicles/index/details/trailers.hbs +1 -0
- package/addon/templates/management/vehicles/index/details.hbs +1 -1
- package/addon/templates/management/vendors/index.hbs +66 -32
- package/addon/utils/device-table-columns.js +10 -6
- package/addon/utils/live-map-card-content.js +56 -0
- package/addon/utils/placeholder-images.js +71 -0
- package/addon/utils/trailer-placeholder-image.js +11 -0
- package/app/components/attachment-list.js +1 -0
- package/app/components/cell/translated-value.js +1 -0
- package/app/components/contact/card.js +1 -0
- package/app/components/equipment/pill.js +1 -0
- package/app/components/filter/multi-model.js +1 -0
- package/app/components/modals/attach-equipment.js +1 -0
- package/app/components/modals/attach-trailer.js +1 -0
- package/app/components/modals/select-trailer.js +1 -0
- package/app/components/trailer/card.js +1 -0
- package/app/components/trailer/details/connections.js +1 -0
- package/app/components/trailer/details/equipment.js +1 -0
- package/app/components/trailer/details/maintenance-history.js +1 -0
- package/app/components/trailer/details/schedules.js +1 -0
- package/app/components/trailer/details/work-orders.js +1 -0
- package/app/components/trailer/details.js +1 -0
- package/app/components/trailer/form.js +1 -0
- package/app/components/trailer/panel-header.js +1 -0
- package/app/components/trailer/pill.js +1 -0
- package/app/components/vehicle/details/equipment.js +1 -0
- package/app/components/vehicle/details/trailers.js +1 -0
- package/app/components/vendor/card.js +1 -0
- package/app/controllers/management/trailers/index/details.js +1 -0
- package/app/controllers/management/trailers/index/edit.js +1 -0
- package/app/controllers/management/trailers/index/new.js +1 -0
- package/app/controllers/management/trailers/index.js +1 -0
- package/app/helpers/placeholder-image.js +1 -0
- package/app/helpers/resource-image.js +1 -0
- package/app/helpers/trailer-placeholder-image.js +1 -0
- package/app/routes/management/trailers/index/details/connections.js +1 -0
- package/app/routes/management/trailers/index/details/devices.js +1 -0
- package/app/routes/management/trailers/index/details/equipment.js +1 -0
- package/app/routes/management/trailers/index/details/index.js +1 -0
- package/app/routes/management/trailers/index/details/maintenance-history.js +1 -0
- package/app/routes/management/trailers/index/details/positions.js +1 -0
- package/app/routes/management/trailers/index/details/schedules.js +1 -0
- package/app/routes/management/trailers/index/details/work-orders.js +1 -0
- package/app/routes/management/trailers/index/details.js +1 -0
- package/app/routes/management/trailers/index/edit.js +1 -0
- package/app/routes/management/trailers/index/new.js +1 -0
- package/app/routes/management/trailers/index.js +1 -0
- package/app/routes/management/vehicles/index/details/trailers.js +1 -0
- package/app/services/trailer-actions.js +1 -0
- package/app/templates/management/trailers/index/details/connections.js +1 -0
- package/app/templates/management/trailers/index/details/devices.js +1 -0
- package/app/templates/management/trailers/index/details/equipment.js +1 -0
- package/app/templates/management/trailers/index/details/index.js +1 -0
- package/app/templates/management/trailers/index/details/maintenance-history.js +1 -0
- package/app/templates/management/trailers/index/details/positions.js +1 -0
- package/app/templates/management/trailers/index/details/schedules.js +1 -0
- package/app/templates/management/trailers/index/details/work-orders.js +1 -0
- package/app/templates/management/trailers/index/details.js +1 -0
- package/app/templates/management/trailers/index/edit.js +1 -0
- package/app/templates/management/trailers/index/new.js +1 -0
- package/app/templates/management/trailers/index.js +1 -0
- package/app/templates/management/vehicles/index/details/trailers.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +1 -1
- package/server/migrations/2026_09_03_000001_add_first_class_trailer_columns_to_assets_table.php +58 -0
- package/server/migrations/2026_09_03_000002_create_asset_connections_table.php +43 -0
- package/server/migrations/2026_09_03_000003_create_device_installations_table.php +33 -0
- package/server/src/Auth/Schemas/FleetOps.php +7 -0
- package/server/src/Events/TrailerLocationChanged.php +70 -0
- package/server/src/Exceptions/DeviceAlreadyAttachedException.php +18 -0
- package/server/src/Exports/TrailerExport.php +31 -0
- package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesFleetOpsApiResources.php +8 -2
- package/server/src/Http/Controllers/Api/v1/DeviceController.php +22 -15
- package/server/src/Http/Controllers/Api/v1/EquipmentController.php +47 -0
- package/server/src/Http/Controllers/Api/v1/TrailerController.php +362 -0
- package/server/src/Http/Controllers/Api/v1/VehicleController.php +1 -0
- package/server/src/Http/Controllers/Internal/v1/DeviceController.php +3 -0
- package/server/src/Http/Controllers/Internal/v1/LiveController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/SearchController.php +16 -0
- package/server/src/Http/Controllers/Internal/v1/TrailerController.php +360 -0
- package/server/src/Http/Controllers/Internal/v1/VehicleController.php +74 -0
- package/server/src/Http/Filter/EquipmentFilter.php +51 -0
- package/server/src/Http/Filter/OrderFilter.php +15 -0
- package/server/src/Http/Filter/TrailerFilter.php +269 -0
- package/server/src/Http/Filter/VehicleFilter.php +70 -3
- package/server/src/Http/Requests/CreateDeviceRequest.php +1 -1
- package/server/src/Http/Requests/CreateEquipmentRequest.php +1 -1
- package/server/src/Http/Requests/CreateTrailerRequest.php +54 -0
- package/server/src/Http/Requests/UpdateTrailerRequest.php +7 -0
- package/server/src/Http/Resources/v1/AssetConnection.php +90 -0
- package/server/src/Http/Resources/v1/Concerns/ResolvesPublicRelationFields.php +29 -3
- package/server/src/Http/Resources/v1/Device.php +3 -1
- package/server/src/Http/Resources/v1/Equipment.php +3 -1
- package/server/src/Http/Resources/v1/Fleet.php +3 -1
- package/server/src/Http/Resources/v1/Index/Vehicle.php +10 -0
- package/server/src/Http/Resources/v1/Trailer.php +199 -0
- package/server/src/Http/Resources/v1/Vehicle.php +2 -0
- package/server/src/Http/Resources/v1/VehicleWithoutDriver.php +3 -0
- package/server/src/Imports/TrailerImport.php +33 -0
- package/server/src/Models/Asset.php +39 -10
- package/server/src/Models/AssetConnection.php +43 -0
- package/server/src/Models/Device.php +39 -8
- package/server/src/Models/DeviceInstallation.php +27 -0
- package/server/src/Models/Equipment.php +2 -0
- package/server/src/Models/Trailer.php +181 -0
- package/server/src/Models/Vehicle.php +13 -0
- package/server/src/Providers/FleetOpsServiceProvider.php +4 -2
- package/server/src/Support/Ai/FleetOpsAiQueryResources.php +22 -0
- package/server/src/Support/Reporting/FleetOpsReportSchema.php +38 -0
- package/server/src/Support/Telematics/TelematicService.php +35 -0
- package/server/src/Support/TrailerSummary.php +35 -0
- package/server/src/routes.php +29 -0
- package/server/tests/CompactResourceSerializationTest.php +36 -0
- package/server/tests/ControllerFilterContractsTest.php +29 -0
- package/server/tests/Feature/Http/Api/DeviceControllerContractsTest.php +30 -13
- package/server/tests/Feature/Http/Api/TrailerControllerLifecycleTest.php +640 -0
- package/server/tests/Feature/Http/Api/VehiclePublicContractTest.php +23 -0
- package/server/tests/Feature/Http/Internal/DeviceControllerContractsTest.php +20 -5
- package/server/tests/Feature/Http/Internal/LiveControllerEndpointsTest.php +19 -0
- package/server/tests/ModelAccessorContractsTest.php +10 -0
- package/server/tests/OrderFilterExecutionTest.php +5 -0
- package/server/tests/PublicApiControllerInputContractsTest.php +11 -0
- package/server/tests/PublicFleetOpsRestEndpointsTest.php +15 -3
- package/server/tests/ReportSchemaContractsTest.php +4 -0
- package/server/tests/TelematicsHardeningTest.php +4 -2
- package/server/tests/TrailerSupportCoverageTest.php +524 -0
- package/server/tests/Unit/Http/Resources/FleetResourceTest.php +17 -0
- package/server/tests/Unit/Http/Resources/IndexVehicleResourceTest.php +35 -0
- package/server/tests/Unit/Models/EquipmentTest.php +22 -0
- package/server/tests/Unit/Models/ModelLookupFallbackSeamsTest.php +7 -3
- package/server/tests/Unit/Models/TrailerTest.php +204 -0
- package/server/tests/Unit/Support/Telematics/TelematicServiceTelemetryTest.php +93 -0
- package/server/tests/VehicleControllerHelperContractsTest.php +14 -0
- package/server/tests/VehicleResourceContractsTest.php +39 -0
- package/translations/ar-ae.yml +396 -0
- package/translations/bg-bg.yaml +424 -0
- package/translations/en-us.yaml +423 -0
- package/translations/es-pa.yaml +425 -0
- package/translations/fr-fr.yaml +424 -0
- package/translations/mn-mn.yaml +424 -0
- package/translations/pt-br.yaml +424 -0
- package/translations/ru-ru.yaml +424 -0
- package/translations/vi-vn.yaml +435 -0
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
use Fleetbase\FleetOps\Models\Driver;
|
|
4
4
|
use Fleetbase\FleetOps\Models\Equipment;
|
|
5
5
|
use Fleetbase\FleetOps\Models\TrackingNumber;
|
|
6
|
+
use Fleetbase\FleetOps\Models\Trailer;
|
|
6
7
|
use Fleetbase\FleetOps\Models\Vehicle;
|
|
7
8
|
use Fleetbase\FleetOps\Models\VehicleDevice;
|
|
8
9
|
use Illuminate\Database\ConnectionResolver;
|
|
@@ -114,16 +115,19 @@ test('vehicle devices fall back to a direct vehicle lookup by uuid', function ()
|
|
|
114
115
|
->and($seen)->toBe(['vehicle_lookupone']);
|
|
115
116
|
});
|
|
116
117
|
|
|
117
|
-
test('
|
|
118
|
+
test('known equipable aliases are normalized and unrecognised types are stored verbatim', function () {
|
|
118
119
|
$equipment = new Equipment();
|
|
119
120
|
|
|
120
121
|
// Known short names map onto their fully qualified model class
|
|
121
122
|
$equipment->equipable_type = 'vehicle';
|
|
122
123
|
expect($equipment->getAttributes()['equipable_type'])->toContain('Vehicle');
|
|
123
124
|
|
|
124
|
-
// Anything else is kept as given rather than silently dropped
|
|
125
125
|
$equipment->equipable_type = 'trailer';
|
|
126
|
-
expect($equipment->getAttributes()['equipable_type'])->toBe(
|
|
126
|
+
expect($equipment->getAttributes()['equipable_type'])->toBe(Trailer::class);
|
|
127
|
+
|
|
128
|
+
// Anything else is kept as given rather than silently dropped
|
|
129
|
+
$equipment->equipable_type = 'cargo-bay';
|
|
130
|
+
expect($equipment->getAttributes()['equipable_type'])->toBe('cargo-bay');
|
|
127
131
|
});
|
|
128
132
|
|
|
129
133
|
test('tracking number generation redraws when the first number is already taken', function () {
|
|
@@ -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
|
});
|
|
@@ -311,3 +311,42 @@ test('vehicle without driver resource omits driver relationships and serializes
|
|
|
311
311
|
])
|
|
312
312
|
->and($webhook)->not->toHaveKey('driver');
|
|
313
313
|
});
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Resolve the request's `with` list the way a nested resource does.
|
|
317
|
+
*
|
|
318
|
+
* @return array<int, string>
|
|
319
|
+
*/
|
|
320
|
+
function fleetopsVehicleResourceRequestedRelations(mixed $resource, array $query): array
|
|
321
|
+
{
|
|
322
|
+
$request = Request::create('/api/v1/fleet-ops/orders', 'GET', $query);
|
|
323
|
+
$method = new ReflectionMethod(VehicleResource::class, 'requestedRelations');
|
|
324
|
+
$method->setAccessible(true);
|
|
325
|
+
|
|
326
|
+
return $method->invoke(new VehicleResource($resource), $request);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
test('requested relations keep only what the wrapped model defines', function () {
|
|
330
|
+
$vehicle = new class(['uuid' => 'vehicle-uuid']) extends FleetOpsVehicleResourceFixture {
|
|
331
|
+
public function driver(): void
|
|
332
|
+
{
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
// Nested under an Order, the request's `with` describes the order:
|
|
337
|
+
// `payload` and `driverAssigned.vehicle` are not vehicle relations and must
|
|
338
|
+
// never reach this model's loader. The vehicle's own relations still pass,
|
|
339
|
+
// including a nested path whose root the vehicle defines.
|
|
340
|
+
expect(fleetopsVehicleResourceRequestedRelations($vehicle, ['with' => ['payload', 'driverAssigned.vehicle', 'driver', 'driver.user', '']]))->toBe(['driver', 'driver.user'])
|
|
341
|
+
->and(fleetopsVehicleResourceRequestedRelations($vehicle, ['with' => 'payload,driver']))->toBe(['driver'])
|
|
342
|
+
->and(fleetopsVehicleResourceRequestedRelations($vehicle, ['with' => ['.driver']]))->toBe([])
|
|
343
|
+
->and(fleetopsVehicleResourceRequestedRelations($vehicle, []))->toBe([]);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test('requested relations are empty when the resource cannot load any', function () {
|
|
347
|
+
// Webhook fixtures and compact serializers hand the resource plain data;
|
|
348
|
+
// there is no loader to feed, so nothing is requested from it.
|
|
349
|
+
expect(fleetopsVehicleResourceRequestedRelations(['uuid' => 'vehicle-uuid'], ['with' => ['driver']]))->toBe([])
|
|
350
|
+
->and(fleetopsVehicleResourceRequestedRelations((object) ['uuid' => 'vehicle-uuid'], ['with' => ['driver']]))->toBe([])
|
|
351
|
+
->and(fleetopsVehicleResourceRequestedRelations(fleetopsVehicleResourceFixture(), ['with' => ['driver']]))->toBe([]);
|
|
352
|
+
});
|