@fleetbase/fleetops-engine 0.6.70 → 0.6.71
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 +21 -47
- package/addon/components/order/details/activity.hbs +8 -6
- package/addon/components/order/details/activity.js +9 -0
- package/addon/components/order/details.hbs +29 -0
- package/addon/components/order/details.js +26 -0
- package/addon/components/order/form/details.hbs +30 -4
- package/addon/components/order/form/details.js +46 -0
- package/addon/components/order/form/route.hbs +4 -2
- package/addon/components/order/form/route.js +11 -0
- package/addon/components/order/form.hbs +12 -0
- package/addon/components/order/form.js +21 -1
- package/addon/components/order/kanban.js +72 -0
- package/addon/components/order-config-manager/activity-flow.js +45 -10
- package/addon/components/order-config-manager/lifecycle.hbs +250 -0
- package/addon/components/order-config-manager/lifecycle.js +358 -0
- package/addon/components/order-config-manager.js +5 -1
- package/addon/controllers/analytics/reports/index.js +10 -1
- package/addon/controllers/connectivity/devices/index/details.js +1 -0
- package/addon/controllers/connectivity/devices/index.js +8 -2
- package/addon/controllers/connectivity/events/index.js +31 -1
- package/addon/controllers/connectivity/sensors/index/details.js +1 -1
- package/addon/controllers/connectivity/sensors/index.js +37 -1
- package/addon/controllers/maintenance/equipment/index/details.js +2 -2
- package/addon/controllers/maintenance/equipment/index.js +28 -9
- package/addon/controllers/maintenance/inspection-forms/index.js +32 -12
- package/addon/controllers/maintenance/inspection-submissions/index/details.js +2 -1
- package/addon/controllers/maintenance/inspection-submissions/index.js +49 -9
- package/addon/controllers/maintenance/maintenances/index/details.js +2 -0
- package/addon/controllers/maintenance/maintenances/index.js +20 -1
- package/addon/controllers/maintenance/parts/index/details.js +2 -2
- package/addon/controllers/maintenance/parts/index.js +28 -8
- package/addon/controllers/maintenance/schedules/index.js +33 -4
- package/addon/controllers/maintenance/work-orders/index/details.js +3 -3
- package/addon/controllers/maintenance/work-orders/index.js +39 -8
- package/addon/controllers/management/contacts/customers.js +13 -0
- package/addon/controllers/management/contacts/index.js +43 -1
- package/addon/controllers/management/drivers/index/details.js +1 -0
- package/addon/controllers/management/drivers/index.js +32 -2
- package/addon/controllers/management/fleets/index/details.js +1 -0
- package/addon/controllers/management/fleets/index.js +39 -1
- package/addon/controllers/management/fuel-reports/index.js +22 -2
- package/addon/controllers/management/fuel-transactions/index.js +43 -7
- package/addon/controllers/management/issues/index/details.js +1 -0
- package/addon/controllers/management/issues/index.js +22 -2
- package/addon/controllers/management/places/index.js +36 -1
- package/addon/controllers/management/trailers/index/details.js +1 -0
- package/addon/controllers/management/trailers/index.js +50 -2
- package/addon/controllers/management/vehicles/index/details.js +1 -0
- package/addon/controllers/management/vehicles/index.js +33 -2
- package/addon/controllers/management/vendors/index.js +26 -2
- package/addon/controllers/operations/orders/index/details.js +31 -4
- package/addon/controllers/operations/orders/index.js +59 -4
- package/addon/controllers/operations/service-rates/index.js +15 -1
- package/addon/extension.js +32 -0
- package/addon/routes/operations/orders/index/details.js +1 -0
- package/addon/services/contact-actions.js +1 -0
- package/addon/services/customer-actions.js +2 -1
- package/addon/services/device-actions.js +1 -0
- package/addon/services/device-event-actions.js +1 -0
- package/addon/services/driver-actions.js +4 -13
- package/addon/services/equipment-actions.js +1 -0
- package/addon/services/fleet-actions.js +1 -0
- package/addon/services/fuel-report-actions.js +1 -0
- package/addon/services/inspection-form-actions.js +1 -0
- package/addon/services/inspection-submission-actions.js +1 -0
- package/addon/services/integrated-vendor-actions.js +1 -0
- package/addon/services/issue-actions.js +1 -0
- package/addon/services/maintenance-actions.js +1 -0
- package/addon/services/maintenance-schedule-actions.js +2 -1
- package/addon/services/order-actions.js +7 -0
- package/addon/services/order-presentation.js +129 -0
- package/addon/services/part-actions.js +1 -0
- package/addon/services/place-actions.js +1 -0
- package/addon/services/sensor-actions.js +1 -0
- package/addon/services/service-area-actions.js +1 -0
- package/addon/services/service-rate-actions.js +1 -0
- package/addon/services/telematic-actions.js +1 -0
- package/addon/services/trailer-actions.js +4 -14
- package/addon/services/vehicle-actions.js +4 -13
- package/addon/services/vendor-actions.js +1 -0
- package/addon/services/work-order-actions.js +1 -0
- package/addon/services/zone-actions.js +1 -0
- package/addon/templates/analytics/reports/index/details.hbs +1 -0
- package/addon/templates/analytics/reports/index.hbs +1 -0
- package/addon/templates/connectivity/devices/index/details.hbs +1 -0
- package/addon/templates/connectivity/devices/index.hbs +1 -0
- package/addon/templates/connectivity/events/index.hbs +1 -0
- package/addon/templates/connectivity/sensors/index/details.hbs +1 -0
- package/addon/templates/connectivity/sensors/index.hbs +1 -0
- package/addon/templates/maintenance/equipment/index/details.hbs +1 -0
- package/addon/templates/maintenance/equipment/index.hbs +2 -0
- package/addon/templates/maintenance/inspection-forms/index/details.hbs +1 -0
- package/addon/templates/maintenance/inspection-forms/index.hbs +1 -0
- package/addon/templates/maintenance/inspection-submissions/index/details.hbs +1 -0
- package/addon/templates/maintenance/inspection-submissions/index.hbs +1 -0
- package/addon/templates/maintenance/maintenances/index/details.hbs +1 -0
- package/addon/templates/maintenance/maintenances/index.hbs +1 -0
- package/addon/templates/maintenance/parts/index/details.hbs +1 -0
- package/addon/templates/maintenance/parts/index.hbs +2 -0
- package/addon/templates/maintenance/schedules/index/details.hbs +1 -0
- package/addon/templates/maintenance/schedules/index.hbs +2 -0
- package/addon/templates/maintenance/work-orders/index/details.hbs +1 -0
- package/addon/templates/maintenance/work-orders/index.hbs +1 -0
- package/addon/templates/management/contacts/customers/details.hbs +1 -0
- package/addon/templates/management/contacts/customers.hbs +1 -0
- package/addon/templates/management/contacts/index/details.hbs +1 -0
- package/addon/templates/management/contacts/index.hbs +1 -0
- package/addon/templates/management/contacts.hbs +1 -0
- package/addon/templates/management/drivers/index/details.hbs +1 -0
- package/addon/templates/management/drivers/index.hbs +2 -0
- package/addon/templates/management/fleets/index/details.hbs +1 -0
- package/addon/templates/management/fleets/index.hbs +1 -0
- package/addon/templates/management/fuel-reports/index/details.hbs +1 -0
- package/addon/templates/management/fuel-reports/index.hbs +1 -0
- package/addon/templates/management/fuel-transactions/index.hbs +1 -0
- package/addon/templates/management/issues/index/details.hbs +1 -0
- package/addon/templates/management/issues/index.hbs +1 -0
- package/addon/templates/management/places/index/details.hbs +1 -0
- package/addon/templates/management/places/index.hbs +1 -0
- package/addon/templates/management/trailers/index/details.hbs +1 -0
- package/addon/templates/management/trailers/index.hbs +2 -0
- package/addon/templates/management/vehicles/index/details.hbs +1 -0
- package/addon/templates/management/vehicles/index.hbs +2 -0
- package/addon/templates/management/vendors/index/details.hbs +1 -0
- package/addon/templates/management/vendors/index.hbs +2 -0
- package/addon/templates/management/vendors/integrated.hbs +1 -0
- package/addon/templates/operations/orders/index/details.hbs +1 -0
- package/addon/templates/operations/orders/index.hbs +1 -0
- package/addon/templates/operations/service-rates/index/details.hbs +1 -0
- package/addon/templates/operations/service-rates/index.hbs +1 -0
- package/addon/utils/normalize-order-config-flow.js +6 -1
- package/app/services/order-presentation.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +3 -3
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +34 -4
- package/server/src/Models/Order.php +35 -3
- package/server/src/Models/OrderConfig.php +129 -0
- package/server/src/Models/TrackingNumber.php +8 -4
- package/server/tests/Feature/Http/Internal/OrderControllerActivityFlowsTest.php +72 -0
- package/server/tests/Feature/Http/Internal/OrderControllerCreateRecordTest.php +26 -0
- package/server/tests/OrderConfigLifecycleTest.php +120 -0
- package/server/tests/Unit/Events/OrderDispatchFailedTest.php +2 -1
- package/translations/en-us.yaml +60 -0
|
@@ -380,6 +380,125 @@ class OrderConfig extends Model
|
|
|
380
380
|
return $this->activities()->firstWhere('code', $code);
|
|
381
381
|
}
|
|
382
382
|
|
|
383
|
+
/**
|
|
384
|
+
* Returns the configured lifecycle semantics stored under `meta.lifecycle`.
|
|
385
|
+
*
|
|
386
|
+
* A configured lifecycle is opt-in. When absent the default dispatch lifecycle
|
|
387
|
+
* (created -> dispatched -> ... -> completed / canceled) applies unchanged.
|
|
388
|
+
*
|
|
389
|
+
* Supported keys:
|
|
390
|
+
* - `initial` (string) activity code new orders start at
|
|
391
|
+
* - `completed` (string) activity code that completes an order
|
|
392
|
+
* - `canceled` (string) activity code used when an order is canceled
|
|
393
|
+
* - `terminal` (string[]) activity codes after which no transition is allowed
|
|
394
|
+
* - `dispatch` (bool) false disables dispatch for orders using this config
|
|
395
|
+
* - `strict_transitions` (bool) true only allows moving to a configured child activity
|
|
396
|
+
*/
|
|
397
|
+
public function lifecycle(): array
|
|
398
|
+
{
|
|
399
|
+
$lifecycle = data_get($this->meta, 'lifecycle');
|
|
400
|
+
|
|
401
|
+
return is_array($lifecycle) ? $lifecycle : [];
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Determines if this config defines its own lifecycle semantics.
|
|
406
|
+
*/
|
|
407
|
+
public function hasConfiguredLifecycle(): bool
|
|
408
|
+
{
|
|
409
|
+
return !empty($this->lifecycle());
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Returns the activity new orders using this config start at.
|
|
414
|
+
*
|
|
415
|
+
* Without a configured lifecycle this is the `created` activity, if present in the flow.
|
|
416
|
+
*/
|
|
417
|
+
public function getInitialActivity(): ?Activity
|
|
418
|
+
{
|
|
419
|
+
$initialCode = data_get($this->lifecycle(), 'initial');
|
|
420
|
+
if ($initialCode) {
|
|
421
|
+
return $this->getActivityByCode($initialCode);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return $this->getCreatedActivity();
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Returns the status code new orders using this config start at.
|
|
429
|
+
*/
|
|
430
|
+
public function getInitialStatusCode(): string
|
|
431
|
+
{
|
|
432
|
+
return data_get($this->lifecycle(), 'initial') ?: 'created';
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Determines if orders using this config may be dispatched.
|
|
437
|
+
*/
|
|
438
|
+
public function allowsDispatch(): bool
|
|
439
|
+
{
|
|
440
|
+
$dispatch = data_get($this->lifecycle(), 'dispatch', true);
|
|
441
|
+
|
|
442
|
+
return !in_array($dispatch, [false, 'false', 0, '0'], true);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Determines if activity updates must follow the configured flow graph.
|
|
447
|
+
*/
|
|
448
|
+
public function hasStrictTransitions(): bool
|
|
449
|
+
{
|
|
450
|
+
return (bool) data_get($this->lifecycle(), 'strict_transitions', false);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Returns the terminal activity codes of this config.
|
|
455
|
+
*/
|
|
456
|
+
public function getTerminalActivityCodes(): array
|
|
457
|
+
{
|
|
458
|
+
if (!$this->hasConfiguredLifecycle()) {
|
|
459
|
+
return ['completed', 'canceled'];
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
$terminal = data_get($this->lifecycle(), 'terminal', []);
|
|
463
|
+
$terminal = is_array($terminal) ? $terminal : [];
|
|
464
|
+
foreach (['completed', 'canceled'] as $semantic) {
|
|
465
|
+
$code = data_get($this->lifecycle(), $semantic);
|
|
466
|
+
if ($code) {
|
|
467
|
+
$terminal[] = $code;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return array_values(array_unique($terminal));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Determines if the given activity code is terminal for this config.
|
|
476
|
+
*/
|
|
477
|
+
public function isTerminalActivityCode(?string $code): bool
|
|
478
|
+
{
|
|
479
|
+
return $code !== null && in_array($code, $this->getTerminalActivityCodes(), true);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Determines if the configured flow permits moving from one activity code to another.
|
|
484
|
+
*
|
|
485
|
+
* Only the configured graph is consulted: the target must be listed as a child of the
|
|
486
|
+
* source activity, and terminal activities permit no further transition.
|
|
487
|
+
*/
|
|
488
|
+
public function canTransition(?string $fromCode, string $toCode): bool
|
|
489
|
+
{
|
|
490
|
+
if ($fromCode === null || $this->isTerminalActivityCode($fromCode)) {
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
$from = $this->getActivityByCode($fromCode);
|
|
495
|
+
if (!$from) {
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return $from->children()->contains(fn ($activity) => $activity->code === $toCode);
|
|
500
|
+
}
|
|
501
|
+
|
|
383
502
|
/**
|
|
384
503
|
* Creates an Activity instance representing a canceled order.
|
|
385
504
|
*
|
|
@@ -391,6 +510,11 @@ class OrderConfig extends Model
|
|
|
391
510
|
*/
|
|
392
511
|
public function getCanceledActivity()
|
|
393
512
|
{
|
|
513
|
+
$configuredCode = data_get($this->lifecycle(), 'canceled');
|
|
514
|
+
if ($configuredCode && ($configuredActivity = $this->getActivityByCode($configuredCode))) {
|
|
515
|
+
return $configuredActivity;
|
|
516
|
+
}
|
|
517
|
+
|
|
394
518
|
$canceledActivity = $this->activities()->firstWhere('code', 'canceled');
|
|
395
519
|
if ($canceledActivity) {
|
|
396
520
|
return $canceledActivity;
|
|
@@ -416,6 +540,11 @@ class OrderConfig extends Model
|
|
|
416
540
|
*/
|
|
417
541
|
public function getCompletedActivity()
|
|
418
542
|
{
|
|
543
|
+
$configuredCode = data_get($this->lifecycle(), 'completed');
|
|
544
|
+
if ($configuredCode && ($configuredActivity = $this->getActivityByCode($configuredCode))) {
|
|
545
|
+
return $configuredActivity;
|
|
546
|
+
}
|
|
547
|
+
|
|
419
548
|
$completedActivity = $this->activities()->firstWhere('code', 'completed');
|
|
420
549
|
if ($completedActivity) {
|
|
421
550
|
return $completedActivity;
|
|
@@ -291,13 +291,17 @@ class TrackingNumber extends Model
|
|
|
291
291
|
|
|
292
292
|
$ownerTypeName = class_basename($values['owner_type']);
|
|
293
293
|
|
|
294
|
+
// owners may define their own initial status (e.g. orders with a configured lifecycle)
|
|
295
|
+
$initialStatus = $owner && method_exists($owner, 'getInitialTrackingStatus') ? $owner->getInitialTrackingStatus() : null;
|
|
296
|
+
$initialCode = is_array($initialStatus) && !empty($initialStatus['code']) ? $initialStatus['code'] : null;
|
|
297
|
+
|
|
294
298
|
// create initial status
|
|
295
299
|
$trackingStatusId = static::createInitialTrackingStatus([
|
|
296
300
|
'tracking_number_uuid' => $uuid,
|
|
297
|
-
'status' => Str::title($ownerTypeName . ' created'),
|
|
298
|
-
'details' => 'New ' . Str::lower($ownerTypeName) . ' created.',
|
|
301
|
+
'status' => $initialCode ? $initialStatus['status'] : Str::title($ownerTypeName . ' created'),
|
|
302
|
+
'details' => $initialCode ? $initialStatus['details'] : 'New ' . Str::lower($ownerTypeName) . ' created.',
|
|
299
303
|
'location' => $values['location'] ?? Utils::parsePointToWkt(new Point(0, 0)),
|
|
300
|
-
'code' => 'CREATED',
|
|
304
|
+
'code' => $initialCode ? TrackingStatus::prepareCode($initialCode) : 'CREATED',
|
|
301
305
|
]);
|
|
302
306
|
|
|
303
307
|
// update status of tracking number
|
|
@@ -309,7 +313,7 @@ class TrackingNumber extends Model
|
|
|
309
313
|
// $model->update([ 'status' => 'created' ]);
|
|
310
314
|
|
|
311
315
|
// silent update
|
|
312
|
-
static::updateOwnerStatusColumn($owner, 'created');
|
|
316
|
+
static::updateOwnerStatusColumn($owner, $initialCode ?? 'created');
|
|
313
317
|
}
|
|
314
318
|
|
|
315
319
|
return $uuid;
|
|
@@ -661,3 +661,75 @@ test('set destination and next activity guard missing orders and configs', funct
|
|
|
661
661
|
$noConfig = $controller->nextActivity('order_internal', Request::create('/x', 'GET'));
|
|
662
662
|
expect($noConfig->getData(true)['error'] ?? '')->toBe('No order config found for order.');
|
|
663
663
|
});
|
|
664
|
+
|
|
665
|
+
function fleetopsInternalActivityUseLifecycle(SQLiteConnection $connection, array $lifecycle = []): void
|
|
666
|
+
{
|
|
667
|
+
$activity = fn (string $code, array $children = []) => ['key' => $code, 'code' => $code, 'status' => ucfirst($code), 'details' => ucfirst($code) . ' loan', 'activities' => $children, 'logic' => [], 'events' => [], 'complete' => $code === 'completed'];
|
|
668
|
+
|
|
669
|
+
$connection->table('order_configs')->where('uuid', 'config-1')->update([
|
|
670
|
+
'flow' => json_encode([
|
|
671
|
+
'requested' => $activity('requested', ['assigned', 'cancelled']),
|
|
672
|
+
'assigned' => $activity('assigned', ['completed']),
|
|
673
|
+
'completed' => $activity('completed'),
|
|
674
|
+
'cancelled' => $activity('cancelled'),
|
|
675
|
+
]),
|
|
676
|
+
'meta' => json_encode(['lifecycle' => array_replace([
|
|
677
|
+
'initial' => 'requested',
|
|
678
|
+
'completed' => 'completed',
|
|
679
|
+
'canceled' => 'cancelled',
|
|
680
|
+
'dispatch' => false,
|
|
681
|
+
'strict_transitions' => true,
|
|
682
|
+
], $lifecycle)]),
|
|
683
|
+
]);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
test('strict transitions reject activities outside the configured flow and use the configured definition', function () {
|
|
687
|
+
$connection = fleetopsInternalActivityBoot();
|
|
688
|
+
$controller = new OrderController();
|
|
689
|
+
|
|
690
|
+
fleetopsInternalActivityUseLifecycle($connection);
|
|
691
|
+
fleetopsInternalActivitySeedOrder($connection, ['status' => 'requested']);
|
|
692
|
+
|
|
693
|
+
// Not a child of the current activity
|
|
694
|
+
$skipped = $controller->updateActivity('order_internal', Request::create('/x', 'POST', ['activity' => ['key' => 'completed', 'code' => 'completed', 'status' => 'Completed'], 'bypass_proof' => 1]));
|
|
695
|
+
expect($skipped->getStatusCode())->toBe(422)
|
|
696
|
+
->and($skipped->getData(true)['error'])->toContain('not a permitted next step');
|
|
697
|
+
|
|
698
|
+
// Not in the flow at all
|
|
699
|
+
$unknown = $controller->updateActivity('order_internal', Request::create('/x', 'POST', ['activity' => ['key' => 'started', 'code' => 'started', 'status' => 'Started'], 'bypass_proof' => 1]));
|
|
700
|
+
expect($unknown->getStatusCode())->toBe(422);
|
|
701
|
+
|
|
702
|
+
// A configured child is accepted, with the server-side definition replacing the client's
|
|
703
|
+
$assigned = $controller->updateActivity('order_internal', Request::create('/x', 'POST', ['activity' => ['key' => 'assigned', 'code' => 'assigned', 'status' => 'Tampered'], 'bypass_proof' => 1]));
|
|
704
|
+
expect($assigned)->toBeInstanceOf(Fleetbase\FleetOps\Http\Resources\v1\Order::class)
|
|
705
|
+
->and($connection->table('tracking_statuses')->where('code', 'ASSIGNED')->value('status'))->toBe('Assigned');
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
test('dispatch order refuses orders whose lifecycle disables dispatch', function () {
|
|
709
|
+
$connection = fleetopsInternalActivityBoot();
|
|
710
|
+
$controller = new OrderController();
|
|
711
|
+
|
|
712
|
+
fleetopsInternalActivityUseLifecycle($connection);
|
|
713
|
+
fleetopsInternalActivitySeedOrder($connection, ['status' => 'requested', 'driver_assigned_uuid' => 'driver-1']);
|
|
714
|
+
|
|
715
|
+
$response = $controller->dispatchOrder(Request::create('/x', 'POST', ['order' => 'order_internal']));
|
|
716
|
+
expect($response->getData(true)['error'] ?? '')->toContain('not dispatched')
|
|
717
|
+
->and($connection->table('orders')->value('dispatched'))->toBe(0);
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
test('orders with a configured lifecycle start their tracking at the initial activity', function () {
|
|
721
|
+
$connection = fleetopsInternalActivityBoot();
|
|
722
|
+
|
|
723
|
+
// No config, or a config without a lifecycle, keeps the default created status
|
|
724
|
+
expect((new Fleetbase\FleetOps\Models\Order())->getInitialTrackingStatus())->toBeNull();
|
|
725
|
+
$order = new Fleetbase\FleetOps\Models\Order();
|
|
726
|
+
$order->order_config_uuid = 'config-1';
|
|
727
|
+
expect($order->getInitialTrackingStatus())->toBeNull();
|
|
728
|
+
|
|
729
|
+
fleetopsInternalActivityUseLifecycle($connection);
|
|
730
|
+
expect($order->getInitialTrackingStatus())->toBe(['code' => 'requested', 'status' => 'Requested', 'details' => 'Requested loan']);
|
|
731
|
+
|
|
732
|
+
// An initial activity missing from the flow falls back to the default
|
|
733
|
+
fleetopsInternalActivityUseLifecycle($connection, ['initial' => 'nowhere']);
|
|
734
|
+
expect($order->getInitialTrackingStatus())->toBeNull();
|
|
735
|
+
});
|
|
@@ -486,3 +486,29 @@ test('create record dispatches orders when the dispatch flag is not disabled', f
|
|
|
486
486
|
expect($result)->toBeArray()->toHaveKey('order')
|
|
487
487
|
->and($connection->table('orders')->value('dispatched'))->not->toBeNull();
|
|
488
488
|
});
|
|
489
|
+
|
|
490
|
+
test('create record starts configured lifecycles at their initial activity and never dispatches them', function () {
|
|
491
|
+
$connection = fleetopsInternalOrderCreateBoot();
|
|
492
|
+
$connection->table('order_configs')->where('uuid', 'config-1')->update([
|
|
493
|
+
'flow' => json_encode([
|
|
494
|
+
'requested' => ['key' => 'requested', 'code' => 'requested', 'status' => 'Requested', 'details' => 'Loan requested', 'activities' => ['completed']],
|
|
495
|
+
'completed' => ['key' => 'completed', 'code' => 'completed', 'status' => 'Completed', 'details' => 'Loan completed', 'activities' => []],
|
|
496
|
+
]),
|
|
497
|
+
'meta' => json_encode(['lifecycle' => ['initial' => 'requested', 'completed' => 'completed', 'dispatch' => false]]),
|
|
498
|
+
]);
|
|
499
|
+
|
|
500
|
+
// Asks to be dispatched; the lifecycle overrides both the status and the dispatch flag
|
|
501
|
+
$result = (new OrderController())->createRecord(Request::create('/int/v1/orders', 'POST', [
|
|
502
|
+
'order' => [
|
|
503
|
+
'status' => 'created',
|
|
504
|
+
'dispatched' => true,
|
|
505
|
+
'adhoc' => true,
|
|
506
|
+
'payload' => ['type' => 'transport'],
|
|
507
|
+
],
|
|
508
|
+
]));
|
|
509
|
+
|
|
510
|
+
expect($result)->toBeArray()->toHaveKey('order')
|
|
511
|
+
->and($connection->table('orders')->value('status'))->toBe('requested')
|
|
512
|
+
->and((bool) $connection->table('orders')->value('dispatched'))->toBeFalse()
|
|
513
|
+
->and((bool) $connection->table('orders')->value('adhoc'))->toBeFalse();
|
|
514
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Fleetbase\FleetOps\Models\OrderConfig;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Opt-in configured lifecycles (`meta.lifecycle`): a config can define its own initial, completed,
|
|
7
|
+
* canceled and terminal activities, disable dispatch and require configured transitions, without
|
|
8
|
+
* changing the default dispatch lifecycle of configs that do not opt in.
|
|
9
|
+
*/
|
|
10
|
+
function lifecycleActivity(string $code, array $children = [], bool $complete = false): array
|
|
11
|
+
{
|
|
12
|
+
return ['key' => $code, 'code' => $code, 'status' => ucfirst(str_replace('_', ' ', $code)), 'details' => '', 'activities' => $children, 'logic' => [], 'events' => [], 'complete' => $complete];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function customLifecycleConfig(array $lifecycle = []): OrderConfig
|
|
16
|
+
{
|
|
17
|
+
$config = new OrderConfig();
|
|
18
|
+
$config->setRawAttributes([
|
|
19
|
+
'flow' => json_encode([
|
|
20
|
+
'requested' => lifecycleActivity('requested', ['assigned', 'cancelled']),
|
|
21
|
+
'assigned' => lifecycleActivity('assigned', ['handed_over', 'cancelled']),
|
|
22
|
+
'handed_over' => lifecycleActivity('handed_over', ['active']),
|
|
23
|
+
'active' => lifecycleActivity('active', ['extended', 'returned']),
|
|
24
|
+
'extended' => lifecycleActivity('extended', ['active']),
|
|
25
|
+
'returned' => lifecycleActivity('returned', ['completed']),
|
|
26
|
+
'completed' => lifecycleActivity('completed', [], true),
|
|
27
|
+
'cancelled' => lifecycleActivity('cancelled'),
|
|
28
|
+
]),
|
|
29
|
+
'meta' => json_encode(['lifecycle' => array_replace([
|
|
30
|
+
'initial' => 'requested',
|
|
31
|
+
'completed' => 'completed',
|
|
32
|
+
'canceled' => 'cancelled',
|
|
33
|
+
'terminal' => ['completed', 'cancelled'],
|
|
34
|
+
'dispatch' => false,
|
|
35
|
+
'strict_transitions' => true,
|
|
36
|
+
], $lifecycle)]),
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
return $config;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function defaultLifecycleConfig(): OrderConfig
|
|
43
|
+
{
|
|
44
|
+
$config = new OrderConfig();
|
|
45
|
+
$config->setRawAttributes([
|
|
46
|
+
'flow' => json_encode([
|
|
47
|
+
'created' => lifecycleActivity('created', ['dispatched']),
|
|
48
|
+
'dispatched' => lifecycleActivity('dispatched', ['started']),
|
|
49
|
+
'started' => lifecycleActivity('started', ['completed']),
|
|
50
|
+
'completed' => lifecycleActivity('completed', [], true),
|
|
51
|
+
]),
|
|
52
|
+
'meta' => json_encode([]),
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
return $config;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
test('configs without meta.lifecycle keep the default dispatch semantics', function () {
|
|
59
|
+
$config = defaultLifecycleConfig();
|
|
60
|
+
|
|
61
|
+
expect($config->hasConfiguredLifecycle())->toBeFalse()
|
|
62
|
+
->and($config->getInitialStatusCode())->toBe('created')
|
|
63
|
+
->and($config->allowsDispatch())->toBeTrue()
|
|
64
|
+
->and($config->hasStrictTransitions())->toBeFalse()
|
|
65
|
+
->and($config->getTerminalActivityCodes())->toBe(['completed', 'canceled'])
|
|
66
|
+
->and($config->getCanceledActivity()->code)->toBe('canceled')
|
|
67
|
+
->and($config->getInitialActivity()->code)->toBe('created');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('a configured lifecycle names its own initial, cancel and completion activities', function () {
|
|
71
|
+
$config = customLifecycleConfig();
|
|
72
|
+
|
|
73
|
+
expect($config->hasConfiguredLifecycle())->toBeTrue()
|
|
74
|
+
->and($config->getInitialStatusCode())->toBe('requested')
|
|
75
|
+
->and($config->getInitialActivity()->code)->toBe('requested')
|
|
76
|
+
->and($config->getCanceledActivity()->code)->toBe('cancelled')
|
|
77
|
+
->and($config->getCompletedActivity()->code)->toBe('completed')
|
|
78
|
+
->and($config->allowsDispatch())->toBeFalse()
|
|
79
|
+
->and($config->hasStrictTransitions())->toBeTrue()
|
|
80
|
+
->and($config->isTerminalActivityCode('cancelled'))->toBeTrue()
|
|
81
|
+
->and($config->isTerminalActivityCode('canceled'))->toBeFalse();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('transitions follow only the configured graph', function () {
|
|
85
|
+
$config = customLifecycleConfig();
|
|
86
|
+
|
|
87
|
+
expect($config->canTransition('requested', 'assigned'))->toBeTrue()
|
|
88
|
+
->and($config->canTransition('handed_over', 'active'))->toBeTrue()
|
|
89
|
+
->and($config->canTransition('active', 'extended'))->toBeTrue()
|
|
90
|
+
->and($config->canTransition('extended', 'active'))->toBeTrue()
|
|
91
|
+
->and($config->canTransition('returned', 'completed'))->toBeTrue()
|
|
92
|
+
// skipped or foreign steps
|
|
93
|
+
->and($config->canTransition('requested', 'handed_over'))->toBeFalse()
|
|
94
|
+
->and($config->canTransition('requested', 'dispatched'))->toBeFalse()
|
|
95
|
+
->and($config->canTransition('active', 'cancelled'))->toBeFalse()
|
|
96
|
+
// terminal and unknown sources
|
|
97
|
+
->and($config->canTransition('completed', 'requested'))->toBeFalse()
|
|
98
|
+
->and($config->canTransition('cancelled', 'assigned'))->toBeFalse()
|
|
99
|
+
->and($config->canTransition(null, 'requested'))->toBeFalse()
|
|
100
|
+
->and($config->canTransition('nope', 'assigned'))->toBeFalse();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('a configuration-only change (added activity) is honoured without code changes', function () {
|
|
104
|
+
$config = customLifecycleConfig();
|
|
105
|
+
$flow = json_decode($config->getAttributes()['flow'], true);
|
|
106
|
+
$flow['active']['activities'][] = 'inspection';
|
|
107
|
+
$flow['inspection'] = lifecycleActivity('inspection', ['returned']);
|
|
108
|
+
$config->setRawAttributes(['flow' => json_encode($flow), 'meta' => $config->getAttributes()['meta']]);
|
|
109
|
+
|
|
110
|
+
expect($config->canTransition('active', 'inspection'))->toBeTrue()
|
|
111
|
+
->and($config->canTransition('inspection', 'returned'))->toBeTrue()
|
|
112
|
+
->and($config->canTransition('inspection', 'completed'))->toBeFalse();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('dispatch can be disabled with string or numeric falsy values', function () {
|
|
116
|
+
foreach ([false, 'false', 0, '0'] as $value) {
|
|
117
|
+
expect(customLifecycleConfig(['dispatch' => $value])->allowsDispatch())->toBeFalse();
|
|
118
|
+
}
|
|
119
|
+
expect(customLifecycleConfig(['dispatch' => true])->allowsDispatch())->toBeTrue();
|
|
120
|
+
});
|
|
@@ -6,7 +6,8 @@ use Illuminate\Support\Str;
|
|
|
6
6
|
|
|
7
7
|
class FleetOpsOrderDispatchFailedUnitProbe extends OrderDispatchFailed
|
|
8
8
|
{
|
|
9
|
-
|
|
9
|
+
// core-api 1.6.68 adds an optional $channel parameter; the literal keeps this probe loadable on earlier versions too.
|
|
10
|
+
public function getEventData(string $channel = 'webhook'): array
|
|
10
11
|
{
|
|
11
12
|
return [];
|
|
12
13
|
}
|
package/translations/en-us.yaml
CHANGED
|
@@ -1256,6 +1256,7 @@ order-config-manager:
|
|
|
1256
1256
|
details: Details
|
|
1257
1257
|
custom-fields: Custom Fields
|
|
1258
1258
|
activity-flow: Activity Flow
|
|
1259
|
+
lifecycle: Lifecycle
|
|
1259
1260
|
entities: Entities
|
|
1260
1261
|
details:
|
|
1261
1262
|
details: Details
|
|
@@ -1272,6 +1273,65 @@ order-config-manager:
|
|
|
1272
1273
|
delete-title: Delete this order config?
|
|
1273
1274
|
delete-body-message: Once this order config is deleted it will not be recoverable and you will lose all configurations.
|
|
1274
1275
|
confirm-delete: Delete
|
|
1276
|
+
lifecycle:
|
|
1277
|
+
title: Lifecycle
|
|
1278
|
+
description: A configured lifecycle lets this order type start, complete and cancel at its own activities instead of the default dispatch lifecycle (created, dispatched, started, completed, canceled).
|
|
1279
|
+
default-lifecycle: This order type uses the default dispatch lifecycle. Orders start at "created", can be dispatched to drivers and complete or cancel through the standard activities.
|
|
1280
|
+
enable: Use a configured lifecycle
|
|
1281
|
+
enable-help: Orders of this type start at the initial activity you choose and follow the activity flow instead of the dispatch lifecycle.
|
|
1282
|
+
initial: Initial activity
|
|
1283
|
+
initial-help: New orders of this type start here. Must be an activity in the flow.
|
|
1284
|
+
completed: Completed activity
|
|
1285
|
+
completed-help: Reaching this activity completes the order.
|
|
1286
|
+
canceled: Canceled activity
|
|
1287
|
+
canceled-help: Canceling an order moves it to this activity.
|
|
1288
|
+
terminal: Terminal activities
|
|
1289
|
+
terminal-help: No further transition is allowed after these. The completed and canceled activities are always terminal.
|
|
1290
|
+
dispatch: Allow dispatch
|
|
1291
|
+
dispatch-help: When off, orders of this type are never dispatched to drivers and the dispatch actions are refused.
|
|
1292
|
+
strict-transitions: Strict transitions
|
|
1293
|
+
strict-transitions-help: When on, an activity can only move to one of its configured next activities in the flow.
|
|
1294
|
+
select-activity: Select activity
|
|
1295
|
+
select-activities: Select activities
|
|
1296
|
+
problems-title: Fix before saving
|
|
1297
|
+
problem-initial-required: Choose the initial activity.
|
|
1298
|
+
problem-no-activities: Add at least one activity.
|
|
1299
|
+
problem-duplicate-code: 'An activity with the code "{code}" already exists.'
|
|
1300
|
+
activities: Activities
|
|
1301
|
+
activities-help: The statuses an order of this type can be in. Each activity lists the activities it can move to; the graph on the Activity Flow tab shows the same flow. Changes here are saved with the lifecycle.
|
|
1302
|
+
activity-label: Label
|
|
1303
|
+
activity-label-placeholder: e.g. Vehicle Assigned
|
|
1304
|
+
activity-code: Code
|
|
1305
|
+
activity-code-help: Stored status value. Derived from the label when left empty; cannot change once added.
|
|
1306
|
+
activity-code-placeholder: e.g. vehicle_assigned
|
|
1307
|
+
activity-details: Details
|
|
1308
|
+
activity-details-placeholder: Shown with the activity in the timeline
|
|
1309
|
+
activity-color: Colour
|
|
1310
|
+
activity-next: Can move to
|
|
1311
|
+
add-activity: Add activity
|
|
1312
|
+
remove-activity: Remove activity
|
|
1313
|
+
no-activities: No activities yet. Add the first one below; it becomes the initial activity.
|
|
1314
|
+
blank-flow: Start from a blank flow
|
|
1315
|
+
blank-flow-title: Remove all activities?
|
|
1316
|
+
blank-flow-body: The default dispatch activities (created, dispatched, started, …) are removed so you can define your own. Nothing is saved until you click Save changes.
|
|
1317
|
+
blank-flow-confirm: Remove activities
|
|
1318
|
+
initial-short: initial
|
|
1319
|
+
problem-missing-activity: '"{code}" is not an activity in the flow.'
|
|
1320
|
+
summary: Summary
|
|
1321
|
+
summary-initial: Orders start at
|
|
1322
|
+
summary-completed: Completed at
|
|
1323
|
+
summary-canceled: Canceled at
|
|
1324
|
+
summary-terminal: Terminal
|
|
1325
|
+
summary-dispatch: Dispatch
|
|
1326
|
+
summary-strict: Strict transitions
|
|
1327
|
+
'yes': 'Yes'
|
|
1328
|
+
'no': 'No'
|
|
1329
|
+
none: None
|
|
1330
|
+
remove: Remove configured lifecycle
|
|
1331
|
+
remove-title: Remove the configured lifecycle?
|
|
1332
|
+
remove-body: Orders of this type will use the default dispatch lifecycle again. Existing orders keep their current status.
|
|
1333
|
+
remove-confirm: Remove
|
|
1334
|
+
saved: Lifecycle saved for {orderConfigName}.
|
|
1275
1335
|
activity-flow:
|
|
1276
1336
|
save-activity-flow: Save Activity Flow
|
|
1277
1337
|
edit-activity-unique-code-warning: Activity code must be unique!
|