@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.
- package/addon/components/customer/create-order-form.hbs +2 -2
- package/addon/components/customer/form.hbs +2 -2
- package/addon/components/customer/order-form.hbs +2 -2
- package/addon/components/customer/orders.hbs +2 -2
- package/addon/components/display-place.hbs +2 -2
- package/addon/components/map/google-live-map.js +6 -2
- package/addon/components/map/leaflet-live-map.hbs +48 -14
- package/addon/components/map/leaflet-live-map.js +184 -20
- package/addon/components/modals/order-change-destination.hbs +46 -0
- package/addon/components/modals/order-change-destination.js +106 -0
- package/addon/components/modals/stop-activity.hbs +28 -0
- package/addon/components/modals/stop-activity.js +52 -0
- package/addon/components/modals/update-order-activity.hbs +45 -3
- package/addon/components/modals/update-order-activity.js +143 -5
- package/addon/components/orchestrator/resource-panel.hbs +0 -10
- package/addon/components/orchestrator/resource-panel.js +2 -4
- package/addon/components/order/activity-list.hbs +2 -2
- package/addon/components/order/activity-timeline.hbs +31 -29
- package/addon/components/order/details/activity.hbs +3 -3
- package/addon/components/order/details/activity.js +11 -0
- package/addon/components/order/details/documents.js +2 -0
- package/addon/components/order/details/purchase-rate.hbs +2 -2
- package/addon/components/order/details/route.hbs +11 -3
- package/addon/components/order/details/route.js +91 -14
- package/addon/components/order/details/tracking.hbs +101 -72
- package/addon/components/order/details/tracking.js +149 -2
- package/addon/components/order/form/details.hbs +3 -3
- package/addon/components/order/form/documents.js +11 -4
- package/addon/components/order/form/route.hbs +108 -104
- package/addon/components/order/form/route.js +37 -1
- package/addon/components/order/form/service-rate.hbs +2 -2
- package/addon/components/order-config-manager/activity-flow.hbs +4 -1
- package/addon/components/order-config-manager/activity-flow.js +178 -12
- package/addon/components/place/address.js +1 -1
- package/addon/components/route-list.hbs +17 -17
- package/addon/components/route-list.js +42 -10
- package/addon/components/service-rate/details.hbs +41 -0
- package/addon/components/service-rate/form.hbs +118 -0
- package/addon/components/service-rate/form.js +55 -0
- package/addon/components/vehicle/form.js +1 -1
- package/addon/components/vehicle/pill.hbs +1 -1
- package/addon/controllers/management/vehicles/index/new.js +1 -1
- package/addon/controllers/operations/orders/index/details.js +4 -4
- package/addon/controllers/operations/orders/index.js +14 -0
- package/addon/helpers/place-address-html.js +2 -2
- package/addon/routes/operations/service-rates/index.js +1 -1
- package/addon/services/geofence.js +163 -19
- package/addon/services/leaflet-contextmenu-manager.js +27 -0
- package/addon/services/vehicle-actions.js +1 -1
- package/addon/styles/fleetops-engine.css +91 -1
- package/addon/utils/fleet-ops-options.js +2 -1
- package/addon/utils/normalize-order-config-flow.js +41 -0
- package/addon/utils/place-address-html.js +3 -2
- package/addon/utils/route-visualization.js +54 -7
- package/app/components/modals/order-change-destination.js +1 -0
- package/app/components/modals/stop-activity.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +3 -3
- package/server/migrations/2026_05_21_000001_add_geographic_rules_to_service_rate_fees_table.php +37 -0
- package/server/migrations/2026_05_25_000001_add_endpoint_tracking_refs_to_payloads_table.php +50 -0
- package/server/migrations/2026_05_27_000001_default_driver_vehicle_statuses_to_available.php +48 -0
- package/server/resources/views/labels/default.php +57 -49
- package/server/resources/views/labels/waypoint-label.php +27 -20
- package/server/seeders/Testing/ConnectivitySeeder.php +1 -0
- package/server/seeders/Testing/FleetSeeder.php +22 -22
- package/server/seeders/Testing/OrdersSeeder.php +6 -22
- package/server/seeders/Testing/VroomOrchestrationSeeder.php +21 -21
- package/server/src/Console/Commands/AuditCustomerUserConflicts.php +75 -0
- package/server/src/Exceptions/CustomerUserConflictException.php +13 -0
- package/server/src/Http/Controllers/Api/v1/ContactController.php +17 -2
- package/server/src/Http/Controllers/Api/v1/CustomerController.php +876 -0
- package/server/src/Http/Controllers/Api/v1/DriverController.php +1 -1
- package/server/src/Http/Controllers/Api/v1/OrderConfigController.php +54 -0
- package/server/src/Http/Controllers/Api/v1/OrderController.php +98 -70
- package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -2
- package/server/src/Http/Controllers/Api/v1/VehicleController.php +20 -3
- package/server/src/Http/Controllers/Internal/v1/ContactController.php +19 -0
- package/server/src/Http/Controllers/Internal/v1/LiveController.php +101 -66
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +429 -26
- package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/SettingController.php +2 -2
- package/server/src/Http/Controllers/Internal/v1/VehicleController.php +54 -1
- package/server/src/Http/Middleware/AuthenticateCustomerToken.php +34 -0
- package/server/src/Http/Requests/CreateCustomerOrderRequest.php +63 -0
- package/server/src/Http/Requests/CreateCustomerRequest.php +48 -0
- package/server/src/Http/Requests/CreateDriverRequest.php +1 -1
- package/server/src/Http/Requests/CreateVehicleRequest.php +25 -1
- package/server/src/Http/Requests/Internal/CreateDriverRequest.php +1 -1
- package/server/src/Http/Requests/VerifyCreateCustomerRequest.php +29 -0
- package/server/src/Http/Resources/Internal/v1/OrderConfig.php +41 -0
- package/server/src/Http/Resources/v1/Customer.php +79 -0
- package/server/src/Http/Resources/v1/OrderConfig.php +74 -0
- package/server/src/Http/Resources/v1/Payload.php +23 -20
- package/server/src/Http/Resources/v1/ServiceArea.php +33 -19
- package/server/src/Http/Resources/v1/ServiceRateFee.php +7 -0
- package/server/src/Listeners/HandleOrderDispatched.php +2 -2
- package/server/src/Models/Contact.php +92 -15
- package/server/src/Models/Customer.php +64 -0
- package/server/src/Models/Driver.php +12 -3
- package/server/src/Models/Entity.php +2 -2
- package/server/src/Models/Order.php +29 -4
- package/server/src/Models/Payload.php +17 -1
- package/server/src/Models/ServiceRate.php +273 -1
- package/server/src/Models/ServiceRateFee.php +39 -9
- package/server/src/Models/TrackingNumber.php +8 -2
- package/server/src/Models/Vehicle.php +18 -2
- package/server/src/Models/Waypoint.php +2 -2
- package/server/src/Notifications/LateDeparture.php +2 -2
- package/server/src/Notifications/ProlongedStoppage.php +2 -2
- package/server/src/Notifications/RouteDeviation.php +2 -2
- package/server/src/Observers/ContactObserver.php +2 -0
- package/server/src/Observers/ServiceRateObserver.php +6 -6
- package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +1 -1
- package/server/src/Providers/FleetOpsServiceProvider.php +1 -0
- package/server/src/Support/Analytics/AbstractAnalytics.php +6 -6
- package/server/src/Support/Analytics/FuelEfficiency.php +2 -2
- package/server/src/Support/Analytics/OrdersByStatus.php +1 -1
- package/server/src/Support/Analytics/RevenueTrend.php +0 -1
- package/server/src/Support/Analytics/TopDrivers.php +1 -1
- package/server/src/Support/CustomerAuth.php +98 -0
- package/server/src/Support/LabelPdf.php +52 -0
- package/server/src/Support/Metrics/AbstractMetric.php +4 -4
- package/server/src/Support/ResolvesOrderServiceStops.php +512 -0
- package/server/src/Tracking/TrackingContextBuilder.php +41 -38
- package/server/src/Tracking/TrackingIntelligenceService.php +109 -9
- package/server/src/Tracking/TrackingStop.php +13 -11
- package/server/src/routes.php +43 -9
- package/server/tests/CapacityAllocationEngineTest.php +10 -10
- package/server/tests/CustomerContactIdentitySafetyTest.php +54 -0
- package/server/tests/CustomerEndpointTest.php +199 -0
- package/server/tests/DriverVehicleStatusDefaultTest.php +39 -0
- package/server/tests/LabelPdfRenderingTest.php +23 -0
- package/server/tests/LiveControllerViewportTest.php +60 -0
- package/server/tests/MetricsRegistryTest.php +4 -8
- package/server/tests/OrchestrationPayloadBuilderTest.php +2 -2
- package/server/tests/OrderConfigEndpointTest.php +86 -0
- package/server/tests/OrderFileAttachmentTest.php +20 -0
- package/server/tests/ServiceAreaZoneApiShapeTest.php +34 -0
- package/server/tests/VehicleDriverAssignmentTest.php +18 -0
- package/translations/en-us.yaml +1 -0
- package/server/migrations/2026_05_13_000001_repair_customer_contact_user_types.php +0 -86
|
@@ -8,6 +8,8 @@ use Illuminate\Support\Facades\Cache;
|
|
|
8
8
|
|
|
9
9
|
class TrackingIntelligenceService
|
|
10
10
|
{
|
|
11
|
+
protected const TERMINAL_STATUSES = ['completed', 'canceled'];
|
|
12
|
+
|
|
11
13
|
public function __construct(
|
|
12
14
|
protected TrackingContextBuilder $contextBuilder,
|
|
13
15
|
protected TrackingProviderManager $providerManager,
|
|
@@ -34,19 +36,28 @@ class TrackingIntelligenceService
|
|
|
34
36
|
'completion_seconds' => data_get($tracker, 'eta.completion_seconds'),
|
|
35
37
|
'active_stop_at' => data_get($tracker, 'eta.active_stop_at'),
|
|
36
38
|
'completion_at' => data_get($tracker, 'eta.completion_at'),
|
|
39
|
+
'start_seconds' => data_get($tracker, 'eta.start_seconds'),
|
|
40
|
+
'start_at' => data_get($tracker, 'eta.start_at'),
|
|
37
41
|
'provider' => data_get($tracker, 'provider'),
|
|
38
42
|
'confidence' => data_get($tracker, 'confidence'),
|
|
43
|
+
'lifecycle' => data_get($tracker, 'lifecycle'),
|
|
39
44
|
'warnings' => data_get($tracker, 'warnings', []),
|
|
40
45
|
];
|
|
41
46
|
}
|
|
42
47
|
|
|
43
48
|
protected function buildResult(TrackingContext $context, TrackingProviderResult $providerResult, TrackingOptions $options): array
|
|
44
49
|
{
|
|
45
|
-
$now
|
|
46
|
-
$
|
|
47
|
-
$
|
|
48
|
-
$
|
|
49
|
-
$
|
|
50
|
+
$now = now();
|
|
51
|
+
$rawCompletionSeconds = $providerResult->durationInTrafficSeconds ?? $providerResult->durationSeconds;
|
|
52
|
+
$rawActiveStopSeconds = data_get($providerResult->legs, '0.duration_in_traffic_s', data_get($providerResult->legs, '0.duration_s', $rawCompletionSeconds));
|
|
53
|
+
$lifecycle = $this->lifecycle($context, $rawActiveStopSeconds, $now);
|
|
54
|
+
$showsLiveEta = (bool) data_get($lifecycle, 'show_live_eta');
|
|
55
|
+
$showsStartEta = (bool) data_get($lifecycle, 'show_start_eta');
|
|
56
|
+
$completionSeconds = $showsLiveEta ? $rawCompletionSeconds : null;
|
|
57
|
+
$activeStopSeconds = $showsLiveEta ? $rawActiveStopSeconds : null;
|
|
58
|
+
$startSeconds = $showsStartEta ? $rawActiveStopSeconds : null;
|
|
59
|
+
$progress = $this->progress($context, $providerResult);
|
|
60
|
+
$warnings = array_values(array_unique([...$context->warnings, ...$providerResult->warnings]));
|
|
50
61
|
|
|
51
62
|
if ($providerResult->confidence === 'low') {
|
|
52
63
|
$warnings[] = 'low_confidence_eta';
|
|
@@ -64,6 +75,7 @@ class TrackingIntelligenceService
|
|
|
64
75
|
'online' => (bool) data_get($context->driver, 'online', false),
|
|
65
76
|
],
|
|
66
77
|
'progress' => $progress,
|
|
78
|
+
'lifecycle' => $lifecycle,
|
|
67
79
|
'stops' => $context->stops->map(fn (TrackingStop $stop) => $stop->toArray())->values()->all(),
|
|
68
80
|
'active_stop' => $context->activeStop?->toArray(),
|
|
69
81
|
'next_stop' => $context->nextStop?->toArray(),
|
|
@@ -73,13 +85,15 @@ class TrackingIntelligenceService
|
|
|
73
85
|
'duration_in_traffic_s' => $providerResult->durationInTrafficSeconds,
|
|
74
86
|
'polyline' => $providerResult->polyline,
|
|
75
87
|
'coordinates' => $providerResult->coordinates,
|
|
76
|
-
'legs' => $this->legs($context, $providerResult, $now),
|
|
88
|
+
'legs' => $this->legs($context, $providerResult, $now, $showsLiveEta),
|
|
77
89
|
],
|
|
78
90
|
'eta' => [
|
|
79
91
|
'active_stop_seconds' => $activeStopSeconds,
|
|
80
92
|
'completion_seconds' => $completionSeconds,
|
|
81
93
|
'active_stop_at' => $this->addSeconds($now, $activeStopSeconds),
|
|
82
94
|
'completion_at' => $this->addSeconds($now, $completionSeconds),
|
|
95
|
+
'start_seconds' => $startSeconds,
|
|
96
|
+
'start_at' => $this->addSeconds($now, $startSeconds),
|
|
83
97
|
],
|
|
84
98
|
'insights' => [
|
|
85
99
|
'is_delayed' => false,
|
|
@@ -107,17 +121,17 @@ class TrackingIntelligenceService
|
|
|
107
121
|
];
|
|
108
122
|
}
|
|
109
123
|
|
|
110
|
-
protected function legs(TrackingContext $context, TrackingProviderResult $providerResult, Carbon $now): array
|
|
124
|
+
protected function legs(TrackingContext $context, TrackingProviderResult $providerResult, Carbon $now, bool $showsLiveEta = true): array
|
|
111
125
|
{
|
|
112
126
|
$elapsedSeconds = 0;
|
|
113
127
|
|
|
114
|
-
return collect($providerResult->legs)->map(function ($leg, $index) use ($context, $now, &$elapsedSeconds) {
|
|
128
|
+
return collect($providerResult->legs)->map(function ($leg, $index) use ($context, $now, $showsLiveEta, &$elapsedSeconds) {
|
|
115
129
|
$stop = $context->remainingStops->values()->get($index);
|
|
116
130
|
$legSeconds = data_get($leg, 'duration_in_traffic_s', data_get($leg, 'duration_s'));
|
|
117
131
|
$etaSeconds = null;
|
|
118
132
|
$etaAt = null;
|
|
119
133
|
|
|
120
|
-
if ($legSeconds !== null) {
|
|
134
|
+
if ($showsLiveEta && $stop instanceof TrackingStop && $legSeconds !== null) {
|
|
121
135
|
$elapsedSeconds += (float) $legSeconds;
|
|
122
136
|
$etaSeconds = $elapsedSeconds;
|
|
123
137
|
$etaAt = $this->addSeconds($now, $etaSeconds);
|
|
@@ -131,6 +145,92 @@ class TrackingIntelligenceService
|
|
|
131
145
|
})->all();
|
|
132
146
|
}
|
|
133
147
|
|
|
148
|
+
protected function lifecycle(TrackingContext $context, ?float $startSeconds, Carbon $now): array
|
|
149
|
+
{
|
|
150
|
+
$order = $context->order;
|
|
151
|
+
$status = strtolower((string) ($order->status ?? 'created'));
|
|
152
|
+
$hasAssignedDriver = !empty($order->driver_assigned_uuid);
|
|
153
|
+
$hasStarted = $this->hasOrderStarted($order);
|
|
154
|
+
$isTerminal = in_array($status, self::TERMINAL_STATUSES, true);
|
|
155
|
+
$isDispatched = $status === 'dispatched' || (bool) $order->dispatched;
|
|
156
|
+
|
|
157
|
+
if ($isTerminal) {
|
|
158
|
+
return [
|
|
159
|
+
'status' => $status,
|
|
160
|
+
'mode' => $status,
|
|
161
|
+
'message' => $status === 'completed' ? 'Order has been completed.' : 'Order has been canceled.',
|
|
162
|
+
'is_terminal' => true,
|
|
163
|
+
'has_started' => $hasStarted,
|
|
164
|
+
'show_live_eta' => false,
|
|
165
|
+
'show_start_eta' => false,
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!$hasAssignedDriver) {
|
|
170
|
+
return [
|
|
171
|
+
'status' => $status,
|
|
172
|
+
'mode' => 'unassigned',
|
|
173
|
+
'message' => 'Assign a driver to start live tracking and improve ETA accuracy.',
|
|
174
|
+
'is_terminal' => false,
|
|
175
|
+
'has_started' => $hasStarted,
|
|
176
|
+
'show_live_eta' => false,
|
|
177
|
+
'show_start_eta' => false,
|
|
178
|
+
];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!$hasStarted && $isDispatched) {
|
|
182
|
+
return [
|
|
183
|
+
'status' => $status,
|
|
184
|
+
'mode' => 'dispatched',
|
|
185
|
+
'message' => 'Order has been dispatched. Estimated start is based on the assigned driver route to the first stop.',
|
|
186
|
+
'is_terminal' => false,
|
|
187
|
+
'has_started' => false,
|
|
188
|
+
'show_live_eta' => false,
|
|
189
|
+
'show_start_eta' => $startSeconds !== null,
|
|
190
|
+
'start_at' => $this->addSeconds($now, $startSeconds),
|
|
191
|
+
];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!$hasStarted) {
|
|
195
|
+
return [
|
|
196
|
+
'status' => $status,
|
|
197
|
+
'mode' => 'pre_start',
|
|
198
|
+
'message' => 'Live ETA will begin once the order is started.',
|
|
199
|
+
'is_terminal' => false,
|
|
200
|
+
'has_started' => false,
|
|
201
|
+
'show_live_eta' => false,
|
|
202
|
+
'show_start_eta' => false,
|
|
203
|
+
];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return [
|
|
207
|
+
'status' => $status,
|
|
208
|
+
'mode' => 'active',
|
|
209
|
+
'message' => null,
|
|
210
|
+
'is_terminal' => false,
|
|
211
|
+
'has_started' => true,
|
|
212
|
+
'show_live_eta' => true,
|
|
213
|
+
'show_start_eta' => false,
|
|
214
|
+
];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
protected function hasOrderStarted(Order $order): bool
|
|
218
|
+
{
|
|
219
|
+
if ((bool) $order->started || $order->started_at || strtolower((string) $order->status) === 'started') {
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
if ($order->relationLoaded('trackingStatuses')) {
|
|
225
|
+
return $order->trackingStatuses->contains(fn ($status) => strtolower((string) data_get($status, 'code')) === 'started');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return $order->trackingStatuses()->whereRaw('LOWER(code) = ?', ['started'])->exists();
|
|
229
|
+
} catch (\Throwable) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
134
234
|
protected function capabilitiesFor(string $provider): array
|
|
135
235
|
{
|
|
136
236
|
$registry = app(TrackingProviderRegistry::class);
|
|
@@ -18,6 +18,7 @@ class TrackingStop implements \JsonSerializable
|
|
|
18
18
|
public ?Waypoint $waypoint = null,
|
|
19
19
|
public bool $completed = false,
|
|
20
20
|
public ?int $sequence = null,
|
|
21
|
+
public ?string $trackingNumberUuid = null,
|
|
21
22
|
) {
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -35,20 +36,21 @@ class TrackingStop implements \JsonSerializable
|
|
|
35
36
|
$point = $this->point();
|
|
36
37
|
|
|
37
38
|
return [
|
|
38
|
-
'uuid'
|
|
39
|
-
'public_id'
|
|
40
|
-
'type'
|
|
41
|
-
'status'
|
|
42
|
-
'completed'
|
|
43
|
-
'sequence'
|
|
44
|
-
'
|
|
45
|
-
'
|
|
46
|
-
'
|
|
39
|
+
'uuid' => $this->uuid,
|
|
40
|
+
'public_id' => $this->publicId,
|
|
41
|
+
'type' => $this->type,
|
|
42
|
+
'status' => $this->status,
|
|
43
|
+
'completed' => $this->completed,
|
|
44
|
+
'sequence' => $this->sequence,
|
|
45
|
+
'tracking_number_uuid' => $this->trackingNumberUuid,
|
|
46
|
+
'address' => data_get($this->place, 'address'),
|
|
47
|
+
'name' => data_get($this->place, 'name'),
|
|
48
|
+
'location' => $point ? [
|
|
47
49
|
'type' => 'Point',
|
|
48
50
|
'coordinates' => [$point->getLng(), $point->getLat()],
|
|
49
51
|
] : null,
|
|
50
|
-
'latitude'
|
|
51
|
-
'longitude'
|
|
52
|
+
'latitude' => $point ? $point->getLat() : null,
|
|
53
|
+
'longitude' => $point ? $point->getLng() : null,
|
|
52
54
|
];
|
|
53
55
|
}
|
|
54
56
|
|
package/server/src/routes.php
CHANGED
|
@@ -49,6 +49,38 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
|
|
|
49
49
|
$router->put('{id}', 'ContactController@update');
|
|
50
50
|
$router->delete('{id}', 'ContactController@delete');
|
|
51
51
|
});
|
|
52
|
+
// order-configs — read-only public projection of the OrderConfig flow.
|
|
53
|
+
$router->group(['prefix' => 'order-configs'], function () use ($router) {
|
|
54
|
+
$router->get('/', 'OrderConfigController@query');
|
|
55
|
+
$router->get('{id}', 'OrderConfigController@find');
|
|
56
|
+
});
|
|
57
|
+
// customers routes — public B2C customer auth + customer-scoped orders.
|
|
58
|
+
// - Public endpoints: API key only (resolves company from credential)
|
|
59
|
+
// - Authenticated endpoints: API key + `Customer-Token` header (Sanctum)
|
|
60
|
+
$router->group(['prefix' => 'customers', 'middleware' => []], function () use ($router) {
|
|
61
|
+
// Public auth flows
|
|
62
|
+
$router->post('request-creation-code', 'CustomerController@requestCreationCode');
|
|
63
|
+
$router->post('/', 'CustomerController@create');
|
|
64
|
+
$router->post('login', 'CustomerController@login');
|
|
65
|
+
$router->post('login-with-sms', 'CustomerController@loginWithPhone');
|
|
66
|
+
$router->post('verify-code', 'CustomerController@verifyCode');
|
|
67
|
+
$router->post('forgot-password', 'CustomerController@forgotPassword');
|
|
68
|
+
$router->post('reset-password', 'CustomerController@resetPassword');
|
|
69
|
+
|
|
70
|
+
// Authenticated (require Customer-Token)
|
|
71
|
+
$router->group(['middleware' => [Fleetbase\FleetOps\Http\Middleware\AuthenticateCustomerToken::class]], function () use ($router) {
|
|
72
|
+
$router->get('me', 'CustomerController@me');
|
|
73
|
+
$router->put('me', 'CustomerController@updateMe');
|
|
74
|
+
$router->match(['post', 'patch'], 'me', 'CustomerController@updateMe');
|
|
75
|
+
$router->post('logout', 'CustomerController@logout');
|
|
76
|
+
$router->post('logout-all', 'CustomerController@logoutAll');
|
|
77
|
+
$router->post('register-device', 'CustomerController@registerDevice');
|
|
78
|
+
$router->get('places', 'CustomerController@places');
|
|
79
|
+
$router->get('orders', 'CustomerController@orders');
|
|
80
|
+
$router->post('orders', 'CustomerController@createOrder');
|
|
81
|
+
$router->get('orders/{id}', 'CustomerController@findOrder');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
52
84
|
// vendors routes
|
|
53
85
|
$router->group(['prefix' => 'vendors'], function () use ($router) {
|
|
54
86
|
$router->post('/', 'VendorController@create');
|
|
@@ -346,8 +378,10 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
|
|
|
346
378
|
$router->get('{id}/tracker', 'OrderController@trackerInfo');
|
|
347
379
|
$router->get('{id}/eta', 'OrderController@waypointEtas');
|
|
348
380
|
$router->post('{id}/ping-driver', $controller('pingDriver'));
|
|
381
|
+
$router->post('{id}/capture-photo/{subjectId?}', $controller('capturePhoto'));
|
|
349
382
|
$router->post('process-imports', $controller('importFromFiles'));
|
|
350
383
|
$router->patch('route/{id}', $controller('editOrderRoute'));
|
|
384
|
+
$router->patch('set-destination/{id}/{placeId}', $controller('setDestination'));
|
|
351
385
|
$router->patch('update-activity/{id}', $controller('updateActivity'));
|
|
352
386
|
$router->get('{id}/proofs/{subjectId?}', $controller('proofs'));
|
|
353
387
|
$router->patch('bulk-assign-driver', $controller('bulkAssignDriver'));
|
|
@@ -579,16 +613,16 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
|
|
|
579
613
|
$router->group(
|
|
580
614
|
['prefix' => 'analytics'],
|
|
581
615
|
function ($router) {
|
|
582
|
-
$router->get('operations-pulse',
|
|
583
|
-
$router->get('revenue-trend',
|
|
584
|
-
$router->get('orders-by-status',
|
|
585
|
-
$router->get('on-time-delivery',
|
|
586
|
-
$router->get('top-drivers',
|
|
587
|
-
$router->get('fuel-efficiency',
|
|
588
|
-
$router->get('issues-insights',
|
|
616
|
+
$router->get('operations-pulse', 'AnalyticsController@operationsPulse');
|
|
617
|
+
$router->get('revenue-trend', 'AnalyticsController@revenueTrend');
|
|
618
|
+
$router->get('orders-by-status', 'AnalyticsController@ordersByStatus');
|
|
619
|
+
$router->get('on-time-delivery', 'AnalyticsController@onTimeDelivery');
|
|
620
|
+
$router->get('top-drivers', 'AnalyticsController@topDrivers');
|
|
621
|
+
$router->get('fuel-efficiency', 'AnalyticsController@fuelEfficiency');
|
|
622
|
+
$router->get('issues-insights', 'AnalyticsController@issuesInsights');
|
|
589
623
|
$router->get('maintenance-overview', 'AnalyticsController@maintenanceOverview');
|
|
590
|
-
$router->get('geofence-violations',
|
|
591
|
-
$router->get('live-fleet',
|
|
624
|
+
$router->get('geofence-violations', 'AnalyticsController@geofenceViolations');
|
|
625
|
+
$router->get('live-fleet', 'AnalyticsController@liveFleet');
|
|
592
626
|
}
|
|
593
627
|
);
|
|
594
628
|
$router->group(
|
|
@@ -16,8 +16,8 @@ function capacityTestOrder(string $publicId, float $weightKg, float $volumeLitre
|
|
|
16
16
|
public float $weight;
|
|
17
17
|
public string $weight_unit = 'kg';
|
|
18
18
|
public float $length;
|
|
19
|
-
public float $width
|
|
20
|
-
public float $height
|
|
19
|
+
public float $width = 1;
|
|
20
|
+
public float $height = 1;
|
|
21
21
|
public string $dimensions_unit = 'm';
|
|
22
22
|
|
|
23
23
|
public function __construct(float $weightKg, float $volumeLitres)
|
|
@@ -30,8 +30,8 @@ function capacityTestOrder(string $publicId, float $weightKg, float $volumeLitre
|
|
|
30
30
|
}
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
-
$order
|
|
34
|
-
$order->public_id
|
|
33
|
+
$order = new Order();
|
|
34
|
+
$order->public_id = $publicId;
|
|
35
35
|
$order->required_skills = $skills;
|
|
36
36
|
$order->setRelation('payload', $payload);
|
|
37
37
|
|
|
@@ -40,14 +40,14 @@ function capacityTestOrder(string $publicId, float $weightKg, float $volumeLitre
|
|
|
40
40
|
|
|
41
41
|
function capacityTestVehicle(string $publicId, float $weightKg, float $volumeM3 = 0, int $maxTasks = 0, array $skills = []): Vehicle
|
|
42
42
|
{
|
|
43
|
-
$vehicle
|
|
44
|
-
$vehicle->public_id
|
|
45
|
-
$vehicle->payload_capacity
|
|
46
|
-
$vehicle->payload_capacity_volume
|
|
43
|
+
$vehicle = new Vehicle();
|
|
44
|
+
$vehicle->public_id = $publicId;
|
|
45
|
+
$vehicle->payload_capacity = $weightKg;
|
|
46
|
+
$vehicle->payload_capacity_volume = $volumeM3;
|
|
47
47
|
$vehicle->payload_capacity_pallets = 0;
|
|
48
48
|
$vehicle->payload_capacity_parcels = 100;
|
|
49
|
-
$vehicle->max_tasks
|
|
50
|
-
$vehicle->skills
|
|
49
|
+
$vehicle->max_tasks = $maxTasks;
|
|
50
|
+
$vehicle->skills = $skills;
|
|
51
51
|
$vehicle->setRelation('driver', null);
|
|
52
52
|
|
|
53
53
|
return $vehicle;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
test('customer contacts reject existing non-customer users by identity', function () {
|
|
4
|
+
$contactModel = file_get_contents(__DIR__ . '/../src/Models/Contact.php');
|
|
5
|
+
$conflictClass = file_get_contents(__DIR__ . '/../src/Exceptions/CustomerUserConflictException.php');
|
|
6
|
+
$observer = file_get_contents(__DIR__ . '/../src/Observers/ContactObserver.php');
|
|
7
|
+
|
|
8
|
+
expect($contactModel)
|
|
9
|
+
->toContain('assertCustomerIdentityIsAvailable')
|
|
10
|
+
->toContain('assertCustomerUserCanBeAssigned')
|
|
11
|
+
->toContain("orWhereHas('companyUsers'")
|
|
12
|
+
->toContain("\$user->type === 'customer'")
|
|
13
|
+
->toContain('cannot be used for a customer account');
|
|
14
|
+
|
|
15
|
+
expect($conflictClass)->toContain('class CustomerUserConflictException extends UserAlreadyExistsException');
|
|
16
|
+
|
|
17
|
+
expect($observer)
|
|
18
|
+
->toContain('$contact->assertCustomerIdentityIsAvailable();')
|
|
19
|
+
->toContain('$contact->normalizeCustomerUser();');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('customer user assignment no longer promotes staff users', function () {
|
|
23
|
+
$contactModel = file_get_contents(__DIR__ . '/../src/Models/Contact.php');
|
|
24
|
+
|
|
25
|
+
expect($contactModel)
|
|
26
|
+
->toContain('public function assignUser(User $user, bool $sendInvite = false): self')
|
|
27
|
+
->toContain('$this->assertCustomerUserCanBeAssigned($user);')
|
|
28
|
+
->not->toContain("if (\$this->isCustomer() && \$user->type !== 'customer') {\n \$user->setType('customer');");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('customer creation entrypoints run the customer identity guard', function () {
|
|
32
|
+
$apiContact = file_get_contents(__DIR__ . '/../src/Http/Controllers/Api/v1/ContactController.php');
|
|
33
|
+
$internalContact = file_get_contents(__DIR__ . '/../src/Http/Controllers/Internal/v1/ContactController.php');
|
|
34
|
+
$orderController = file_get_contents(__DIR__ . '/../src/Http/Controllers/Api/v1/OrderController.php');
|
|
35
|
+
$customerForm = file_get_contents(__DIR__ . '/../../addon/components/customer/form.hbs');
|
|
36
|
+
|
|
37
|
+
expect($apiContact)->toContain('$contactCandidate->assertCustomerIdentityIsAvailable();');
|
|
38
|
+
expect($internalContact)->toContain('$this->assertCustomerIdentityIsAvailable($input');
|
|
39
|
+
expect($orderController)->toContain('$customerCandidate->assertCustomerIdentityIsAvailable();');
|
|
40
|
+
expect($customerForm)->toContain('is_customer=true');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('customer conflict audit command is registered and read only', function () {
|
|
44
|
+
$provider = file_get_contents(__DIR__ . '/../src/Providers/FleetOpsServiceProvider.php');
|
|
45
|
+
$command = file_get_contents(__DIR__ . '/../src/Console/Commands/AuditCustomerUserConflicts.php');
|
|
46
|
+
|
|
47
|
+
expect($provider)->toContain('AuditCustomerUserConflicts::class');
|
|
48
|
+
expect($command)
|
|
49
|
+
->toContain('fleetops:audit-customer-user-conflicts')
|
|
50
|
+
->toContain('Reports customer contacts linked to users')
|
|
51
|
+
->not->toContain('->save(')
|
|
52
|
+
->not->toContain('->update(')
|
|
53
|
+
->not->toContain('forceFill');
|
|
54
|
+
});
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Fleetbase\FleetOps\Http\Controllers\Api\v1\CustomerController;
|
|
4
|
+
use Fleetbase\FleetOps\Http\Middleware\AuthenticateCustomerToken;
|
|
5
|
+
use Fleetbase\FleetOps\Http\Requests\CreateCustomerOrderRequest;
|
|
6
|
+
use Fleetbase\FleetOps\Http\Requests\CreateCustomerRequest;
|
|
7
|
+
use Fleetbase\FleetOps\Http\Requests\VerifyCreateCustomerRequest;
|
|
8
|
+
use Fleetbase\FleetOps\Http\Resources\v1\Customer as CustomerResource;
|
|
9
|
+
use Fleetbase\FleetOps\Models\Customer as CustomerModel;
|
|
10
|
+
use Fleetbase\FleetOps\Support\CustomerAuth;
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
|--------------------------------------------------------------------------
|
|
14
|
+
| Customer API surface — static shape checks.
|
|
15
|
+
|
|
|
16
|
+
| These mirror the lightweight static checks used elsewhere in this package
|
|
17
|
+
| (see PingDriverEndpointTest.php). They verify that the customer endpoints,
|
|
18
|
+
| controller methods, middleware, and supporting classes exist and are wired
|
|
19
|
+
| through `Api/v1/CustomerController` without the Storefront layer.
|
|
20
|
+
|
|
|
21
|
+
| End-to-end HTTP tests against a running app belong in the parent `api/`
|
|
22
|
+
| project's test harness; this file ensures the package surface is correct.
|
|
23
|
+
|--------------------------------------------------------------------------
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
test('customers route group is registered inside the consumable v1 namespace', function () {
|
|
27
|
+
$routes = file_get_contents(dirname(__DIR__) . '/src/routes.php');
|
|
28
|
+
|
|
29
|
+
expect($routes)
|
|
30
|
+
->toContain("\$router->group(['prefix' => 'customers', 'middleware' => []]")
|
|
31
|
+
->and($routes)->toContain('CustomerController@requestCreationCode')
|
|
32
|
+
->and($routes)->toContain('CustomerController@create')
|
|
33
|
+
->and($routes)->toContain('CustomerController@login')
|
|
34
|
+
->and($routes)->toContain('CustomerController@loginWithPhone')
|
|
35
|
+
->and($routes)->toContain('CustomerController@verifyCode')
|
|
36
|
+
->and($routes)->toContain('CustomerController@forgotPassword')
|
|
37
|
+
->and($routes)->toContain('CustomerController@resetPassword');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('authenticated customer routes are gated by AuthenticateCustomerToken middleware', function () {
|
|
41
|
+
$routes = file_get_contents(dirname(__DIR__) . '/src/routes.php');
|
|
42
|
+
|
|
43
|
+
expect($routes)
|
|
44
|
+
->toContain('AuthenticateCustomerToken::class')
|
|
45
|
+
->and($routes)->toContain('CustomerController@me')
|
|
46
|
+
->and($routes)->toContain('CustomerController@updateMe')
|
|
47
|
+
->and($routes)->toContain('CustomerController@logout')
|
|
48
|
+
->and($routes)->toContain('CustomerController@logoutAll')
|
|
49
|
+
->and($routes)->toContain('CustomerController@orders')
|
|
50
|
+
->and($routes)->toContain('CustomerController@createOrder')
|
|
51
|
+
->and($routes)->toContain('CustomerController@findOrder')
|
|
52
|
+
->and($routes)->toContain('CustomerController@places')
|
|
53
|
+
->and($routes)->toContain('CustomerController@registerDevice');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('customer controller exposes the documented method surface', function () {
|
|
57
|
+
$expected = [
|
|
58
|
+
'requestCreationCode', 'create',
|
|
59
|
+
'login', 'loginWithPhone', 'verifyCode',
|
|
60
|
+
'forgotPassword', 'resetPassword',
|
|
61
|
+
'me', 'updateMe', 'logout', 'logoutAll',
|
|
62
|
+
'orders', 'createOrder', 'findOrder',
|
|
63
|
+
'places', 'registerDevice',
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
foreach ($expected as $method) {
|
|
67
|
+
expect(method_exists(CustomerController::class, $method))->toBeTrue("CustomerController::{$method} missing");
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('customer controller does not reference Storefront concerns', function () {
|
|
72
|
+
$source = file_get_contents(dirname(__DIR__) . '/src/Http/Controllers/Api/v1/CustomerController.php');
|
|
73
|
+
|
|
74
|
+
expect($source)
|
|
75
|
+
->not->toContain('Storefront::about')
|
|
76
|
+
->not->toContain("session('storefront_key')")
|
|
77
|
+
->not->toContain("session('storefront_store')")
|
|
78
|
+
->not->toContain("session('storefront_network')")
|
|
79
|
+
->not->toContain('storefront_id')
|
|
80
|
+
->not->toContain('createStripeCustomerForContact');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('customer controller does not introduce client-portal field aliases', function () {
|
|
84
|
+
// Public Fleetbase APIs must accept canonical model field names only. No
|
|
85
|
+
// aliasing like line1/state/zip → street1/province/postal_code, no portal-
|
|
86
|
+
// specific tagging like meta.origin, no top-level item/weight/value/mode
|
|
87
|
+
// aliases on order create — clients conform to the API, not vice versa.
|
|
88
|
+
$source = file_get_contents(dirname(__DIR__) . '/src/Http/Controllers/Api/v1/CustomerController.php');
|
|
89
|
+
|
|
90
|
+
expect($source)
|
|
91
|
+
->not->toContain("'line1'")
|
|
92
|
+
->not->toContain("'line2'")
|
|
93
|
+
->not->toContain("'state'")
|
|
94
|
+
->not->toContain("'zip'")
|
|
95
|
+
->not->toContain("'origin' => 'fleetops_customer_portal'")
|
|
96
|
+
->not->toContain('fleetops_customer_portal')
|
|
97
|
+
->not->toContain("\$request->input('item'")
|
|
98
|
+
->not->toContain("\$request->input('value'")
|
|
99
|
+
->not->toContain("\$request->input('delivery'")
|
|
100
|
+
->not->toContain("\$request->input('category'");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('createOrder mirrors the canonical Fleet-Ops order shape', function () {
|
|
104
|
+
$source = file_get_contents(dirname(__DIR__) . '/src/Http/Controllers/Api/v1/CustomerController.php');
|
|
105
|
+
|
|
106
|
+
expect($source)
|
|
107
|
+
->toContain('OrderConfig::resolveFromIdentifier')
|
|
108
|
+
->toContain('buildPayloadFromInput')
|
|
109
|
+
->toContain('Payload')
|
|
110
|
+
->toContain('setPickup')
|
|
111
|
+
->toContain('setDropoff')
|
|
112
|
+
->toContain('setEntities')
|
|
113
|
+
->toContain("'customer_uuid'");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('verification code slugs are FleetOps-namespaced, not Storefront', function () {
|
|
117
|
+
$source = file_get_contents(dirname(__DIR__) . '/src/Http/Controllers/Api/v1/CustomerController.php');
|
|
118
|
+
|
|
119
|
+
expect($source)
|
|
120
|
+
->toContain("'fleetops_create_customer'")
|
|
121
|
+
->toContain("'fleetops_customer_login'")
|
|
122
|
+
->toContain("'fleetops_customer_password_reset'")
|
|
123
|
+
->not->toContain("'storefront_create_customer'")
|
|
124
|
+
->not->toContain("'storefront_login'");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('AuthenticateCustomerToken enforces a customer-token + company cross-check', function () {
|
|
128
|
+
expect(class_exists(AuthenticateCustomerToken::class))->toBeTrue();
|
|
129
|
+
|
|
130
|
+
$source = file_get_contents(dirname(__DIR__) . '/src/Http/Middleware/AuthenticateCustomerToken.php');
|
|
131
|
+
expect($source)
|
|
132
|
+
->toContain('Customer token is missing or invalid')
|
|
133
|
+
->toContain('Customer does not belong to this company')
|
|
134
|
+
->toContain('CustomerAuth::resolveFromHeader')
|
|
135
|
+
->toContain('CustomerAuth::setCurrent');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('CustomerAuth resolves tokens by contact UUID with company-preferred fallback', function () {
|
|
139
|
+
expect(class_exists(CustomerAuth::class))->toBeTrue()
|
|
140
|
+
->and(method_exists(CustomerAuth::class, 'resolveFromHeader'))->toBeTrue()
|
|
141
|
+
->and(method_exists(CustomerAuth::class, 'current'))->toBeTrue()
|
|
142
|
+
->and(method_exists(CustomerAuth::class, 'setCurrent'))->toBeTrue()
|
|
143
|
+
->and(CustomerAuth::HEADER)->toBe('Customer-Token');
|
|
144
|
+
|
|
145
|
+
$source = file_get_contents(dirname(__DIR__) . '/src/Support/CustomerAuth.php');
|
|
146
|
+
expect($source)
|
|
147
|
+
->toContain('PersonalAccessToken::findToken')
|
|
148
|
+
->toContain("->where('type', 'customer')")
|
|
149
|
+
->toContain("->where('company_uuid'");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('Customer model extends Contact with a type=customer global scope', function () {
|
|
153
|
+
expect(is_subclass_of(CustomerModel::class, Fleetbase\FleetOps\Models\Contact::class))->toBeTrue();
|
|
154
|
+
|
|
155
|
+
$source = file_get_contents(dirname(__DIR__) . '/src/Models/Customer.php');
|
|
156
|
+
expect($source)
|
|
157
|
+
->toContain("\$model->type = 'customer'")
|
|
158
|
+
->toContain("->where('type', 'customer')");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('Customer API resource exposes token and orders_count for the consumable shape', function () {
|
|
162
|
+
expect(class_exists(CustomerResource::class))->toBeTrue();
|
|
163
|
+
|
|
164
|
+
$source = file_get_contents(dirname(__DIR__) . '/src/Http/Resources/v1/Customer.php');
|
|
165
|
+
expect($source)
|
|
166
|
+
->toContain("'token'")
|
|
167
|
+
->toContain("'orders_count'")
|
|
168
|
+
->toContain("Str::replaceFirst('contact', 'customer'");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('Customer API resource includes a company sub-object resolved from the API credential', function () {
|
|
172
|
+
$source = file_get_contents(dirname(__DIR__) . '/src/Http/Resources/v1/Customer.php');
|
|
173
|
+
|
|
174
|
+
expect($source)
|
|
175
|
+
// The sub-object lives on `company`, keyed off the customer's company_uuid.
|
|
176
|
+
->toContain("'company'")
|
|
177
|
+
->toContain('buildCompanyPayload')
|
|
178
|
+
// Currency must be resolved through the existing canonical helper —
|
|
179
|
+
// never hardcoded, never aliased.
|
|
180
|
+
->toContain('Utils::getCompanyTransactionCurrency')
|
|
181
|
+
// Public-safe projection only: id, name, currency, country, phone.
|
|
182
|
+
->toContain("'currency'")
|
|
183
|
+
->toContain("'country'")
|
|
184
|
+
->toContain("'phone'");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('FormRequest validators are present and authorize via api credential', function () {
|
|
188
|
+
expect(class_exists(CreateCustomerRequest::class))->toBeTrue()
|
|
189
|
+
->and(class_exists(VerifyCreateCustomerRequest::class))->toBeTrue()
|
|
190
|
+
->and(class_exists(CreateCustomerOrderRequest::class))->toBeTrue();
|
|
191
|
+
|
|
192
|
+
$create = file_get_contents(dirname(__DIR__) . '/src/Http/Requests/CreateCustomerRequest.php');
|
|
193
|
+
$verify = file_get_contents(dirname(__DIR__) . '/src/Http/Requests/VerifyCreateCustomerRequest.php');
|
|
194
|
+
|
|
195
|
+
expect($create)
|
|
196
|
+
->toContain("'code' => 'required|exists:verification_codes,code'")
|
|
197
|
+
->toContain("'password' => 'required|string|min:8'")
|
|
198
|
+
->and($verify)->toContain("'mode' => 'required|in:email,sms'");
|
|
199
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Fleetbase\FleetOps\Models\Driver;
|
|
4
|
+
use Fleetbase\FleetOps\Models\Vehicle;
|
|
5
|
+
|
|
6
|
+
test('driver and vehicle statuses default to available', function () {
|
|
7
|
+
expect((new Driver())->status)
|
|
8
|
+
->toBe('available')
|
|
9
|
+
->and((new Vehicle())->status)
|
|
10
|
+
->toBe('available');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('legacy active and null driver and vehicle statuses normalize to available', function () {
|
|
14
|
+
$driver = new Driver();
|
|
15
|
+
$driver->status = 'active';
|
|
16
|
+
expect($driver->status)->toBe('available');
|
|
17
|
+
|
|
18
|
+
$driver->status = null;
|
|
19
|
+
expect($driver->status)->toBe('available');
|
|
20
|
+
|
|
21
|
+
$vehicle = new Vehicle();
|
|
22
|
+
$vehicle->status = 'active';
|
|
23
|
+
expect($vehicle->status)->toBe('available');
|
|
24
|
+
|
|
25
|
+
$vehicle->status = null;
|
|
26
|
+
expect($vehicle->status)->toBe('available');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('dispatch driver availability queries use available status', function () {
|
|
30
|
+
$listener = file_get_contents(dirname(__DIR__) . '/src/Listeners/HandleOrderDispatched.php');
|
|
31
|
+
$order = file_get_contents(dirname(__DIR__) . '/src/Models/Order.php');
|
|
32
|
+
|
|
33
|
+
expect($listener)
|
|
34
|
+
->toContain("'status' => 'available'")
|
|
35
|
+
->not->toContain("Driver::where(['status' => 'active'")
|
|
36
|
+
->and($order)
|
|
37
|
+
->toContain("'status' => 'available'")
|
|
38
|
+
->not->toContain("Driver::where(['status' => 'active'");
|
|
39
|
+
});
|