@fleetbase/fleetops-engine 0.6.60 → 0.6.61
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 +9 -20
- package/addon/components/ai/route-preview-map.js +1 -5
- package/addon/components/customer/orders.js +2 -1
- package/addon/components/driver/card.hbs +49 -0
- package/addon/components/driver/card.js +30 -0
- package/addon/components/map/leaflet-live-map.js +4 -19
- package/addon/components/modals/place-details.hbs +1 -1
- package/addon/components/modals/point-map.hbs +1 -1
- package/addon/components/orchestrator-workbench.js +2 -1
- package/addon/components/order/form/route.js +3 -0
- package/addon/components/order-tracking-lookup.js +2 -1
- package/addon/components/place/details.hbs +1 -1
- package/addon/components/positions-replay.js +4 -1
- package/addon/components/service-area/details.hbs +1 -1
- package/addon/components/widget/live-fleet.js +7 -7
- package/addon/components/zone/details.hbs +1 -1
- package/addon/controllers/management/drivers/index.js +22 -1
- package/addon/controllers/settings/map.js +16 -0
- package/addon/helpers/leaflet-tile-url.js +19 -0
- package/addon/services/map-settings.js +22 -0
- package/addon/templates/management/drivers/index.hbs +67 -33
- package/addon/templates/settings/map.hbs +10 -0
- package/addon/utils/leaflet-tile-url.js +31 -0
- package/addon/utils/prepare-place-for-save.js +13 -0
- package/app/components/driver/card.js +1 -0
- package/app/helpers/leaflet-tile-url.js +1 -0
- package/app/utils/leaflet-tile-url.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/index.js +11 -0
- package/package.json +2 -1
- package/server/src/Http/Controllers/Api/v1/DriverController.php +220 -3
- package/server/src/Http/Controllers/Api/v1/ManifestController.php +250 -0
- package/server/src/Http/Controllers/Api/v1/VehicleController.php +6 -0
- package/server/src/Http/Controllers/Internal/v1/SettingController.php +22 -0
- package/server/src/Http/Filter/FuelReportFilter.php +29 -0
- package/server/src/Http/Filter/IssueFilter.php +29 -0
- package/server/src/Http/Requests/CreateVehicleRequest.php +10 -5
- package/server/src/Http/Resources/v1/Manifest.php +52 -0
- package/server/src/Http/Resources/v1/ManifestStop.php +47 -0
- package/server/src/Http/Resources/v1/OrderConfig.php +51 -4
- package/server/src/Models/Place.php +1 -1
- package/server/src/routes.php +21 -0
- package/server/tests/ApiDriverControllerContractsTest.php +33 -0
- package/server/tests/ApiDriverPasswordContractsTest.php +457 -0
- package/server/tests/ApiManifestControllerContractsTest.php +599 -0
- package/server/tests/ApiVehicleControllerContractsTest.php +35 -0
- package/server/tests/CompactResourceSerializationTest.php +13 -7
- package/server/tests/Feature/Http/Api/DriverControllerSessionHelpersTest.php +61 -0
- package/server/tests/Feature/Http/Internal/PlaceControllerTest.php +45 -0
- package/server/tests/RequestContractsTest.php +5 -0
- package/server/tests/SettingControllerContractsTest.php +33 -0
- package/server/tests/Unit/Http/Filter/FuelReportFilterTest.php +135 -0
- package/server/tests/Unit/Http/Filter/IssueFilterTest.php +43 -0
- package/server/tests/Unit/Http/Resources/ManifestResourceTest.php +129 -0
- package/server/tests/Unit/Http/Resources/OrderConfigResourceTest.php +91 -0
- package/translations/en-us.yaml +4 -0
|
@@ -10,6 +10,7 @@ use Illuminate\Database\ConnectionResolver;
|
|
|
10
10
|
use Illuminate\Database\Eloquent\Model as EloquentModel;
|
|
11
11
|
use Illuminate\Database\SQLiteConnection;
|
|
12
12
|
use Illuminate\Http\Request;
|
|
13
|
+
use Illuminate\Support\Facades\Hash;
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Covers the API DriverController protected session/persistence helpers
|
|
@@ -73,6 +74,35 @@ function fleetopsDriverSessionHelpersBoot(): SQLiteConnection
|
|
|
73
74
|
});
|
|
74
75
|
Illuminate\Support\Facades\DB::clearResolvedInstance('db');
|
|
75
76
|
|
|
77
|
+
/*
|
|
78
|
+
* Only the hashing contract ships with this package, not an implementation,
|
|
79
|
+
* so the User password mutator has nothing to call. Bind PHP's own hashing
|
|
80
|
+
* behind the contract: the point of the assertions below is that a password
|
|
81
|
+
* is stored hashed and verifies, not which algorithm did it.
|
|
82
|
+
*/
|
|
83
|
+
app()->instance('hash', new class implements Illuminate\Contracts\Hashing\Hasher {
|
|
84
|
+
public function info($hashedValue): array
|
|
85
|
+
{
|
|
86
|
+
return password_get_info($hashedValue);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public function make($value, array $options = []): string
|
|
90
|
+
{
|
|
91
|
+
return password_hash($value, PASSWORD_BCRYPT);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
public function check($value, $hashedValue, array $options = []): bool
|
|
95
|
+
{
|
|
96
|
+
return is_string($hashedValue) && password_verify($value, $hashedValue);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public function needsRehash($hashedValue, array $options = []): bool
|
|
100
|
+
{
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
Hash::clearResolvedInstance('hash');
|
|
105
|
+
|
|
76
106
|
$schema = $connection->getSchemaBuilder();
|
|
77
107
|
app()->instance('db.schema', $schema);
|
|
78
108
|
$tables = [
|
|
@@ -163,6 +193,18 @@ test('user and driver persistence helpers write records', function () {
|
|
|
163
193
|
expect($user)->toBeInstanceOf(User::class)
|
|
164
194
|
->and($connection->table('users')->where('name', 'New User')->count())->toBe(1);
|
|
165
195
|
|
|
196
|
+
/*
|
|
197
|
+
* `password` is guarded on User, so mass assignment drops it without a
|
|
198
|
+
* word. The create endpoint has always documented and validated one, so
|
|
199
|
+
* unless the helper sets it explicitly a driver created through the API
|
|
200
|
+
* can never sign in with the password chosen for them.
|
|
201
|
+
*/
|
|
202
|
+
$withPassword = $probe->callHelper('createUser', ['name' => 'Password User', 'password' => 'seeded-password']);
|
|
203
|
+
$stored = $connection->table('users')->where('name', 'Password User')->value('password');
|
|
204
|
+
expect(Hash::check('seeded-password', $withPassword->password))->toBeTrue()
|
|
205
|
+
->and($stored)->not->toBeNull()
|
|
206
|
+
->and($stored)->not->toBe('seeded-password');
|
|
207
|
+
|
|
166
208
|
$driver = $probe->callHelper('createDriver', ['company_uuid' => '22222222-2222-4222-8222-222222222222', 'user_uuid' => 'user-1']);
|
|
167
209
|
expect($driver)->toBeInstanceOf(Driver::class);
|
|
168
210
|
|
|
@@ -173,6 +215,25 @@ test('user and driver persistence helpers write records', function () {
|
|
|
173
215
|
->and($connection->table('user_devices')->count())->toBe(1);
|
|
174
216
|
});
|
|
175
217
|
|
|
218
|
+
test('the driver user loader reads the password column the relation omits', function () {
|
|
219
|
+
$connection = fleetopsDriverSessionHelpersBoot();
|
|
220
|
+
$probe = new FleetOpsDriverSessionHelpersProbe();
|
|
221
|
+
|
|
222
|
+
$connection->table('users')->where('uuid', 'user-1')->update(['password' => 'stored-hash']);
|
|
223
|
+
|
|
224
|
+
/*
|
|
225
|
+
* The `user` relation selects a named subset of columns and `password` is
|
|
226
|
+
* not among them, so a password comparison made through it fails for
|
|
227
|
+
* everyone regardless of what they typed. Anything checking a password has
|
|
228
|
+
* to load the record itself.
|
|
229
|
+
*/
|
|
230
|
+
$driver = Driver::where('public_id', 'driver_one')->first();
|
|
231
|
+
expect($probe->callHelper('findDriverUserRecord', $driver)->password)->toBe('stored-hash');
|
|
232
|
+
|
|
233
|
+
$unlinked = new Driver();
|
|
234
|
+
expect($probe->callHelper('findDriverUserRecord', $unlinked))->toBeNull();
|
|
235
|
+
});
|
|
236
|
+
|
|
176
237
|
test('lookup utility and resource helpers resolve records and wrappers', function () {
|
|
177
238
|
fleetopsDriverSessionHelpersBoot();
|
|
178
239
|
$probe = new FleetOpsDriverSessionHelpersProbe();
|
|
@@ -36,6 +36,18 @@ if (!Request::hasMacro('resolveFilesFromIds')) {
|
|
|
36
36
|
Request::macro('resolveFilesFromIds', fn () => FleetOpsInternalPlaceEndpointsState::$files);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
if (!Request::hasMacro('or')) {
|
|
40
|
+
Request::macro('or', function (array $params = [], $default = null) {
|
|
41
|
+
foreach ($params as $param) {
|
|
42
|
+
if ($this->has($param)) {
|
|
43
|
+
return $this->input($param);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return $default;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
39
51
|
class FleetOpsInternalPlaceEndpointsState
|
|
40
52
|
{
|
|
41
53
|
public static array $files = [];
|
|
@@ -81,6 +93,14 @@ class FleetOpsInternalPlaceEndpointsPlaceFake extends Place
|
|
|
81
93
|
}
|
|
82
94
|
}
|
|
83
95
|
|
|
96
|
+
class FleetOpsInternalPlaceUpdateFake extends Place
|
|
97
|
+
{
|
|
98
|
+
public function applyDirectivesToQuery(Request $request, $builder)
|
|
99
|
+
{
|
|
100
|
+
return $builder;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
84
104
|
class FleetOpsInternalPlaceEndpointsProbe extends PlaceController
|
|
85
105
|
{
|
|
86
106
|
public function callProtected(string $method, array $arguments = []): mixed
|
|
@@ -128,6 +148,7 @@ function fleetopsInternalPlaceEndpointsBoot(): SQLiteConnection
|
|
|
128
148
|
$table->string('postal_code')->nullable();
|
|
129
149
|
$table->string('location')->nullable();
|
|
130
150
|
$table->timestamp('created_at')->nullable();
|
|
151
|
+
$table->timestamp('updated_at')->nullable();
|
|
131
152
|
$table->timestamp('deleted_at')->nullable();
|
|
132
153
|
});
|
|
133
154
|
$schema->create('files', function ($table) {
|
|
@@ -250,3 +271,27 @@ test('sync custom field values helper delegates to the place model', function ()
|
|
|
250
271
|
|
|
251
272
|
expect($place->syncedValues)->toBe([['priority' => 'high']]);
|
|
252
273
|
});
|
|
274
|
+
|
|
275
|
+
test('place updates accept serialized display-only attributes', function () {
|
|
276
|
+
$connection = fleetopsInternalPlaceEndpointsBoot();
|
|
277
|
+
$connection->table('places')->insert([
|
|
278
|
+
'uuid' => '77777777-7777-4777-8777-777777777777',
|
|
279
|
+
'public_id' => 'place_display1',
|
|
280
|
+
'company_uuid' => 'company-1',
|
|
281
|
+
'name' => 'Original place',
|
|
282
|
+
'street1' => '205 Dostyk Avenue',
|
|
283
|
+
]);
|
|
284
|
+
|
|
285
|
+
$place = (new FleetOpsInternalPlaceUpdateFake())->updateRecordFromRequest(Request::create('/int/v1/places/place_display1', 'PUT', [
|
|
286
|
+
'place' => [
|
|
287
|
+
'name' => 'Updated place',
|
|
288
|
+
'street2' => 'Entrance 2',
|
|
289
|
+
'avatar_value' => 'basic-building',
|
|
290
|
+
'eta' => '12 minutes',
|
|
291
|
+
],
|
|
292
|
+
]), 'place_display1', options: ['return_object' => true]);
|
|
293
|
+
|
|
294
|
+
expect($place->name)->toBe('Updated place')
|
|
295
|
+
->and($place->street2)->toBe('Entrance 2')
|
|
296
|
+
->and($connection->table('places')->where('public_id', 'place_display1')->value('name'))->toBe('Updated place');
|
|
297
|
+
});
|
|
@@ -300,6 +300,11 @@ namespace {
|
|
|
300
300
|
->and($vehicleRules['longitude'])->toBe(['nullable', 'required_with:latitude'])
|
|
301
301
|
->and(ruleStrings($vehicleRules['status']))->toContain('nullable')
|
|
302
302
|
->and(implode('|', ruleStrings($vehicleRules['status'])))->toContain('operational')
|
|
303
|
+
// The model casts odometer to an integer, so an unvalidated string
|
|
304
|
+
// would land as 0 — a vehicle reporting zero miles rather than an
|
|
305
|
+
// error. These rules are what stop that.
|
|
306
|
+
->and($vehicleRules['odometer'])->toBe('nullable|numeric|min:0')
|
|
307
|
+
->and($vehicleRules['odometer_unit'])->toBe('nullable|string|max:12')
|
|
303
308
|
->and($fuelReportRules['driver'])->toBe(['required'])
|
|
304
309
|
->and($fuelReportRules['odometer'])->toBe(['required'])
|
|
305
310
|
->and($fuelReportRules['volume'])->toBe(['required'])
|
|
@@ -408,3 +408,36 @@ test('setting controller handles map scheduling orchestrator and card field sett
|
|
|
408
408
|
'settings' => ['standard' => ['tracking'], 'byConfig' => [], 'meta' => ['dense' => true]],
|
|
409
409
|
]);
|
|
410
410
|
});
|
|
411
|
+
|
|
412
|
+
test('setting controller sanitizes leaflet tile provider urls', function () {
|
|
413
|
+
$controller = new FleetOpsSettingControllerProbe();
|
|
414
|
+
|
|
415
|
+
$saved = fleetopsJsonPayload($controller->saveMapSettings(new Request([
|
|
416
|
+
'settings' => [
|
|
417
|
+
'leafletTileUrl' => ' https://tile.openstreetmap.org/{z}/{x}/{y}.png ',
|
|
418
|
+
'leafletDarkTileUrl' => 'javascript:alert(1)',
|
|
419
|
+
],
|
|
420
|
+
])));
|
|
421
|
+
|
|
422
|
+
expect($saved['leafletTileUrl'])->toBe('https://tile.openstreetmap.org/{z}/{x}/{y}.png')
|
|
423
|
+
->and($saved['leafletDarkTileUrl'])->toBe('')
|
|
424
|
+
->and($controller->configuredCompany['fleet-ops.map-settings'])->toMatchArray([
|
|
425
|
+
'leafletTileUrl' => 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
|
426
|
+
'leafletDarkTileUrl' => '',
|
|
427
|
+
]);
|
|
428
|
+
|
|
429
|
+
$savedNonString = fleetopsJsonPayload($controller->saveMapSettings(new Request([
|
|
430
|
+
'settings' => [
|
|
431
|
+
'leafletTileUrl' => ['not' => 'a-string'],
|
|
432
|
+
'leafletDarkTileUrl' => 'ftp://tiles.example.com/{z}/{x}/{y}.png',
|
|
433
|
+
],
|
|
434
|
+
])));
|
|
435
|
+
|
|
436
|
+
expect($savedNonString['leafletTileUrl'])->toBe('')
|
|
437
|
+
->and($savedNonString['leafletDarkTileUrl'])->toBe('');
|
|
438
|
+
|
|
439
|
+
$defaults = fleetopsJsonPayload((new FleetOpsSettingControllerProbe())->getMapSettings());
|
|
440
|
+
|
|
441
|
+
expect($defaults['leafletTileUrl'])->toBe('')
|
|
442
|
+
->and($defaults['leafletDarkTileUrl'])->toBe('');
|
|
443
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Fleetbase\FleetOps\Http\Filter\FuelReportFilter;
|
|
4
|
+
use Fleetbase\Http\Filter\Filter;
|
|
5
|
+
use Illuminate\Http\Request;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Covers the FuelReportFilter query branches against a recording builder fake:
|
|
9
|
+
* base scoping, keyword search, priority and status constraints, and the
|
|
10
|
+
* assignee, reporter, driver and vehicle relation lookups routing by uuid,
|
|
11
|
+
* public id, or fallback search.
|
|
12
|
+
*/
|
|
13
|
+
class FleetOpsFuelReportFilterUnitQuery
|
|
14
|
+
{
|
|
15
|
+
public array $calls = [];
|
|
16
|
+
|
|
17
|
+
public function where(...$arguments): self
|
|
18
|
+
{
|
|
19
|
+
if (isset($arguments[0]) && is_callable($arguments[0])) {
|
|
20
|
+
$nested = new self();
|
|
21
|
+
$arguments[0]($nested);
|
|
22
|
+
$this->calls[] = ['whereNested', $nested->calls];
|
|
23
|
+
|
|
24
|
+
return $this;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
$this->calls[] = ['where', $arguments];
|
|
28
|
+
|
|
29
|
+
return $this;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public function whereHas(string $relation, ?callable $callback = null): self
|
|
33
|
+
{
|
|
34
|
+
$nested = new self();
|
|
35
|
+
if ($callback) {
|
|
36
|
+
$callback($nested);
|
|
37
|
+
}
|
|
38
|
+
$this->calls[] = ['whereHas', $relation, $nested->calls];
|
|
39
|
+
|
|
40
|
+
return $this;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public function search(?string $query): self
|
|
44
|
+
{
|
|
45
|
+
$this->calls[] = ['search', $query];
|
|
46
|
+
|
|
47
|
+
return $this;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public function whereIn(string $column, mixed $values): self
|
|
51
|
+
{
|
|
52
|
+
$this->calls[] = ['whereIn', $column, $values];
|
|
53
|
+
|
|
54
|
+
return $this;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public function whereBetween(string $column, mixed $bounds): self
|
|
58
|
+
{
|
|
59
|
+
$this->calls[] = ['whereBetween', $column, $bounds];
|
|
60
|
+
|
|
61
|
+
return $this;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public function whereDate(string $column, mixed $value): self
|
|
65
|
+
{
|
|
66
|
+
$this->calls[] = ['whereDate', $column, $value];
|
|
67
|
+
|
|
68
|
+
return $this;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public function __call($method, $arguments): self
|
|
72
|
+
{
|
|
73
|
+
$this->calls[] = [$method, $arguments];
|
|
74
|
+
|
|
75
|
+
return $this;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function fleetopsFuelReportFilterUnitFilter(FleetOpsFuelReportFilterUnitQuery $builder, ?Request $request = null): FuelReportFilter
|
|
80
|
+
{
|
|
81
|
+
$filter = (new ReflectionClass(FuelReportFilter::class))->newInstanceWithoutConstructor();
|
|
82
|
+
|
|
83
|
+
foreach ([
|
|
84
|
+
'builder' => $builder,
|
|
85
|
+
'session' => new class {
|
|
86
|
+
public function get(string $key): ?string
|
|
87
|
+
{
|
|
88
|
+
return $key === 'company' ? 'company-uuid' : null;
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
'request' => $request ?? new Request(),
|
|
92
|
+
] as $property => $value) {
|
|
93
|
+
$reflection = new ReflectionProperty(Filter::class, $property);
|
|
94
|
+
$reflection->setAccessible(true);
|
|
95
|
+
$reflection->setValue($filter, $value);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return $filter;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
test('fuel report filter scopes by driver_uuid and driver_assigned, not just driver', function () {
|
|
102
|
+
foreach (['driverUuid', 'driverAssigned'] as $method) {
|
|
103
|
+
$builder = new FleetOpsFuelReportFilterUnitQuery();
|
|
104
|
+
$filter = fleetopsFuelReportFilterUnitFilter($builder);
|
|
105
|
+
|
|
106
|
+
$filter->{$method}('driver_filterone');
|
|
107
|
+
|
|
108
|
+
$whereHas = collect($builder->calls)->where(0, 'whereHas')->firstWhere(1, 'driver');
|
|
109
|
+
expect($whereHas)->not->toBeNull("{$method} must constrain the driver relation")
|
|
110
|
+
->and($whereHas[2][0][1][0])->toBe('public_id');
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('fuel report filter driver aliases route uuids the same way driver does', function () {
|
|
115
|
+
$builder = new FleetOpsFuelReportFilterUnitQuery();
|
|
116
|
+
$filter = fleetopsFuelReportFilterUnitFilter($builder);
|
|
117
|
+
|
|
118
|
+
$filter->driverUuid('33333333-3333-4333-8333-333333333333');
|
|
119
|
+
|
|
120
|
+
$whereHas = collect($builder->calls)->where(0, 'whereHas')->firstWhere(1, 'driver');
|
|
121
|
+
expect($whereHas[2][0][1][0])->toBe('uuid');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('fuel report filter scopes by vehicle_uuid as well as vehicle', function () {
|
|
125
|
+
$builder = new FleetOpsFuelReportFilterUnitQuery();
|
|
126
|
+
$filter = fleetopsFuelReportFilterUnitFilter($builder);
|
|
127
|
+
|
|
128
|
+
$filter->vehicleUuid('44444444-4444-4444-8444-444444444444');
|
|
129
|
+
$filter->vehicleUuid('vehicle_filterone');
|
|
130
|
+
|
|
131
|
+
$whereHasCalls = collect($builder->calls)->where(0, 'whereHas')->where(1, 'vehicle')->values();
|
|
132
|
+
expect($whereHasCalls)->toHaveCount(2)
|
|
133
|
+
->and($whereHasCalls[0][2][0][1][0])->toBe('uuid')
|
|
134
|
+
->and($whereHasCalls[1][2][0][1][0])->toBe('public_id');
|
|
135
|
+
});
|
|
@@ -127,3 +127,46 @@ test('issue filter relation branches route uuids public ids and searches', funct
|
|
|
127
127
|
$searchNested = $whereHasCalls->filter(fn ($call) => collect($call[2])->contains(fn ($inner) => $inner[0] === 'search'));
|
|
128
128
|
expect($searchNested->count())->toBeGreaterThanOrEqual(3);
|
|
129
129
|
});
|
|
130
|
+
|
|
131
|
+
test('issue filter scopes by driver_uuid and driver_assigned, not just driver', function () {
|
|
132
|
+
/*
|
|
133
|
+
* The base filter resolves a query parameter to a method of the same name
|
|
134
|
+
* and silently ignores what it cannot match. `driver_uuid` matched nothing,
|
|
135
|
+
* so the filter was dropped and the list came back scoped only by company —
|
|
136
|
+
* every driver's issues, with a 200 and no sign the request had been
|
|
137
|
+
* narrowed. For a driver app that is a disclosure, not a nuisance.
|
|
138
|
+
*/
|
|
139
|
+
foreach (['driverUuid', 'driverAssigned'] as $method) {
|
|
140
|
+
$builder = new FleetOpsIssueFilterUnitQuery();
|
|
141
|
+
$filter = fleetopsIssueFilterUnitFilter($builder);
|
|
142
|
+
|
|
143
|
+
$filter->{$method}('driver_filterone');
|
|
144
|
+
|
|
145
|
+
$whereHas = collect($builder->calls)->where(0, 'whereHas')->firstWhere(1, 'driver');
|
|
146
|
+
expect($whereHas)->not->toBeNull("{$method} must constrain the driver relation")
|
|
147
|
+
->and($whereHas[2][0][1][0])->toBe('public_id');
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('issue filter driver aliases route uuids the same way driver does', function () {
|
|
152
|
+
$builder = new FleetOpsIssueFilterUnitQuery();
|
|
153
|
+
$filter = fleetopsIssueFilterUnitFilter($builder);
|
|
154
|
+
|
|
155
|
+
$filter->driverUuid('33333333-3333-4333-8333-333333333333');
|
|
156
|
+
|
|
157
|
+
$whereHas = collect($builder->calls)->where(0, 'whereHas')->firstWhere(1, 'driver');
|
|
158
|
+
expect($whereHas[2][0][1][0])->toBe('uuid');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('issue filter scopes by vehicle_uuid as well as vehicle', function () {
|
|
162
|
+
$builder = new FleetOpsIssueFilterUnitQuery();
|
|
163
|
+
$filter = fleetopsIssueFilterUnitFilter($builder);
|
|
164
|
+
|
|
165
|
+
$filter->vehicleUuid('44444444-4444-4444-8444-444444444444');
|
|
166
|
+
$filter->vehicleUuid('vehicle_filterone');
|
|
167
|
+
|
|
168
|
+
$whereHasCalls = collect($builder->calls)->where(0, 'whereHas')->where(1, 'vehicle')->values();
|
|
169
|
+
expect($whereHasCalls)->toHaveCount(2)
|
|
170
|
+
->and($whereHasCalls[0][2][0][1][0])->toBe('uuid')
|
|
171
|
+
->and($whereHasCalls[1][2][0][1][0])->toBe('public_id');
|
|
172
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* The models resolve their connection through a namespaced `config()`, which
|
|
5
|
+
* does not exist without the full application. Stubbed so a resource can be
|
|
6
|
+
* exercised on its own — the projection is the thing under test, not Eloquent.
|
|
7
|
+
*/
|
|
8
|
+
if (!function_exists('Fleetbase\Traits\config')) {
|
|
9
|
+
eval('namespace Fleetbase\Traits; function config($key = null, $default = null) { return $key === "api.cache.enabled" ? false : $default; }');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (!function_exists('Fleetbase\Models\config')) {
|
|
13
|
+
eval('namespace Fleetbase\Models; function config($key = null, $default = null) { return $default; }');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (!function_exists('Fleetbase\FleetOps\Models\config')) {
|
|
17
|
+
eval('namespace Fleetbase\FleetOps\Models; function config($key = null, $default = null) { return $default; }');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
use Fleetbase\FleetOps\Http\Resources\v1\Manifest as ManifestResource;
|
|
21
|
+
use Fleetbase\FleetOps\Http\Resources\v1\ManifestStop as ManifestStopResource;
|
|
22
|
+
use Fleetbase\FleetOps\Models\Manifest;
|
|
23
|
+
use Fleetbase\FleetOps\Models\ManifestStop;
|
|
24
|
+
use Illuminate\Http\Request;
|
|
25
|
+
|
|
26
|
+
if (!class_exists('FleetOpsSupportRequestState')) {
|
|
27
|
+
class FleetOpsSupportRequestState
|
|
28
|
+
{
|
|
29
|
+
public static ?Request $request = null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!function_exists('Fleetbase\Support\request')) {
|
|
34
|
+
eval('namespace Fleetbase\Support; function request($key = null, $default = null) { return \FleetOpsSupportRequestState::$request; }');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The manifest's stop counts are accessors that query the database, and the
|
|
39
|
+
* driver and vehicle names walk relations. None of that is what a resource test
|
|
40
|
+
* is for, so they are answered directly — the projection is under test, not
|
|
41
|
+
* Eloquent.
|
|
42
|
+
*/
|
|
43
|
+
class FleetOpsManifestFake extends Manifest
|
|
44
|
+
{
|
|
45
|
+
public function getCompletedStopsAttribute(): int
|
|
46
|
+
{
|
|
47
|
+
return 3;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public function getPendingStopsAttribute(): int
|
|
51
|
+
{
|
|
52
|
+
return 5;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public function getDriverNameAttribute(): ?string
|
|
56
|
+
{
|
|
57
|
+
return 'Ron';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public function getVehicleNameAttribute(): ?string
|
|
61
|
+
{
|
|
62
|
+
return 'EAS-01';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
test('manifest resource publishes what a driver needs to run a route', function () {
|
|
67
|
+
$request = Request::create('/v1/manifests/manifest_public', 'GET');
|
|
68
|
+
FleetOpsSupportRequestState::$request = $request;
|
|
69
|
+
|
|
70
|
+
$manifest = new FleetOpsManifestFake();
|
|
71
|
+
$manifest->setRawAttributes([
|
|
72
|
+
'id' => 3,
|
|
73
|
+
'uuid' => 'manifest-uuid',
|
|
74
|
+
'public_id' => 'manifest_public',
|
|
75
|
+
'status' => 'in_progress',
|
|
76
|
+
'total_distance_m' => 41200,
|
|
77
|
+
'total_duration_s' => 5400,
|
|
78
|
+
'stop_count' => 8,
|
|
79
|
+
'notes' => 'Cold chain first',
|
|
80
|
+
], true);
|
|
81
|
+
|
|
82
|
+
$payload = (new ManifestResource($manifest))->resolve($request);
|
|
83
|
+
|
|
84
|
+
expect($payload['id'])->toBe('manifest_public')
|
|
85
|
+
->and($payload['status'])->toBe('in_progress')
|
|
86
|
+
->and($payload['total_distance_m'])->toBe(41200)
|
|
87
|
+
->and($payload['stop_count'])->toBe(8)
|
|
88
|
+
->and($payload['notes'])->toBe('Cold chain first')
|
|
89
|
+
->and($payload['completed_stops'])->toBe(3)
|
|
90
|
+
->and($payload['pending_stops'])->toBe(5)
|
|
91
|
+
->and($payload['driver_name'])->toBe('Ron');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('manifest resource omits stops when they were not loaded, so a list stays a list', function () {
|
|
95
|
+
// A driver's manifest list on a busy fleet must not drag every stop of
|
|
96
|
+
// every route along with it.
|
|
97
|
+
$request = Request::create('/v1/drivers/driver_public/manifests', 'GET');
|
|
98
|
+
FleetOpsSupportRequestState::$request = $request;
|
|
99
|
+
|
|
100
|
+
$manifest = new FleetOpsManifestFake();
|
|
101
|
+
$manifest->setRawAttributes(['id' => 4, 'uuid' => 'm-uuid', 'public_id' => 'manifest_public', 'status' => 'pending'], true);
|
|
102
|
+
|
|
103
|
+
$payload = (new ManifestResource($manifest))->resolve($request);
|
|
104
|
+
|
|
105
|
+
expect($payload)->not->toHaveKey('stops');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('manifest stop resource carries the sequence a re-sequence rewrites', function () {
|
|
109
|
+
$request = Request::create('/v1/manifest-stops/stop_public', 'GET');
|
|
110
|
+
FleetOpsSupportRequestState::$request = $request;
|
|
111
|
+
|
|
112
|
+
$stop = new ManifestStop();
|
|
113
|
+
$stop->setRawAttributes([
|
|
114
|
+
'id' => 9,
|
|
115
|
+
'uuid' => 'stop-uuid',
|
|
116
|
+
'public_id' => 'stop_public',
|
|
117
|
+
'status' => 'pending',
|
|
118
|
+
'sequence' => 4,
|
|
119
|
+
'distance_from_prev_m' => 1800,
|
|
120
|
+
'duration_from_prev_s' => 240,
|
|
121
|
+
], true);
|
|
122
|
+
|
|
123
|
+
$payload = (new ManifestStopResource($stop))->resolve($request);
|
|
124
|
+
|
|
125
|
+
expect($payload['id'])->toBe('stop_public')
|
|
126
|
+
->and($payload['sequence'])->toBe(4)
|
|
127
|
+
->and($payload['status'])->toBe('pending')
|
|
128
|
+
->and($payload['distance_from_prev_m'])->toBe(1800);
|
|
129
|
+
});
|
|
@@ -45,3 +45,94 @@ test('order config resource projects empty and invalid flow as an empty list', f
|
|
|
45
45
|
expect($payload['id'])->toBe('order_config_public')
|
|
46
46
|
->and($payload['flow'])->toBe([]);
|
|
47
47
|
});
|
|
48
|
+
|
|
49
|
+
test('order config resource publishes the flow graph so consumers can sequence it', function () {
|
|
50
|
+
$request = Request::create('/v1/fleet-ops/order-configs/order_config_public', 'GET');
|
|
51
|
+
FleetOpsOrderConfigResourceRequestState::$request = $request;
|
|
52
|
+
FleetOpsSupportRequestState::$request = $request;
|
|
53
|
+
|
|
54
|
+
// Declared deliberately out of workflow order, which is how flows are
|
|
55
|
+
// stored: `completed` before the step that leads to it, `dispatched` last.
|
|
56
|
+
$config = new OrderConfig();
|
|
57
|
+
$config->setRawAttributes([
|
|
58
|
+
'id' => 8,
|
|
59
|
+
'uuid' => 'order-config-uuid',
|
|
60
|
+
'public_id' => 'order_config_public',
|
|
61
|
+
'key' => 'transport',
|
|
62
|
+
'name' => 'Transport',
|
|
63
|
+
'flow' => [
|
|
64
|
+
['code' => 'completed', 'status' => 'Order Completed', 'complete' => true, 'activities' => []],
|
|
65
|
+
['code' => 'created', 'status' => 'Order Created', 'activities' => ['dispatched']],
|
|
66
|
+
['code' => 'dispatched', 'status' => 'Order Dispatched', 'activities' => ['enroute'], 'sequence' => 2],
|
|
67
|
+
[
|
|
68
|
+
'code' => 'enroute',
|
|
69
|
+
'status' => 'Driver Enroute',
|
|
70
|
+
'activities' => ['completed', 'failed'],
|
|
71
|
+
'logic' => [['type' => 'and', 'conditions' => []]],
|
|
72
|
+
],
|
|
73
|
+
],
|
|
74
|
+
], true);
|
|
75
|
+
|
|
76
|
+
$flow = (new OrderConfigResource($config))->resolve($request)['flow'];
|
|
77
|
+
$byCode = collect($flow)->keyBy('code');
|
|
78
|
+
|
|
79
|
+
expect($byCode['created']['activities'])->toBe(['dispatched'])
|
|
80
|
+
->and($byCode['dispatched']['sequence'])->toBe(2)
|
|
81
|
+
->and($byCode['enroute']['activities'])->toBe(['completed', 'failed'])
|
|
82
|
+
->and($byCode['enroute']['logic'])->toBe([['type' => 'and', 'conditions' => []]])
|
|
83
|
+
->and($byCode['completed']['complete'])->toBeTrue();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('order config resource normalises transitions written as objects', function () {
|
|
87
|
+
$request = Request::create('/v1/fleet-ops/order-configs/order_config_public', 'GET');
|
|
88
|
+
FleetOpsOrderConfigResourceRequestState::$request = $request;
|
|
89
|
+
FleetOpsSupportRequestState::$request = $request;
|
|
90
|
+
|
|
91
|
+
// Flows have been authored both ways; a consumer should not have to guess.
|
|
92
|
+
$config = new OrderConfig();
|
|
93
|
+
$config->setRawAttributes([
|
|
94
|
+
'id' => 9,
|
|
95
|
+
'uuid' => 'order-config-uuid',
|
|
96
|
+
'public_id' => 'order_config_public',
|
|
97
|
+
'key' => 'transport',
|
|
98
|
+
'name' => 'Transport',
|
|
99
|
+
'flow' => [
|
|
100
|
+
['code' => 'created', 'activities' => [['code' => 'dispatched'], 'enroute', ['label' => 'no code here']]],
|
|
101
|
+
// A flow hand-edited down to a single transition can leave a bare
|
|
102
|
+
// string where the list belongs. Publish an empty list rather than
|
|
103
|
+
// letting the shape vary — a consumer iterating the field should
|
|
104
|
+
// never have to type-check it.
|
|
105
|
+
['code' => 'dispatched', 'activities' => 'enroute'],
|
|
106
|
+
],
|
|
107
|
+
], true);
|
|
108
|
+
|
|
109
|
+
$flow = (new OrderConfigResource($config))->resolve($request)['flow'];
|
|
110
|
+
|
|
111
|
+
expect($flow[0]['activities'])->toBe(['dispatched', 'enroute'])
|
|
112
|
+
->and($flow[1]['activities'])->toBe([]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('order config resource keeps flow fields absent from a legacy activity null rather than missing', function () {
|
|
116
|
+
$request = Request::create('/v1/fleet-ops/order-configs/order_config_public', 'GET');
|
|
117
|
+
FleetOpsOrderConfigResourceRequestState::$request = $request;
|
|
118
|
+
FleetOpsSupportRequestState::$request = $request;
|
|
119
|
+
|
|
120
|
+
$config = new OrderConfig();
|
|
121
|
+
$config->setRawAttributes([
|
|
122
|
+
'id' => 10,
|
|
123
|
+
'uuid' => 'order-config-uuid',
|
|
124
|
+
'public_id' => 'order_config_public',
|
|
125
|
+
'key' => 'transport',
|
|
126
|
+
'name' => 'Transport',
|
|
127
|
+
'flow' => [['code' => 'created', 'status' => 'Order Created']],
|
|
128
|
+
], true);
|
|
129
|
+
|
|
130
|
+
$flow = (new OrderConfigResource($config))->resolve($request)['flow'];
|
|
131
|
+
|
|
132
|
+
// Present-and-null is a contract a client can read; absent is one it has to
|
|
133
|
+
// feel for.
|
|
134
|
+
expect($flow[0])->toHaveKeys(['sequence', 'activities', 'logic'])
|
|
135
|
+
->and($flow[0]['sequence'])->toBeNull()
|
|
136
|
+
->and($flow[0]['activities'])->toBe([])
|
|
137
|
+
->and($flow[0]['logic'])->toBeNull();
|
|
138
|
+
});
|
package/translations/en-us.yaml
CHANGED
|
@@ -2006,6 +2006,10 @@ settings:
|
|
|
2006
2006
|
map-provider: Map Provider
|
|
2007
2007
|
map-provider-help-text: Choose the map engine used to render the live operations map. Leaflet uses OpenStreetMap tiles and requires no API key. Google Maps requires a valid Google Maps JavaScript API key.
|
|
2008
2008
|
select-map-provider: Select map provider
|
|
2009
|
+
leaflet-tile-url: Leaflet Tile Provider URL
|
|
2010
|
+
leaflet-tile-url-help-text: Optional custom XYZ tile provider URL template used by the Leaflet map. Leave blank to use the default OpenStreetMap tiles which require no API key. Use this to point Leaflet at your own tile provider, including keyed providers such as CARTO.
|
|
2011
|
+
leaflet-dark-tile-url: Leaflet Dark Mode Tile Provider URL
|
|
2012
|
+
leaflet-dark-tile-url-help-text: Optional tile provider URL used when the console is in dark mode. Leave blank to fall back to the tile provider URL above, or the default OpenStreetMap tiles.
|
|
2009
2013
|
google-maps-map-type: Map Type
|
|
2010
2014
|
google-maps-map-type-help-text: The default map style to display when Google Maps is active.
|
|
2011
2015
|
select-map-type: Select map type
|