@fleetbase/fleetops-engine 0.6.49 → 0.6.50

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 (142) hide show
  1. package/addon/components/customer/create-order-form.hbs +2 -2
  2. package/addon/components/customer/form.hbs +2 -2
  3. package/addon/components/customer/order-form.hbs +2 -2
  4. package/addon/components/customer/orders.hbs +2 -2
  5. package/addon/components/display-place.hbs +2 -2
  6. package/addon/components/map/google-live-map.js +6 -2
  7. package/addon/components/map/leaflet-live-map.hbs +48 -14
  8. package/addon/components/map/leaflet-live-map.js +184 -20
  9. package/addon/components/modals/order-change-destination.hbs +46 -0
  10. package/addon/components/modals/order-change-destination.js +106 -0
  11. package/addon/components/modals/stop-activity.hbs +28 -0
  12. package/addon/components/modals/stop-activity.js +52 -0
  13. package/addon/components/modals/update-order-activity.hbs +45 -3
  14. package/addon/components/modals/update-order-activity.js +143 -5
  15. package/addon/components/orchestrator/resource-panel.hbs +0 -10
  16. package/addon/components/orchestrator/resource-panel.js +2 -4
  17. package/addon/components/order/activity-list.hbs +2 -2
  18. package/addon/components/order/activity-timeline.hbs +31 -29
  19. package/addon/components/order/details/activity.hbs +3 -3
  20. package/addon/components/order/details/activity.js +11 -0
  21. package/addon/components/order/details/documents.js +2 -0
  22. package/addon/components/order/details/purchase-rate.hbs +2 -2
  23. package/addon/components/order/details/route.hbs +11 -3
  24. package/addon/components/order/details/route.js +91 -14
  25. package/addon/components/order/details/tracking.hbs +101 -72
  26. package/addon/components/order/details/tracking.js +149 -2
  27. package/addon/components/order/form/details.hbs +3 -3
  28. package/addon/components/order/form/documents.js +11 -4
  29. package/addon/components/order/form/route.hbs +108 -104
  30. package/addon/components/order/form/route.js +37 -1
  31. package/addon/components/order/form/service-rate.hbs +2 -2
  32. package/addon/components/order-config-manager/activity-flow.hbs +4 -1
  33. package/addon/components/order-config-manager/activity-flow.js +178 -12
  34. package/addon/components/place/address.js +1 -1
  35. package/addon/components/route-list.hbs +17 -17
  36. package/addon/components/route-list.js +42 -10
  37. package/addon/components/service-rate/details.hbs +41 -0
  38. package/addon/components/service-rate/form.hbs +118 -0
  39. package/addon/components/service-rate/form.js +55 -0
  40. package/addon/components/vehicle/form.js +1 -1
  41. package/addon/components/vehicle/pill.hbs +1 -1
  42. package/addon/controllers/management/vehicles/index/new.js +1 -1
  43. package/addon/controllers/operations/orders/index/details.js +4 -4
  44. package/addon/controllers/operations/orders/index.js +14 -0
  45. package/addon/helpers/place-address-html.js +2 -2
  46. package/addon/routes/operations/service-rates/index.js +1 -1
  47. package/addon/services/geofence.js +163 -19
  48. package/addon/services/leaflet-contextmenu-manager.js +27 -0
  49. package/addon/services/vehicle-actions.js +1 -1
  50. package/addon/styles/fleetops-engine.css +91 -1
  51. package/addon/utils/fleet-ops-options.js +2 -1
  52. package/addon/utils/normalize-order-config-flow.js +41 -0
  53. package/addon/utils/place-address-html.js +3 -2
  54. package/addon/utils/route-visualization.js +54 -7
  55. package/app/components/modals/order-change-destination.js +1 -0
  56. package/app/components/modals/stop-activity.js +1 -0
  57. package/composer.json +1 -1
  58. package/extension.json +1 -1
  59. package/package.json +3 -3
  60. package/server/migrations/2026_05_21_000001_add_geographic_rules_to_service_rate_fees_table.php +37 -0
  61. package/server/migrations/2026_05_25_000001_add_endpoint_tracking_refs_to_payloads_table.php +50 -0
  62. package/server/migrations/2026_05_27_000001_default_driver_vehicle_statuses_to_available.php +48 -0
  63. package/server/resources/views/labels/default.php +57 -49
  64. package/server/resources/views/labels/waypoint-label.php +27 -20
  65. package/server/seeders/Testing/ConnectivitySeeder.php +1 -0
  66. package/server/seeders/Testing/FleetSeeder.php +22 -22
  67. package/server/seeders/Testing/OrdersSeeder.php +6 -22
  68. package/server/seeders/Testing/VroomOrchestrationSeeder.php +21 -21
  69. package/server/src/Console/Commands/AuditCustomerUserConflicts.php +75 -0
  70. package/server/src/Exceptions/CustomerUserConflictException.php +13 -0
  71. package/server/src/Http/Controllers/Api/v1/ContactController.php +17 -2
  72. package/server/src/Http/Controllers/Api/v1/CustomerController.php +876 -0
  73. package/server/src/Http/Controllers/Api/v1/DriverController.php +1 -1
  74. package/server/src/Http/Controllers/Api/v1/OrderConfigController.php +54 -0
  75. package/server/src/Http/Controllers/Api/v1/OrderController.php +98 -70
  76. package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -2
  77. package/server/src/Http/Controllers/Api/v1/VehicleController.php +20 -3
  78. package/server/src/Http/Controllers/Internal/v1/ContactController.php +19 -0
  79. package/server/src/Http/Controllers/Internal/v1/LiveController.php +101 -66
  80. package/server/src/Http/Controllers/Internal/v1/OrderController.php +429 -26
  81. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
  82. package/server/src/Http/Controllers/Internal/v1/SettingController.php +2 -2
  83. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +54 -1
  84. package/server/src/Http/Middleware/AuthenticateCustomerToken.php +34 -0
  85. package/server/src/Http/Requests/CreateCustomerOrderRequest.php +63 -0
  86. package/server/src/Http/Requests/CreateCustomerRequest.php +48 -0
  87. package/server/src/Http/Requests/CreateDriverRequest.php +1 -1
  88. package/server/src/Http/Requests/CreateVehicleRequest.php +25 -1
  89. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +1 -1
  90. package/server/src/Http/Requests/VerifyCreateCustomerRequest.php +29 -0
  91. package/server/src/Http/Resources/Internal/v1/OrderConfig.php +41 -0
  92. package/server/src/Http/Resources/v1/Customer.php +79 -0
  93. package/server/src/Http/Resources/v1/OrderConfig.php +74 -0
  94. package/server/src/Http/Resources/v1/Payload.php +23 -20
  95. package/server/src/Http/Resources/v1/ServiceArea.php +33 -19
  96. package/server/src/Http/Resources/v1/ServiceRateFee.php +7 -0
  97. package/server/src/Listeners/HandleOrderDispatched.php +2 -2
  98. package/server/src/Models/Contact.php +92 -15
  99. package/server/src/Models/Customer.php +64 -0
  100. package/server/src/Models/Driver.php +12 -3
  101. package/server/src/Models/Entity.php +2 -2
  102. package/server/src/Models/Order.php +29 -4
  103. package/server/src/Models/Payload.php +17 -1
  104. package/server/src/Models/ServiceRate.php +273 -1
  105. package/server/src/Models/ServiceRateFee.php +39 -9
  106. package/server/src/Models/TrackingNumber.php +8 -2
  107. package/server/src/Models/Vehicle.php +18 -2
  108. package/server/src/Models/Waypoint.php +2 -2
  109. package/server/src/Notifications/LateDeparture.php +2 -2
  110. package/server/src/Notifications/ProlongedStoppage.php +2 -2
  111. package/server/src/Notifications/RouteDeviation.php +2 -2
  112. package/server/src/Observers/ContactObserver.php +2 -0
  113. package/server/src/Observers/ServiceRateObserver.php +6 -6
  114. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +1 -1
  115. package/server/src/Providers/FleetOpsServiceProvider.php +1 -0
  116. package/server/src/Support/Analytics/AbstractAnalytics.php +6 -6
  117. package/server/src/Support/Analytics/FuelEfficiency.php +2 -2
  118. package/server/src/Support/Analytics/OrdersByStatus.php +1 -1
  119. package/server/src/Support/Analytics/RevenueTrend.php +0 -1
  120. package/server/src/Support/Analytics/TopDrivers.php +1 -1
  121. package/server/src/Support/CustomerAuth.php +98 -0
  122. package/server/src/Support/LabelPdf.php +52 -0
  123. package/server/src/Support/Metrics/AbstractMetric.php +4 -4
  124. package/server/src/Support/ResolvesOrderServiceStops.php +512 -0
  125. package/server/src/Tracking/TrackingContextBuilder.php +41 -38
  126. package/server/src/Tracking/TrackingIntelligenceService.php +109 -9
  127. package/server/src/Tracking/TrackingStop.php +13 -11
  128. package/server/src/routes.php +43 -9
  129. package/server/tests/CapacityAllocationEngineTest.php +10 -10
  130. package/server/tests/CustomerContactIdentitySafetyTest.php +54 -0
  131. package/server/tests/CustomerEndpointTest.php +199 -0
  132. package/server/tests/DriverVehicleStatusDefaultTest.php +39 -0
  133. package/server/tests/LabelPdfRenderingTest.php +23 -0
  134. package/server/tests/LiveControllerViewportTest.php +60 -0
  135. package/server/tests/MetricsRegistryTest.php +4 -8
  136. package/server/tests/OrchestrationPayloadBuilderTest.php +2 -2
  137. package/server/tests/OrderConfigEndpointTest.php +86 -0
  138. package/server/tests/OrderFileAttachmentTest.php +20 -0
  139. package/server/tests/ServiceAreaZoneApiShapeTest.php +34 -0
  140. package/server/tests/VehicleDriverAssignmentTest.php +18 -0
  141. package/translations/en-us.yaml +1 -0
  142. package/server/migrations/2026_05_13_000001_repair_customer_contact_user_types.php +0 -86
@@ -0,0 +1,23 @@
1
+ <?php
2
+
3
+ test('labels use the shared utf8 pdf renderer instead of arabic-only html rewriting', function () {
4
+ $labelPdf = file_get_contents(dirname(__DIR__) . '/src/Support/LabelPdf.php');
5
+ $order = file_get_contents(dirname(__DIR__) . '/src/Models/Order.php');
6
+ $waypoint = file_get_contents(dirname(__DIR__) . '/src/Models/Waypoint.php');
7
+ $entity = file_get_contents(dirname(__DIR__) . '/src/Models/Entity.php');
8
+ $default = file_get_contents(dirname(__DIR__) . '/resources/views/labels/default.php');
9
+ $stop = file_get_contents(dirname(__DIR__) . '/resources/views/labels/waypoint-label.php');
10
+ $utils = file_get_contents(dirname(__DIR__) . '/src/Support/Utils.php');
11
+
12
+ expect($labelPdf)
13
+ ->toContain("Pdf::loadHTML(\$html, 'UTF-8')")
14
+ ->toContain("'defaultFont' => 'DejaVu Sans'")
15
+ ->and($order)->toContain('LabelPdf::fromHtml($this->label())')
16
+ ->and($waypoint)->toContain('LabelPdf::fromHtml($this->label())')
17
+ ->and($entity)->toContain('LabelPdf::fromHtml($this->label())')
18
+ ->and($default)->toContain('Noto Sans Arabic')
19
+ ->not->toContain('strtoupper($company->name)')
20
+ ->and($stop)->toContain('Noto Sans CJK JP')
21
+ ->not->toContain('strtoupper($company->name)')
22
+ ->and($utils)->not->toContain('fixArabicInHTML');
23
+ });
@@ -0,0 +1,60 @@
1
+ <?php
2
+
3
+ use Fleetbase\FleetOps\Http\Controllers\Internal\v1\LiveController;
4
+ use Fleetbase\FleetOps\Models\Driver;
5
+ use Illuminate\Http\Request;
6
+
7
+ function callLiveControllerMethod(string $method, array $arguments = [])
8
+ {
9
+ $reflection = new ReflectionMethod(LiveController::class, $method);
10
+ $reflection->setAccessible(true);
11
+
12
+ return $reflection->invokeArgs(new LiveController(), $arguments);
13
+ }
14
+
15
+ test('live viewport bounds are normalized for stable cache keys', function () {
16
+ $request = new Request([
17
+ 'bounds' => ['1.234567', '103.876543', '1.345678', '103.987654'],
18
+ ]);
19
+
20
+ expect(callLiveControllerMethod('normalizeLiveBounds', [$request]))
21
+ ->toBe([1.2346, 103.8765, 1.3457, 103.9877]);
22
+ });
23
+
24
+ test('invalid live viewport bounds fall back to unbounded queries', function ($bounds) {
25
+ $request = new Request(['bounds' => $bounds]);
26
+
27
+ expect(callLiveControllerMethod('normalizeLiveBounds', [$request]))->toBeNull();
28
+ })->with([
29
+ 'missing coordinate' => [[1, 2, 3]],
30
+ 'non numeric' => [[1, 'west', 3, 4]],
31
+ 'invalid latitude' => [[-91, 103, 1, 104]],
32
+ 'invalid longitude' => [[1, -181, 2, 104]],
33
+ 'inverted latitude' => [[2, 103, 1, 104]],
34
+ 'inverted longitude' => [[1, 104, 2, 103]],
35
+ ]);
36
+
37
+ test('live viewport limit defaults and clamps', function () {
38
+ expect(callLiveControllerMethod('normalizeLiveLimit', [new Request()]))->toBe(500)
39
+ ->and(callLiveControllerMethod('normalizeLiveLimit', [new Request(['limit' => 25])]))->toBe(25)
40
+ ->and(callLiveControllerMethod('normalizeLiveLimit', [new Request(['limit' => 0])]))->toBe(500)
41
+ ->and(callLiveControllerMethod('normalizeLiveLimit', [new Request(['limit' => 5000])]))->toBe(1000);
42
+ });
43
+
44
+ test('live viewport query avoids spatial constructors with fixed srids', function () {
45
+ $query = Driver::query();
46
+
47
+ callLiveControllerMethod('applyLiveLocationGuards', [$query]);
48
+ callLiveControllerMethod('applyLiveViewportBounds', [$query, [1.2, 103.8, 1.4, 104.0]]);
49
+ $query->orderByDesc('updated_at')->orderByDesc('id')->limit(25);
50
+
51
+ $sql = $query->toSql();
52
+
53
+ expect($sql)->toContain('ST_Y(location) BETWEEN ? AND ?')
54
+ ->and($sql)->toContain('ST_X(location) BETWEEN ? AND ?')
55
+ ->and($sql)->toContain('limit 25')
56
+ ->and($sql)->not->toContain('ST_MakeEnvelope')
57
+ ->and($sql)->not->toContain('ST_GeomFromText');
58
+
59
+ expect($query->getBindings())->toContain(1.2, 1.4, 103.8, 104.0);
60
+ });
@@ -1,12 +1,8 @@
1
1
  <?php
2
2
 
3
3
  use Fleetbase\FleetOps\Support\Metrics\AbstractMetric;
4
- use Fleetbase\FleetOps\Support\Metrics\AvgOrderValueMetric;
5
- use Fleetbase\FleetOps\Support\Metrics\EarningsMetric;
6
- use Fleetbase\FleetOps\Support\Metrics\OpenIssuesMetric;
7
4
  use Fleetbase\FleetOps\Support\Metrics\OrdersInProgressMetric;
8
5
  use Fleetbase\FleetOps\Support\Metrics\Registry;
9
- use Fleetbase\FleetOps\Support\Metrics\ResolvedIssuesMetric;
10
6
  use Fleetbase\FleetOps\Support\Metrics\TotalTimeTraveledMetric;
11
7
 
12
8
  test('registry exposes every known metric slug', function () {
@@ -42,7 +38,7 @@ test('every registered metric extends the abstract base', function () {
42
38
  });
43
39
 
44
40
  test('open and resolved issue metrics scope to the constructor company (not session)', function () {
45
- $sourceOpen = file_get_contents(dirname(__DIR__) . '/src/Support/Metrics/OpenIssuesMetric.php');
41
+ $sourceOpen = file_get_contents(dirname(__DIR__) . '/src/Support/Metrics/OpenIssuesMetric.php');
46
42
  $sourceResolved = file_get_contents(dirname(__DIR__) . '/src/Support/Metrics/ResolvedIssuesMetric.php');
47
43
 
48
44
  expect($sourceOpen)->not->toContain("session('company')");
@@ -60,7 +56,7 @@ test('total_time_traveled metric sums the time column, not distance', function (
60
56
  });
61
57
 
62
58
  test('money metrics return float values to preserve cents', function () {
63
- $earnings = file_get_contents(dirname(__DIR__) . '/src/Support/Metrics/EarningsMetric.php');
59
+ $earnings = file_get_contents(dirname(__DIR__) . '/src/Support/Metrics/EarningsMetric.php');
64
60
  $fuelCosts = file_get_contents(dirname(__DIR__) . '/src/Support/Metrics/FuelCostsMetric.php');
65
61
 
66
62
  expect($earnings)->toContain('(float) $query->sum');
@@ -70,9 +66,9 @@ test('money metrics return float values to preserve cents', function () {
70
66
  });
71
67
 
72
68
  test('money metrics filter by currency to avoid mixed-currency sums', function () {
73
- $earnings = file_get_contents(dirname(__DIR__) . '/src/Support/Metrics/EarningsMetric.php');
69
+ $earnings = file_get_contents(dirname(__DIR__) . '/src/Support/Metrics/EarningsMetric.php');
74
70
  $fuelCosts = file_get_contents(dirname(__DIR__) . '/src/Support/Metrics/FuelCostsMetric.php');
75
- $aov = file_get_contents(dirname(__DIR__) . '/src/Support/Metrics/AvgOrderValueMetric.php');
71
+ $aov = file_get_contents(dirname(__DIR__) . '/src/Support/Metrics/AvgOrderValueMetric.php');
76
72
 
77
73
  expect($earnings)->toContain("->where('currency'");
78
74
  expect($fuelCosts)->toContain("->where('currency'");
@@ -39,7 +39,7 @@ function orchestrationTestOrder($pickup = null, $dropoff = null, array $waypoint
39
39
  public function __construct(public $pickup, public $dropoff, array $waypoints)
40
40
  {
41
41
  $this->entities = collect();
42
- $this->waypointMarkers = collect($waypoints)->map(function ($place, $index) {
42
+ $this->waypointMarkers = collect($waypoints)->map(function ($place, $index) {
43
43
  return new class($place, $index) {
44
44
  public string $public_id;
45
45
  public string $uuid;
@@ -61,7 +61,7 @@ function orchestrationTestOrder($pickup = null, $dropoff = null, array $waypoint
61
61
  }
62
62
  };
63
63
 
64
- $order = new Order();
64
+ $order = new Order();
65
65
  $order->public_id = 'order_test';
66
66
  $order->setRelation('payload', $payload);
67
67
 
@@ -0,0 +1,86 @@
1
+ <?php
2
+
3
+ use Fleetbase\FleetOps\Http\Controllers\Api\v1\OrderConfigController;
4
+ use Fleetbase\FleetOps\Http\Resources\Internal\v1\OrderConfig as InternalOrderConfigResource;
5
+ use Fleetbase\FleetOps\Http\Resources\v1\OrderConfig as OrderConfigResource;
6
+
7
+ /*
8
+ |--------------------------------------------------------------------------
9
+ | OrderConfig API surface — static shape checks.
10
+ |
11
+ | The OrderConfig public API surface gives consumers (customer portals,
12
+ | third-party integrations) a read-only projection of the OrderConfig
13
+ | `flow` JSON so they can drive status filters, activity labels, and
14
+ | similar UI from the canonical config rather than hardcoding their own.
15
+ |
16
+ | These mirror the lightweight static checks used elsewhere in this
17
+ | package and verify the routes, controller methods, and resource shape
18
+ | exist without booting Laravel.
19
+ |--------------------------------------------------------------------------
20
+ */
21
+
22
+ test('order-configs route group is registered in the consumable v1 namespace', function () {
23
+ $routes = file_get_contents(dirname(__DIR__) . '/src/routes.php');
24
+
25
+ expect($routes)
26
+ ->toContain("\$router->group(['prefix' => 'order-configs']")
27
+ ->and($routes)->toContain('OrderConfigController@query')
28
+ ->and($routes)->toContain('OrderConfigController@find');
29
+ });
30
+
31
+ test('OrderConfigController exposes only read-only methods', function () {
32
+ foreach (['query', 'find'] as $method) {
33
+ expect(method_exists(OrderConfigController::class, $method))->toBeTrue(
34
+ "OrderConfigController::{$method} missing",
35
+ );
36
+ }
37
+
38
+ // No write methods are exposed — OrderConfigs are owned by operator UIs.
39
+ foreach (['create', 'update', 'delete', 'store', 'destroy'] as $method) {
40
+ expect(method_exists(OrderConfigController::class, $method))->toBeFalse(
41
+ "OrderConfigController::{$method} should NOT exist on the public surface",
42
+ );
43
+ }
44
+ });
45
+
46
+ test('find() resolves by uuid, public_id, namespace, or key', function () {
47
+ $source = file_get_contents(dirname(__DIR__) . '/src/Http/Controllers/Api/v1/OrderConfigController.php');
48
+
49
+ // The controller defers to the canonical resolver so /transport,
50
+ // /system:order-config:transport, the public_id, and the uuid all work.
51
+ expect($source)
52
+ ->toContain('OrderConfig::resolveFromIdentifier')
53
+ ->toContain('findRecordOrFail');
54
+ });
55
+
56
+ test('OrderConfig resource projects only public-safe fields', function () {
57
+ expect(class_exists(OrderConfigResource::class))->toBeTrue();
58
+
59
+ $source = file_get_contents(dirname(__DIR__) . '/src/Http/Resources/v1/OrderConfig.php');
60
+
61
+ expect($source)
62
+ // Public contract: id/key/name/namespace + the flow projection.
63
+ ->toContain("'key'")
64
+ ->toContain("'name'")
65
+ ->toContain("'namespace'")
66
+ ->toContain("'flow'")
67
+ ->toContain('projectFlow')
68
+ // Each flow row keeps the canonical activity keys.
69
+ ->toContain("'code'")
70
+ ->toContain("'status'")
71
+ ->toContain("'complete'")
72
+ // Internal config payload must not be exposed on the public surface.
73
+ ->not->toContain("'entities'");
74
+ });
75
+
76
+ test('internal OrderConfig resource preserves editable flow graph fields', function () {
77
+ expect(class_exists(InternalOrderConfigResource::class))->toBeTrue();
78
+
79
+ $source = file_get_contents(dirname(__DIR__) . '/src/Http/Resources/Internal/v1/OrderConfig.php');
80
+
81
+ expect($source)
82
+ ->toContain("'flow'")
83
+ ->toContain('$this->flow')
84
+ ->toContain("'entities'")
85
+ ->toContain("'core_service'");
86
+ });
@@ -0,0 +1,20 @@
1
+ <?php
2
+
3
+ test('order file updates attach files to the order like creation does', function () {
4
+ $controller = file_get_contents(dirname(__DIR__) . '/src/Http/Controllers/Internal/v1/OrderController.php');
5
+ $orderModel = file_get_contents(dirname(__DIR__) . '/src/Models/Order.php');
6
+ $detailsJs = file_get_contents(dirname(dirname(__DIR__)) . '/addon/components/order/details/documents.js');
7
+ $formJs = file_get_contents(dirname(dirname(__DIR__)) . '/addon/components/order/form/documents.js');
8
+
9
+ expect($controller)
10
+ ->toContain("\$uploads = \$request->array('order.files')")
11
+ ->toContain('$order->attachFiles($uploads)')
12
+ ->and($orderModel)
13
+ ->toContain('public function attachFiles($uploads): self')
14
+ ->toContain('$file->setKey($this)')
15
+ ->and($detailsJs)
16
+ ->toContain("subject_type: 'fleet-ops:order'")
17
+ ->and($formJs)
18
+ ->toContain('if (!this.args.resource.isNew)')
19
+ ->toContain('subject_uuid = this.args.resource.id');
20
+ });
@@ -0,0 +1,34 @@
1
+ <?php
2
+
3
+ test('service area create and update accept persisted map geometry and style fields', function () {
4
+ $controller = file_get_contents(__DIR__ . '/../src/Http/Controllers/Api/v1/ServiceAreaController.php');
5
+
6
+ expect($controller)
7
+ ->toContain("'border'")
8
+ ->toContain("'color'")
9
+ ->toContain("'stroke_color'");
10
+ });
11
+
12
+ test('service area resource returns map geometry and style fields', function () {
13
+ $resource = file_get_contents(__DIR__ . '/../src/Http/Resources/v1/ServiceArea.php');
14
+
15
+ expect($resource)
16
+ ->toContain("'border'")
17
+ ->toContain("'color'")
18
+ ->toContain("'stroke_color'")
19
+ ->toContain("'trigger_on_entry'")
20
+ ->toContain("'trigger_on_exit'");
21
+ });
22
+
23
+ test('zone create accepts only service area public ids on the consumable api', function () {
24
+ $request = file_get_contents(__DIR__ . '/../src/Http/Requests/CreateZoneRequest.php');
25
+ $controller = file_get_contents(__DIR__ . '/../src/Http/Controllers/Api/v1/ZoneController.php');
26
+
27
+ expect($request)
28
+ ->toContain("'service_area'")
29
+ ->not->toContain("'service_area_uuid'");
30
+
31
+ expect($controller)
32
+ ->toContain("'public_id' => \$request->input('service_area')")
33
+ ->not->toContain("->orWhere('uuid'");
34
+ });
@@ -0,0 +1,18 @@
1
+ <?php
2
+
3
+ test('vehicle updates preserve driver assignment unless driver input is explicit', function () {
4
+ $internalController = file_get_contents(dirname(__DIR__) . '/src/Http/Controllers/Internal/v1/VehicleController.php');
5
+ $publicController = file_get_contents(dirname(__DIR__) . '/src/Http/Controllers/Api/v1/VehicleController.php');
6
+ $vehicleModel = file_get_contents(dirname(__DIR__) . '/src/Models/Vehicle.php');
7
+
8
+ expect($internalController)
9
+ ->toContain('hasDriverInput($request)')
10
+ ->toContain('syncDriverAssignment($vehicle, $this->driverIdentifierFromRequest($request))')
11
+ ->toContain("Arr::has(\$payload, 'vehicle.driver_uuid')")
12
+ ->and($publicController)
13
+ ->toContain("\$request->exists('driver')")
14
+ ->toContain('$vehicle->unassignDriver()')
15
+ ->and($vehicleModel)
16
+ ->toContain('public function unassignDriver(): self')
17
+ ->toContain("Driver::where('vehicle_uuid', \$this->uuid)->update(['vehicle_uuid' => null])");
18
+ });
@@ -8,6 +8,7 @@ common:
8
8
  dispatch-orders: Dispatch Orders
9
9
  dispatch-order: Dispatch Order
10
10
  assign-driver: Assign Driver
11
+ unassign-driver: Unassign Driver
11
12
  assign-drivers: Assign Drivers
12
13
  no-driver-assigned: No driver assigned
13
14
  details: Details
@@ -1,86 +0,0 @@
1
- <?php
2
-
3
- use Fleetbase\FleetOps\Models\Contact;
4
- use Fleetbase\Models\CompanyUser;
5
- use Illuminate\Database\Migrations\Migration;
6
- use Illuminate\Support\Facades\DB;
7
- use Illuminate\Support\Facades\Schema;
8
-
9
- return new class extends Migration {
10
- /**
11
- * Run the migrations.
12
- *
13
- * @return void
14
- */
15
- public function up()
16
- {
17
- Contact::withoutGlobalScopes()
18
- ->where('type', 'customer')
19
- ->whereNull('deleted_at')
20
- ->chunkById(100, function ($contacts) {
21
- foreach ($contacts as $contact) {
22
- try {
23
- $contact->repairCustomerTypeInvariant(true);
24
- } catch (Throwable $e) {
25
- // Skip ambiguous or unrecoverable records; runtime invariants handle new writes.
26
- }
27
- }
28
- });
29
-
30
- if (!$this->canUseCustomerRoleHint()) {
31
- return;
32
- }
33
-
34
- DB::table('contacts')
35
- ->select('contacts.id')
36
- ->join('company_users', function ($join) {
37
- $join->on('company_users.user_uuid', '=', 'contacts.user_uuid');
38
- $join->on('company_users.company_uuid', '=', 'contacts.company_uuid');
39
- })
40
- ->join('model_has_roles', function ($join) {
41
- $join->on('model_has_roles.model_uuid', '=', 'company_users.uuid');
42
- $join->where('model_has_roles.model_type', CompanyUser::class);
43
- })
44
- ->join('roles', 'roles.id', '=', 'model_has_roles.role_id')
45
- ->where('roles.name', 'Fleet-Ops Customer')
46
- ->whereNull('contacts.deleted_at')
47
- ->where(function ($query) {
48
- $query->whereNull('contacts.type');
49
- $query->orWhere('contacts.type', '!=', 'customer');
50
- })
51
- ->distinct()
52
- ->orderBy('contacts.id')
53
- ->chunk(100, function ($rows) {
54
- foreach ($rows as $row) {
55
- $contact = Contact::withoutGlobalScopes()->find($row->id);
56
- if (!$contact) {
57
- continue;
58
- }
59
-
60
- try {
61
- $contact->repairCustomerTypeInvariant(true);
62
- } catch (Throwable $e) {
63
- // Skip ambiguous or unrecoverable records; role is only a recovery hint.
64
- }
65
- }
66
- });
67
- }
68
-
69
- /**
70
- * Reverse the migrations.
71
- *
72
- * @return void
73
- */
74
- public function down()
75
- {
76
- // Data repair is intentionally not reversible.
77
- }
78
-
79
- private function canUseCustomerRoleHint(): bool
80
- {
81
- return Schema::hasTable('contacts')
82
- && Schema::hasTable('company_users')
83
- && Schema::hasTable('model_has_roles')
84
- && Schema::hasTable('roles');
85
- }
86
- };