@fleetbase/fleetops-engine 0.6.43 → 0.6.45
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/entity/form.hbs +24 -0
- package/addon/components/entity/form.js +30 -0
- package/addon/components/leaflet-draw-control.js +1 -1
- package/addon/components/map/drawer/geofence-event-listing.hbs +123 -0
- package/addon/components/map/drawer/geofence-event-listing.js +103 -0
- package/addon/components/map/drawer.js +1 -0
- package/addon/components/map/leaflet-live-map.js +83 -0
- package/addon/components/modals/orchestrator-import.js +2 -78
- package/addon/components/order/form/service-rate.hbs +1 -1
- package/addon/components/service-area/form.hbs +59 -0
- package/addon/components/service-rate/details.hbs +6 -6
- package/addon/components/service-rate/form.hbs +28 -15
- package/addon/components/zone/form.hbs +60 -1
- package/addon/controllers/operations/orders/index/details/virtual.js +5 -0
- package/addon/controllers/operations/orders/index/details.js +27 -2
- package/addon/controllers/operations/service-rates/index/edit.js +1 -0
- package/addon/controllers/operations/service-rates/index/new.js +1 -0
- package/addon/helpers/f-to-int.js +6 -0
- package/addon/routes/application.js +3 -3
- package/addon/routes/operations/orders/index/details/virtual.js +1 -0
- package/addon/routes/operations/orders/index/details.js +19 -3
- package/addon/services/equipment-actions.js +4 -6
- package/addon/services/geofence-event-bus.js +223 -0
- package/addon/services/leaflet-map-manager.js +4 -0
- package/addon/services/maintenance-actions.js +4 -6
- package/addon/services/part-actions.js +4 -6
- package/addon/services/service-rate-actions.js +8 -3
- package/addon/templates/operations/orders/index/details/virtual.hbs +7 -1
- package/addon/utils/fleet-ops-options.js +33 -0
- package/addon/utils/serialize-payload.js +1 -1
- package/app/components/map/drawer/geofence-event-listing.js +1 -0
- package/app/controllers/operations/orders/index/details/virtual.js +1 -0
- package/app/helpers/f-to-int.js +1 -0
- package/app/services/geofence-event-bus.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +2 -2
- package/server/migrations/2026_04_05_000001_add_geofence_config_to_zones_table.php +48 -0
- package/server/migrations/2026_04_05_000002_add_geofence_config_to_service_areas_table.php +38 -0
- package/server/migrations/2026_04_05_000003_create_driver_geofence_states_table.php +62 -0
- package/server/migrations/2026_04_05_000004_create_geofence_events_log_table.php +66 -0
- package/server/migrations/2026_04_22_000001_add_vehicle_support_to_geofence_events_log.php +31 -0
- package/server/migrations/2026_04_22_000002_create_vehicle_geofence_states_table.php +39 -0
- package/server/src/Console/Commands/SimulateGeofenceEvents.php +278 -0
- package/server/src/Events/GeofenceDwelled.php +208 -0
- package/server/src/Events/GeofenceEntered.php +207 -0
- package/server/src/Events/GeofenceExited.php +213 -0
- package/server/src/Http/Controllers/Api/v1/DriverController.php +100 -0
- package/server/src/Http/Controllers/Api/v1/GeofenceController.php +263 -0
- package/server/src/Http/Controllers/Api/v1/OrderController.php +13 -8
- package/server/src/Http/Controllers/Api/v1/PurchaseRateController.php +11 -3
- package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -2
- package/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php +4 -0
- package/server/src/Http/Controllers/Api/v1/ServiceRateController.php +6 -1
- package/server/src/Http/Controllers/Api/v1/VehicleController.php +88 -0
- package/server/src/Http/Controllers/Api/v1/ZoneController.php +2 -2
- package/server/src/Http/Controllers/Internal/v1/GeofenceController.php +7 -0
- package/server/src/Http/Requests/CreateServiceAreaRequest.php +8 -4
- package/server/src/Http/Requests/CreateServiceRateRequest.php +2 -2
- package/server/src/Http/Requests/CreateZoneRequest.php +8 -4
- package/server/src/Http/Resources/v1/Order.php +0 -1
- package/server/src/Http/Resources/v1/ServiceQuote.php +2 -2
- package/server/src/Jobs/CheckGeofenceDwell.php +139 -0
- package/server/src/Listeners/HandleGeofenceDwelled.php +56 -0
- package/server/src/Listeners/HandleGeofenceEntered.php +187 -0
- package/server/src/Listeners/HandleGeofenceExited.php +56 -0
- package/server/src/Models/Entity.php +7 -0
- package/server/src/Models/GeofenceEventLog.php +155 -0
- package/server/src/Models/Order.php +39 -7
- package/server/src/Models/ServiceArea.php +21 -2
- package/server/src/Models/ServiceQuote.php +3 -1
- package/server/src/Models/ServiceRate.php +122 -30
- package/server/src/Models/Zone.php +20 -2
- package/server/src/Notifications/DriverArrivedAtGeofence.php +86 -0
- package/server/src/Observers/PurchaseRateObserver.php +3 -5
- package/server/src/Providers/EventServiceProvider.php +19 -0
- package/server/src/Providers/FleetOpsServiceProvider.php +10 -0
- package/server/src/Rules/ComputableAlgo.php +1 -3
- package/server/src/Support/Algo.php +87 -1
- package/server/src/Support/GeofenceIntersectionService.php +206 -0
- package/server/src/Support/Utils.php +35 -0
- package/server/src/routes.php +16 -0
|
@@ -41,10 +41,19 @@ class PurchaseRateController extends Controller
|
|
|
41
41
|
|
|
42
42
|
// order assignment
|
|
43
43
|
if ($request->has('order')) {
|
|
44
|
-
$
|
|
44
|
+
$orderUuid = Utils::getUuid('orders', [
|
|
45
45
|
'public_id' => $request->input('order'),
|
|
46
46
|
'company_uuid' => session('company'),
|
|
47
47
|
]);
|
|
48
|
+
|
|
49
|
+
$order = Order::where('uuid', $orderUuid)->first();
|
|
50
|
+
|
|
51
|
+
if ($order instanceof Order) {
|
|
52
|
+
$input['payload_uuid'] = $order->payload_uuid;
|
|
53
|
+
$input['customer_uuid'] = $order->customer_uuid;
|
|
54
|
+
$input['customer_type'] = $order->customer_type;
|
|
55
|
+
$input['company_uuid'] = $order->company_uuid ?? $input['company_uuid'];
|
|
56
|
+
}
|
|
48
57
|
} elseif ($createOrder) {
|
|
49
58
|
// create order from service quote
|
|
50
59
|
$serviceQuote = ServiceQuote::where('uuid', $input['service_quote_uuid'])->orWhere('public_id', $request->input('service_quote'))->first();
|
|
@@ -70,9 +79,8 @@ class PurchaseRateController extends Controller
|
|
|
70
79
|
// create the purchaseRate
|
|
71
80
|
$purchaseRate = PurchaseRate::create($input);
|
|
72
81
|
|
|
73
|
-
// set purchase rate to order
|
|
74
82
|
if ($order instanceof Order) {
|
|
75
|
-
$order->
|
|
83
|
+
$order->attachPurchaseRate($purchaseRate);
|
|
76
84
|
}
|
|
77
85
|
|
|
78
86
|
// response the driver resource
|
|
@@ -24,7 +24,7 @@ class ServiceAreaController extends Controller
|
|
|
24
24
|
public function create(CreateServiceAreaRequest $request)
|
|
25
25
|
{
|
|
26
26
|
// get request input
|
|
27
|
-
$input = $request->only(['name', 'type', 'status']);
|
|
27
|
+
$input = $request->only(['name', 'type', 'status', 'country', 'color', 'stroke_color', 'trigger_on_entry', 'trigger_on_exit', 'dwell_threshold_minutes', 'speed_limit_kmh']);
|
|
28
28
|
|
|
29
29
|
// get radius for creating service area border - default to 500 meters
|
|
30
30
|
$radius = (int) $request->input('radius', 500);
|
|
@@ -98,7 +98,7 @@ class ServiceAreaController extends Controller
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
// get request input
|
|
101
|
-
$input = $request->only(['name', 'type', 'status']);
|
|
101
|
+
$input = $request->only(['name', 'type', 'status', 'country', 'color', 'stroke_color', 'trigger_on_entry', 'trigger_on_exit', 'dwell_threshold_minutes', 'speed_limit_kmh']);
|
|
102
102
|
|
|
103
103
|
// get radius for creating service area border - default to 500 meters
|
|
104
104
|
$radius = $request->input('radius', 500);
|
|
@@ -98,6 +98,7 @@ class ServiceQuoteController extends Controller
|
|
|
98
98
|
'amount' => $subTotal,
|
|
99
99
|
'currency' => $serviceRate->currency,
|
|
100
100
|
]);
|
|
101
|
+
$quote->setRelation('serviceRate', $serviceRate);
|
|
101
102
|
|
|
102
103
|
$items = $lines->map(function ($line) use ($quote) {
|
|
103
104
|
return ServiceQuoteItem::create([
|
|
@@ -143,6 +144,7 @@ class ServiceQuoteController extends Controller
|
|
|
143
144
|
'amount' => $subTotal,
|
|
144
145
|
'currency' => $serviceRate->currency,
|
|
145
146
|
]);
|
|
147
|
+
$quote->setRelation('serviceRate', $serviceRate);
|
|
146
148
|
|
|
147
149
|
$items = $lines->map(function ($line) use ($quote) {
|
|
148
150
|
return ServiceQuoteItem::create([
|
|
@@ -288,6 +290,7 @@ class ServiceQuoteController extends Controller
|
|
|
288
290
|
'amount' => $subTotal,
|
|
289
291
|
'currency' => $serviceRate->currency,
|
|
290
292
|
]);
|
|
293
|
+
$quote->setRelation('serviceRate', $serviceRate);
|
|
291
294
|
|
|
292
295
|
// set preliminary data to meta
|
|
293
296
|
$quote->updateMeta('preliminary_data', $preliminaryData);
|
|
@@ -335,6 +338,7 @@ class ServiceQuoteController extends Controller
|
|
|
335
338
|
'amount' => $subTotal,
|
|
336
339
|
'currency' => $serviceRate->currency,
|
|
337
340
|
]);
|
|
341
|
+
$quote->setRelation('serviceRate', $serviceRate);
|
|
338
342
|
|
|
339
343
|
// set preliminary data to meta
|
|
340
344
|
$quote->updateMeta('preliminary_data', $preliminaryData);
|
|
@@ -30,6 +30,8 @@ class ServiceRateController extends Controller
|
|
|
30
30
|
'rate_calculation_method',
|
|
31
31
|
'currency',
|
|
32
32
|
'base_fee',
|
|
33
|
+
'max_distance_unit',
|
|
34
|
+
'max_distance',
|
|
33
35
|
'per_meter_unit',
|
|
34
36
|
'per_meter_flat_rate_fee',
|
|
35
37
|
'meter_fees',
|
|
@@ -119,7 +121,10 @@ class ServiceRateController extends Controller
|
|
|
119
121
|
'rate_calculation_method',
|
|
120
122
|
'currency',
|
|
121
123
|
'base_fee',
|
|
122
|
-
'
|
|
124
|
+
'max_distance_unit',
|
|
125
|
+
'max_distance',
|
|
126
|
+
'per_meter_unit',
|
|
127
|
+
'per_meter_flat_rate_fee',
|
|
123
128
|
'meter_fees',
|
|
124
129
|
'meter_fees.*.distance',
|
|
125
130
|
'meter_fees.*.fee',
|
|
@@ -2,17 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
namespace Fleetbase\FleetOps\Http\Controllers\Api\v1;
|
|
4
4
|
|
|
5
|
+
use Fleetbase\FleetOps\Events\GeofenceEntered;
|
|
6
|
+
use Fleetbase\FleetOps\Events\GeofenceExited;
|
|
5
7
|
use Fleetbase\FleetOps\Events\VehicleLocationChanged;
|
|
6
8
|
use Fleetbase\FleetOps\Http\Requests\CreateVehicleRequest;
|
|
7
9
|
use Fleetbase\FleetOps\Http\Requests\UpdateVehicleRequest;
|
|
8
10
|
use Fleetbase\FleetOps\Http\Resources\v1\DeletedResource;
|
|
9
11
|
use Fleetbase\FleetOps\Http\Resources\v1\Vehicle as VehicleResource;
|
|
12
|
+
use Fleetbase\FleetOps\Jobs\CheckGeofenceDwell;
|
|
10
13
|
use Fleetbase\FleetOps\Models\Driver;
|
|
11
14
|
use Fleetbase\FleetOps\Models\Vehicle;
|
|
15
|
+
use Fleetbase\FleetOps\Support\GeofenceIntersectionService;
|
|
12
16
|
use Fleetbase\FleetOps\Support\Utils;
|
|
13
17
|
use Fleetbase\Http\Controllers\Controller;
|
|
14
18
|
use Fleetbase\LaravelMysqlSpatial\Types\Point;
|
|
15
19
|
use Illuminate\Http\Request;
|
|
20
|
+
use Illuminate\Support\Carbon;
|
|
21
|
+
use Illuminate\Support\Facades\DB;
|
|
16
22
|
|
|
17
23
|
class VehicleController extends Controller
|
|
18
24
|
{
|
|
@@ -276,6 +282,88 @@ class VehicleController extends Controller
|
|
|
276
282
|
|
|
277
283
|
broadcast(new VehicleLocationChanged($vehicle));
|
|
278
284
|
|
|
285
|
+
try {
|
|
286
|
+
$newLocation = new Point($latitude, $longitude);
|
|
287
|
+
$geofenceService = app(GeofenceIntersectionService::class);
|
|
288
|
+
$this->processVehicleGeofenceCrossings($vehicle, $newLocation, $geofenceService->detectVehicleCrossings($vehicle, $newLocation));
|
|
289
|
+
} catch (\Throwable $geofenceException) {
|
|
290
|
+
if (app()->bound('sentry')) {
|
|
291
|
+
app('sentry')->captureException($geofenceException);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
279
295
|
return new VehicleResource($vehicle);
|
|
280
296
|
}
|
|
297
|
+
|
|
298
|
+
private function processVehicleGeofenceCrossings(Vehicle $vehicle, Point $newLocation, array $crossings): void
|
|
299
|
+
{
|
|
300
|
+
foreach ($crossings as $crossing) {
|
|
301
|
+
$geofence = $crossing['geofence'];
|
|
302
|
+
$geofenceType = $crossing['geofence_type'];
|
|
303
|
+
|
|
304
|
+
if ($crossing['type'] === 'entered') {
|
|
305
|
+
if (!$geofence->trigger_on_entry && empty($geofence->dwell_threshold_minutes)) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
DB::table('vehicle_geofence_states')->upsert(
|
|
310
|
+
[
|
|
311
|
+
'vehicle_uuid' => $vehicle->uuid,
|
|
312
|
+
'geofence_uuid' => $geofence->uuid,
|
|
313
|
+
'geofence_type' => $geofenceType,
|
|
314
|
+
'is_inside' => true,
|
|
315
|
+
'entered_at' => now(),
|
|
316
|
+
'exited_at' => null,
|
|
317
|
+
'dwell_job_id' => null,
|
|
318
|
+
'created_at' => now(),
|
|
319
|
+
'updated_at' => now(),
|
|
320
|
+
],
|
|
321
|
+
['vehicle_uuid', 'geofence_uuid'],
|
|
322
|
+
['is_inside', 'entered_at', 'exited_at', 'dwell_job_id', 'updated_at']
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
if ($geofence->trigger_on_entry) {
|
|
326
|
+
event(new GeofenceEntered($vehicle, $geofence, $geofenceType, $newLocation));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if ($geofence->dwell_threshold_minutes > 0) {
|
|
330
|
+
$dwellJob = CheckGeofenceDwell::dispatch(
|
|
331
|
+
$vehicle->uuid,
|
|
332
|
+
$geofence->uuid,
|
|
333
|
+
$geofenceType,
|
|
334
|
+
'vehicle'
|
|
335
|
+
)->delay(now()->addMinutes($geofence->dwell_threshold_minutes));
|
|
336
|
+
|
|
337
|
+
DB::table('vehicle_geofence_states')
|
|
338
|
+
->where('vehicle_uuid', $vehicle->uuid)
|
|
339
|
+
->where('geofence_uuid', $geofence->uuid)
|
|
340
|
+
->update(['dwell_job_id' => (string) $dwellJob]);
|
|
341
|
+
}
|
|
342
|
+
} elseif ($crossing['type'] === 'exited') {
|
|
343
|
+
$state = DB::table('vehicle_geofence_states')
|
|
344
|
+
->where('vehicle_uuid', $vehicle->uuid)
|
|
345
|
+
->where('geofence_uuid', $geofence->uuid)
|
|
346
|
+
->first();
|
|
347
|
+
|
|
348
|
+
$dwellMinutes = null;
|
|
349
|
+
if ($state && $state->entered_at) {
|
|
350
|
+
$dwellMinutes = (int) Carbon::parse($state->entered_at)->diffInMinutes(now());
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
DB::table('vehicle_geofence_states')
|
|
354
|
+
->where('vehicle_uuid', $vehicle->uuid)
|
|
355
|
+
->where('geofence_uuid', $geofence->uuid)
|
|
356
|
+
->update([
|
|
357
|
+
'is_inside' => false,
|
|
358
|
+
'exited_at' => now(),
|
|
359
|
+
'dwell_job_id' => null,
|
|
360
|
+
'updated_at' => now(),
|
|
361
|
+
]);
|
|
362
|
+
|
|
363
|
+
if ($geofence->trigger_on_exit) {
|
|
364
|
+
event(new GeofenceExited($vehicle, $geofence, $geofenceType, $newLocation, $dwellMinutes));
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
281
369
|
}
|
|
@@ -24,7 +24,7 @@ class ZoneController extends Controller
|
|
|
24
24
|
public function create(CreateZoneRequest $request)
|
|
25
25
|
{
|
|
26
26
|
// get request input
|
|
27
|
-
$input = $request->only(['name', 'border', 'status', 'description', 'color', 'stroke_color']);
|
|
27
|
+
$input = $request->only(['name', 'border', 'status', 'description', 'color', 'stroke_color', 'trigger_on_entry', 'trigger_on_exit', 'dwell_threshold_minutes', 'speed_limit_kmh']);
|
|
28
28
|
|
|
29
29
|
// get radius for creating zone border - default to 500 meters
|
|
30
30
|
$radius = $request->input('radius', 500);
|
|
@@ -97,7 +97,7 @@ class ZoneController extends Controller
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
// get request input
|
|
100
|
-
$input = $request->only(['name', 'border', 'status', 'description', 'color', 'stroke_color']);
|
|
100
|
+
$input = $request->only(['name', 'border', 'status', 'description', 'color', 'stroke_color', 'trigger_on_entry', 'trigger_on_exit', 'dwell_threshold_minutes', 'speed_limit_kmh']);
|
|
101
101
|
|
|
102
102
|
// get radius for creating zone border - default to 500 meters
|
|
103
103
|
$radius = $request->input('radius', 500);
|
|
@@ -46,10 +46,14 @@ class CreateServiceAreaRequest extends FleetbaseRequest
|
|
|
46
46
|
|
|
47
47
|
return false;
|
|
48
48
|
})],
|
|
49
|
-
'parent'
|
|
50
|
-
'location'
|
|
51
|
-
'latitude'
|
|
52
|
-
'longitude'
|
|
49
|
+
'parent' => ['nullable', 'exists:service_areas,public_id'],
|
|
50
|
+
'location' => ['nullable', new ResolvablePoint()],
|
|
51
|
+
'latitude' => ['nullable', 'required_with:longitude'],
|
|
52
|
+
'longitude' => ['nullable', 'required_with:latitude'],
|
|
53
|
+
'trigger_on_entry' => ['nullable', 'boolean'],
|
|
54
|
+
'trigger_on_exit' => ['nullable', 'boolean'],
|
|
55
|
+
'dwell_threshold_minutes' => ['nullable', 'integer', 'min:1', 'max:10080'],
|
|
56
|
+
'speed_limit_kmh' => ['nullable', 'integer', 'min:1', 'max:1000'],
|
|
53
57
|
];
|
|
54
58
|
}
|
|
55
59
|
}
|
|
@@ -31,10 +31,10 @@ class CreateServiceRateRequest extends FleetbaseRequest
|
|
|
31
31
|
'service_type' => [Rule::requiredIf($this->isMethod('POST')), 'string'],
|
|
32
32
|
'service_area' => [Rule::exists('service_areas', 'public_id')->whereNull('deleted_at')],
|
|
33
33
|
'zone' => [Rule::exists('zones', 'public_id')->whereNull('deleted_at')],
|
|
34
|
-
'rate_calculation_method' => [Rule::requiredIf($this->isMethod('POST')), 'string', 'in:fixed_meter,fixed_rate,per_meter,per_drop,algo'],
|
|
34
|
+
'rate_calculation_method' => [Rule::requiredIf($this->isMethod('POST')), 'string', 'in:fixed_meter,fixed_rate,per_meter,per_drop,algo,parcel'],
|
|
35
35
|
'currency' => ['required', 'size:3'],
|
|
36
36
|
'base_fee' => ['numeric'],
|
|
37
|
-
'per_meter_unit' => ['required_if:rate_calculation_method,per_meter', 'string', 'in:km,m'],
|
|
37
|
+
'per_meter_unit' => ['required_if:rate_calculation_method,per_meter', 'string', 'in:km,m,ft,yd,mi'],
|
|
38
38
|
'per_meter_flat_rate_fee' => ['required_if:rate_calculation_method,per_meter', 'numeric'],
|
|
39
39
|
'meter_fees' => [Rule::requiredIf(function () {
|
|
40
40
|
return in_array($this->input('rate_calculation_method'), ['fixed_meter', 'fixed_rate']);
|
|
@@ -45,10 +45,14 @@ class CreateZoneRequest extends FleetbaseRequest
|
|
|
45
45
|
|
|
46
46
|
return false;
|
|
47
47
|
})],
|
|
48
|
-
'location'
|
|
49
|
-
'latitude'
|
|
50
|
-
'longitude'
|
|
51
|
-
'status'
|
|
48
|
+
'location' => ['nullable', new ResolvablePoint()],
|
|
49
|
+
'latitude' => ['nullable', 'required_with:longitude'],
|
|
50
|
+
'longitude' => ['nullable', 'required_with:latitude'],
|
|
51
|
+
'status' => ['nullable', 'in:active,inactive'],
|
|
52
|
+
'trigger_on_entry' => ['nullable', 'boolean'],
|
|
53
|
+
'trigger_on_exit' => ['nullable', 'boolean'],
|
|
54
|
+
'dwell_threshold_minutes' => ['nullable', 'integer', 'min:1', 'max:10080'],
|
|
55
|
+
'speed_limit_kmh' => ['nullable', 'integer', 'min:1', 'max:1000'],
|
|
52
56
|
];
|
|
53
57
|
}
|
|
54
58
|
}
|
|
@@ -45,7 +45,6 @@ class Order extends FleetbaseResource
|
|
|
45
45
|
'tracking_number_uuid' => $this->when($isInternal, $this->tracking_number_uuid),
|
|
46
46
|
'driver_assigned_uuid' => $this->when($isInternal, $this->driver_assigned_uuid),
|
|
47
47
|
'vehicle_assigned_uuid'=> $this->when($isInternal, $this->vehicle_assigned_uuid),
|
|
48
|
-
'service_quote_uuid' => $this->when($isInternal, $this->service_quote_uuid),
|
|
49
48
|
'has_driver_assigned' => $this->when($isInternal, $this->has_driver_assigned),
|
|
50
49
|
'is_scheduled' => $this->when($isInternal, $this->is_scheduled),
|
|
51
50
|
'order_config_uuid' => $this->when($isInternal, $this->order_config_uuid),
|
|
@@ -23,8 +23,8 @@ class ServiceQuote extends FleetbaseResource
|
|
|
23
23
|
'public_id' => $this->when(Http::isInternalRequest(), $this->public_id),
|
|
24
24
|
'service_rate_uuid' => $this->when(Http::isInternalRequest(), $this->service_rate_uuid),
|
|
25
25
|
'payload_uuid' => $this->when(Http::isInternalRequest(), $this->payload_uuid),
|
|
26
|
-
'service_rate_name' => data_get($this, 'serviceRate.
|
|
27
|
-
'service_name' => data_get($this, 'serviceRate.
|
|
26
|
+
'service_rate_name' => data_get($this, 'serviceRate.service_name'),
|
|
27
|
+
'service_name' => data_get($this, 'serviceRate.service_name'),
|
|
28
28
|
'service_rate' => $this->when(Http::isPublicRequest(), data_get($this, 'serviceRate.public_id')),
|
|
29
29
|
'facilitator' => $this->when(Http::isPublicRequest(), data_get($this, 'integratedVendor.public_id')),
|
|
30
30
|
'items' => ServiceQuoteItem::collection($this->items),
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Jobs;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Events\GeofenceDwelled;
|
|
6
|
+
use Fleetbase\FleetOps\Models\Driver;
|
|
7
|
+
use Fleetbase\FleetOps\Models\ServiceArea;
|
|
8
|
+
use Fleetbase\FleetOps\Models\Vehicle;
|
|
9
|
+
use Fleetbase\FleetOps\Models\Zone;
|
|
10
|
+
use Illuminate\Bus\Queueable;
|
|
11
|
+
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
12
|
+
use Illuminate\Foundation\Bus\Dispatchable;
|
|
13
|
+
use Illuminate\Queue\InteractsWithQueue;
|
|
14
|
+
use Illuminate\Queue\SerializesModels;
|
|
15
|
+
use Illuminate\Support\Facades\DB;
|
|
16
|
+
use Illuminate\Support\Facades\Log;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* CheckGeofenceDwell.
|
|
20
|
+
*
|
|
21
|
+
* A delayed queue job dispatched when a driver enters a geofence that has
|
|
22
|
+
* a dwell_threshold_minutes configured. The job is delayed by that number
|
|
23
|
+
* of minutes. When it runs, it checks whether the driver is still inside
|
|
24
|
+
* the geofence. If so, it fires the GeofenceDwelled event.
|
|
25
|
+
*
|
|
26
|
+
* If the driver exits before the job runs, the dwell_job_id in the state
|
|
27
|
+
* table is cleared, but the job still runs and exits silently because the
|
|
28
|
+
* state record will show is_inside = false.
|
|
29
|
+
*
|
|
30
|
+
* This job runs on the dedicated 'geofence' queue and has tries = 1 because
|
|
31
|
+
* dwell checks should not be retried — a failed check is simply missed.
|
|
32
|
+
*/
|
|
33
|
+
class CheckGeofenceDwell implements ShouldQueue
|
|
34
|
+
{
|
|
35
|
+
use Dispatchable;
|
|
36
|
+
use InteractsWithQueue;
|
|
37
|
+
use Queueable;
|
|
38
|
+
use SerializesModels;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The number of times the job may be attempted.
|
|
42
|
+
* Dwell checks are fire-and-forget; retrying would produce false events.
|
|
43
|
+
*/
|
|
44
|
+
public int $tries = 1;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The number of seconds the job can run before timing out.
|
|
48
|
+
*/
|
|
49
|
+
public int $timeout = 30;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The UUID of the driver to check.
|
|
53
|
+
*/
|
|
54
|
+
protected string $driverUuid;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The UUID of the geofence to check.
|
|
58
|
+
*/
|
|
59
|
+
protected string $geofenceUuid;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The type of geofence: 'zone' or 'service_area'.
|
|
63
|
+
*/
|
|
64
|
+
protected string $geofenceType;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The subject type for the dwell check: 'driver' or 'vehicle'.
|
|
68
|
+
*/
|
|
69
|
+
protected string $subjectType;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Create a new CheckGeofenceDwell job.
|
|
73
|
+
*
|
|
74
|
+
* @param string $geofenceType 'zone' | 'service_area'
|
|
75
|
+
*/
|
|
76
|
+
public function __construct(string $subjectUuid, string $geofenceUuid, string $geofenceType, string $subjectType = 'driver')
|
|
77
|
+
{
|
|
78
|
+
$this->driverUuid = $subjectUuid;
|
|
79
|
+
$this->geofenceUuid = $geofenceUuid;
|
|
80
|
+
$this->geofenceType = $geofenceType;
|
|
81
|
+
$this->subjectType = $subjectType;
|
|
82
|
+
$this->onQueue('geofence');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Execute the job.
|
|
87
|
+
*/
|
|
88
|
+
public function handle(): void
|
|
89
|
+
{
|
|
90
|
+
// Check if the driver is still inside the geofence
|
|
91
|
+
$stateTable = $this->subjectType === 'vehicle' ? 'vehicle_geofence_states' : 'driver_geofence_states';
|
|
92
|
+
$subjectColumn = $this->subjectType === 'vehicle' ? 'vehicle_uuid' : 'driver_uuid';
|
|
93
|
+
|
|
94
|
+
$state = DB::table($stateTable)
|
|
95
|
+
->where($subjectColumn, $this->driverUuid)
|
|
96
|
+
->where('geofence_uuid', $this->geofenceUuid)
|
|
97
|
+
->where('is_inside', true)
|
|
98
|
+
->first();
|
|
99
|
+
|
|
100
|
+
if (!$state) {
|
|
101
|
+
// Driver has already exited; dwell event should not fire
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Load the driver
|
|
106
|
+
$subject = $this->subjectType === 'vehicle'
|
|
107
|
+
? Vehicle::where('uuid', $this->driverUuid)->withoutGlobalScopes()->first()
|
|
108
|
+
: Driver::where('uuid', $this->driverUuid)->withoutGlobalScopes()->first();
|
|
109
|
+
|
|
110
|
+
if (!$subject) {
|
|
111
|
+
Log::warning('CheckGeofenceDwell: Subject not found', [
|
|
112
|
+
'subject_uuid' => $this->driverUuid,
|
|
113
|
+
'subject_type' => $this->subjectType,
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Load the geofence model
|
|
120
|
+
$geofence = $this->geofenceType === 'service_area'
|
|
121
|
+
? ServiceArea::where('uuid', $this->geofenceUuid)->first()
|
|
122
|
+
: Zone::where('uuid', $this->geofenceUuid)->first();
|
|
123
|
+
|
|
124
|
+
if (!$geofence) {
|
|
125
|
+
Log::warning('CheckGeofenceDwell: Geofence not found', [
|
|
126
|
+
'geofence_uuid' => $this->geofenceUuid,
|
|
127
|
+
'geofence_type' => $this->geofenceType,
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Parse the entry timestamp
|
|
134
|
+
$enteredAt = \Carbon\Carbon::parse($state->entered_at);
|
|
135
|
+
|
|
136
|
+
// Fire the dwell event
|
|
137
|
+
event(new GeofenceDwelled($subject, $geofence, $this->geofenceType, $enteredAt));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Listeners;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Events\GeofenceDwelled;
|
|
6
|
+
use Fleetbase\FleetOps\Models\GeofenceEventLog;
|
|
7
|
+
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
8
|
+
use Illuminate\Queue\InteractsWithQueue;
|
|
9
|
+
use Illuminate\Support\Str;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* HandleGeofenceDwelled.
|
|
13
|
+
*
|
|
14
|
+
* Handles the business logic triggered when a driver has dwelled inside
|
|
15
|
+
* a geofence for the configured threshold duration:
|
|
16
|
+
* 1. Writes a record to the geofence_events_log table with the dwell duration.
|
|
17
|
+
*/
|
|
18
|
+
class HandleGeofenceDwelled implements ShouldQueue
|
|
19
|
+
{
|
|
20
|
+
use InteractsWithQueue;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The number of times the job may be attempted.
|
|
24
|
+
*/
|
|
25
|
+
public int $tries = 3;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Handle the event.
|
|
29
|
+
*/
|
|
30
|
+
public function handle(GeofenceDwelled $event): void
|
|
31
|
+
{
|
|
32
|
+
$driver = $event->driver;
|
|
33
|
+
$vehicle = $event->vehicle;
|
|
34
|
+
$order = $driver?->getCurrentOrder();
|
|
35
|
+
$subject = $event->subjectType === 'vehicle' ? $vehicle : $driver;
|
|
36
|
+
|
|
37
|
+
GeofenceEventLog::create([
|
|
38
|
+
'uuid' => Str::uuid()->toString(),
|
|
39
|
+
'company_uuid' => $event->getCompanyUuid(),
|
|
40
|
+
'driver_uuid' => $driver?->uuid,
|
|
41
|
+
'vehicle_uuid' => $vehicle?->uuid,
|
|
42
|
+
'order_uuid' => $order?->uuid,
|
|
43
|
+
'subject_uuid' => $subject?->uuid,
|
|
44
|
+
'subject_type' => $event->subjectType,
|
|
45
|
+
'subject_name' => $event->subjectType === 'vehicle' ? ($vehicle?->display_name ?? $vehicle?->plate_number) : $driver?->name,
|
|
46
|
+
'geofence_uuid' => $event->geofence->uuid,
|
|
47
|
+
'geofence_type' => $event->geofenceType,
|
|
48
|
+
'geofence_name' => $event->geofence->name,
|
|
49
|
+
'event_type' => 'dwelled',
|
|
50
|
+
'latitude' => null,
|
|
51
|
+
'longitude' => null,
|
|
52
|
+
'dwell_duration_minutes' => $event->dwellDurationMinutes,
|
|
53
|
+
'occurred_at' => $event->timestamp,
|
|
54
|
+
]);
|
|
55
|
+
}
|
|
56
|
+
}
|