@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.
Files changed (144) hide show
  1. package/RELEASE.md +21 -47
  2. package/addon/components/order/details/activity.hbs +8 -6
  3. package/addon/components/order/details/activity.js +9 -0
  4. package/addon/components/order/details.hbs +29 -0
  5. package/addon/components/order/details.js +26 -0
  6. package/addon/components/order/form/details.hbs +30 -4
  7. package/addon/components/order/form/details.js +46 -0
  8. package/addon/components/order/form/route.hbs +4 -2
  9. package/addon/components/order/form/route.js +11 -0
  10. package/addon/components/order/form.hbs +12 -0
  11. package/addon/components/order/form.js +21 -1
  12. package/addon/components/order/kanban.js +72 -0
  13. package/addon/components/order-config-manager/activity-flow.js +45 -10
  14. package/addon/components/order-config-manager/lifecycle.hbs +250 -0
  15. package/addon/components/order-config-manager/lifecycle.js +358 -0
  16. package/addon/components/order-config-manager.js +5 -1
  17. package/addon/controllers/analytics/reports/index.js +10 -1
  18. package/addon/controllers/connectivity/devices/index/details.js +1 -0
  19. package/addon/controllers/connectivity/devices/index.js +8 -2
  20. package/addon/controllers/connectivity/events/index.js +31 -1
  21. package/addon/controllers/connectivity/sensors/index/details.js +1 -1
  22. package/addon/controllers/connectivity/sensors/index.js +37 -1
  23. package/addon/controllers/maintenance/equipment/index/details.js +2 -2
  24. package/addon/controllers/maintenance/equipment/index.js +28 -9
  25. package/addon/controllers/maintenance/inspection-forms/index.js +32 -12
  26. package/addon/controllers/maintenance/inspection-submissions/index/details.js +2 -1
  27. package/addon/controllers/maintenance/inspection-submissions/index.js +49 -9
  28. package/addon/controllers/maintenance/maintenances/index/details.js +2 -0
  29. package/addon/controllers/maintenance/maintenances/index.js +20 -1
  30. package/addon/controllers/maintenance/parts/index/details.js +2 -2
  31. package/addon/controllers/maintenance/parts/index.js +28 -8
  32. package/addon/controllers/maintenance/schedules/index.js +33 -4
  33. package/addon/controllers/maintenance/work-orders/index/details.js +3 -3
  34. package/addon/controllers/maintenance/work-orders/index.js +39 -8
  35. package/addon/controllers/management/contacts/customers.js +13 -0
  36. package/addon/controllers/management/contacts/index.js +43 -1
  37. package/addon/controllers/management/drivers/index/details.js +1 -0
  38. package/addon/controllers/management/drivers/index.js +32 -2
  39. package/addon/controllers/management/fleets/index/details.js +1 -0
  40. package/addon/controllers/management/fleets/index.js +39 -1
  41. package/addon/controllers/management/fuel-reports/index.js +22 -2
  42. package/addon/controllers/management/fuel-transactions/index.js +43 -7
  43. package/addon/controllers/management/issues/index/details.js +1 -0
  44. package/addon/controllers/management/issues/index.js +22 -2
  45. package/addon/controllers/management/places/index.js +36 -1
  46. package/addon/controllers/management/trailers/index/details.js +1 -0
  47. package/addon/controllers/management/trailers/index.js +50 -2
  48. package/addon/controllers/management/vehicles/index/details.js +1 -0
  49. package/addon/controllers/management/vehicles/index.js +33 -2
  50. package/addon/controllers/management/vendors/index.js +26 -2
  51. package/addon/controllers/operations/orders/index/details.js +31 -4
  52. package/addon/controllers/operations/orders/index.js +59 -4
  53. package/addon/controllers/operations/service-rates/index.js +15 -1
  54. package/addon/extension.js +32 -0
  55. package/addon/routes/operations/orders/index/details.js +1 -0
  56. package/addon/services/contact-actions.js +1 -0
  57. package/addon/services/customer-actions.js +2 -1
  58. package/addon/services/device-actions.js +1 -0
  59. package/addon/services/device-event-actions.js +1 -0
  60. package/addon/services/driver-actions.js +4 -13
  61. package/addon/services/equipment-actions.js +1 -0
  62. package/addon/services/fleet-actions.js +1 -0
  63. package/addon/services/fuel-report-actions.js +1 -0
  64. package/addon/services/inspection-form-actions.js +1 -0
  65. package/addon/services/inspection-submission-actions.js +1 -0
  66. package/addon/services/integrated-vendor-actions.js +1 -0
  67. package/addon/services/issue-actions.js +1 -0
  68. package/addon/services/maintenance-actions.js +1 -0
  69. package/addon/services/maintenance-schedule-actions.js +2 -1
  70. package/addon/services/order-actions.js +7 -0
  71. package/addon/services/order-presentation.js +129 -0
  72. package/addon/services/part-actions.js +1 -0
  73. package/addon/services/place-actions.js +1 -0
  74. package/addon/services/sensor-actions.js +1 -0
  75. package/addon/services/service-area-actions.js +1 -0
  76. package/addon/services/service-rate-actions.js +1 -0
  77. package/addon/services/telematic-actions.js +1 -0
  78. package/addon/services/trailer-actions.js +4 -14
  79. package/addon/services/vehicle-actions.js +4 -13
  80. package/addon/services/vendor-actions.js +1 -0
  81. package/addon/services/work-order-actions.js +1 -0
  82. package/addon/services/zone-actions.js +1 -0
  83. package/addon/templates/analytics/reports/index/details.hbs +1 -0
  84. package/addon/templates/analytics/reports/index.hbs +1 -0
  85. package/addon/templates/connectivity/devices/index/details.hbs +1 -0
  86. package/addon/templates/connectivity/devices/index.hbs +1 -0
  87. package/addon/templates/connectivity/events/index.hbs +1 -0
  88. package/addon/templates/connectivity/sensors/index/details.hbs +1 -0
  89. package/addon/templates/connectivity/sensors/index.hbs +1 -0
  90. package/addon/templates/maintenance/equipment/index/details.hbs +1 -0
  91. package/addon/templates/maintenance/equipment/index.hbs +2 -0
  92. package/addon/templates/maintenance/inspection-forms/index/details.hbs +1 -0
  93. package/addon/templates/maintenance/inspection-forms/index.hbs +1 -0
  94. package/addon/templates/maintenance/inspection-submissions/index/details.hbs +1 -0
  95. package/addon/templates/maintenance/inspection-submissions/index.hbs +1 -0
  96. package/addon/templates/maintenance/maintenances/index/details.hbs +1 -0
  97. package/addon/templates/maintenance/maintenances/index.hbs +1 -0
  98. package/addon/templates/maintenance/parts/index/details.hbs +1 -0
  99. package/addon/templates/maintenance/parts/index.hbs +2 -0
  100. package/addon/templates/maintenance/schedules/index/details.hbs +1 -0
  101. package/addon/templates/maintenance/schedules/index.hbs +2 -0
  102. package/addon/templates/maintenance/work-orders/index/details.hbs +1 -0
  103. package/addon/templates/maintenance/work-orders/index.hbs +1 -0
  104. package/addon/templates/management/contacts/customers/details.hbs +1 -0
  105. package/addon/templates/management/contacts/customers.hbs +1 -0
  106. package/addon/templates/management/contacts/index/details.hbs +1 -0
  107. package/addon/templates/management/contacts/index.hbs +1 -0
  108. package/addon/templates/management/contacts.hbs +1 -0
  109. package/addon/templates/management/drivers/index/details.hbs +1 -0
  110. package/addon/templates/management/drivers/index.hbs +2 -0
  111. package/addon/templates/management/fleets/index/details.hbs +1 -0
  112. package/addon/templates/management/fleets/index.hbs +1 -0
  113. package/addon/templates/management/fuel-reports/index/details.hbs +1 -0
  114. package/addon/templates/management/fuel-reports/index.hbs +1 -0
  115. package/addon/templates/management/fuel-transactions/index.hbs +1 -0
  116. package/addon/templates/management/issues/index/details.hbs +1 -0
  117. package/addon/templates/management/issues/index.hbs +1 -0
  118. package/addon/templates/management/places/index/details.hbs +1 -0
  119. package/addon/templates/management/places/index.hbs +1 -0
  120. package/addon/templates/management/trailers/index/details.hbs +1 -0
  121. package/addon/templates/management/trailers/index.hbs +2 -0
  122. package/addon/templates/management/vehicles/index/details.hbs +1 -0
  123. package/addon/templates/management/vehicles/index.hbs +2 -0
  124. package/addon/templates/management/vendors/index/details.hbs +1 -0
  125. package/addon/templates/management/vendors/index.hbs +2 -0
  126. package/addon/templates/management/vendors/integrated.hbs +1 -0
  127. package/addon/templates/operations/orders/index/details.hbs +1 -0
  128. package/addon/templates/operations/orders/index.hbs +1 -0
  129. package/addon/templates/operations/service-rates/index/details.hbs +1 -0
  130. package/addon/templates/operations/service-rates/index.hbs +1 -0
  131. package/addon/utils/normalize-order-config-flow.js +6 -1
  132. package/app/services/order-presentation.js +1 -0
  133. package/composer.json +1 -1
  134. package/extension.json +1 -1
  135. package/package.json +3 -3
  136. package/server/src/Http/Controllers/Internal/v1/OrderController.php +34 -4
  137. package/server/src/Models/Order.php +35 -3
  138. package/server/src/Models/OrderConfig.php +129 -0
  139. package/server/src/Models/TrackingNumber.php +8 -4
  140. package/server/tests/Feature/Http/Internal/OrderControllerActivityFlowsTest.php +72 -0
  141. package/server/tests/Feature/Http/Internal/OrderControllerCreateRecordTest.php +26 -0
  142. package/server/tests/OrderConfigLifecycleTest.php +120 -0
  143. package/server/tests/Unit/Events/OrderDispatchFailedTest.php +2 -1
  144. 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
- public function getEventData(): array
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
  }
@@ -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!