@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.
Files changed (82) hide show
  1. package/addon/components/entity/form.hbs +24 -0
  2. package/addon/components/entity/form.js +30 -0
  3. package/addon/components/leaflet-draw-control.js +1 -1
  4. package/addon/components/map/drawer/geofence-event-listing.hbs +123 -0
  5. package/addon/components/map/drawer/geofence-event-listing.js +103 -0
  6. package/addon/components/map/drawer.js +1 -0
  7. package/addon/components/map/leaflet-live-map.js +83 -0
  8. package/addon/components/modals/orchestrator-import.js +2 -78
  9. package/addon/components/order/form/service-rate.hbs +1 -1
  10. package/addon/components/service-area/form.hbs +59 -0
  11. package/addon/components/service-rate/details.hbs +6 -6
  12. package/addon/components/service-rate/form.hbs +28 -15
  13. package/addon/components/zone/form.hbs +60 -1
  14. package/addon/controllers/operations/orders/index/details/virtual.js +5 -0
  15. package/addon/controllers/operations/orders/index/details.js +27 -2
  16. package/addon/controllers/operations/service-rates/index/edit.js +1 -0
  17. package/addon/controllers/operations/service-rates/index/new.js +1 -0
  18. package/addon/helpers/f-to-int.js +6 -0
  19. package/addon/routes/application.js +3 -3
  20. package/addon/routes/operations/orders/index/details/virtual.js +1 -0
  21. package/addon/routes/operations/orders/index/details.js +19 -3
  22. package/addon/services/equipment-actions.js +4 -6
  23. package/addon/services/geofence-event-bus.js +223 -0
  24. package/addon/services/leaflet-map-manager.js +4 -0
  25. package/addon/services/maintenance-actions.js +4 -6
  26. package/addon/services/part-actions.js +4 -6
  27. package/addon/services/service-rate-actions.js +8 -3
  28. package/addon/templates/operations/orders/index/details/virtual.hbs +7 -1
  29. package/addon/utils/fleet-ops-options.js +33 -0
  30. package/addon/utils/serialize-payload.js +1 -1
  31. package/app/components/map/drawer/geofence-event-listing.js +1 -0
  32. package/app/controllers/operations/orders/index/details/virtual.js +1 -0
  33. package/app/helpers/f-to-int.js +1 -0
  34. package/app/services/geofence-event-bus.js +1 -0
  35. package/composer.json +1 -1
  36. package/extension.json +1 -1
  37. package/package.json +2 -2
  38. package/server/migrations/2026_04_05_000001_add_geofence_config_to_zones_table.php +48 -0
  39. package/server/migrations/2026_04_05_000002_add_geofence_config_to_service_areas_table.php +38 -0
  40. package/server/migrations/2026_04_05_000003_create_driver_geofence_states_table.php +62 -0
  41. package/server/migrations/2026_04_05_000004_create_geofence_events_log_table.php +66 -0
  42. package/server/migrations/2026_04_22_000001_add_vehicle_support_to_geofence_events_log.php +31 -0
  43. package/server/migrations/2026_04_22_000002_create_vehicle_geofence_states_table.php +39 -0
  44. package/server/src/Console/Commands/SimulateGeofenceEvents.php +278 -0
  45. package/server/src/Events/GeofenceDwelled.php +208 -0
  46. package/server/src/Events/GeofenceEntered.php +207 -0
  47. package/server/src/Events/GeofenceExited.php +213 -0
  48. package/server/src/Http/Controllers/Api/v1/DriverController.php +100 -0
  49. package/server/src/Http/Controllers/Api/v1/GeofenceController.php +263 -0
  50. package/server/src/Http/Controllers/Api/v1/OrderController.php +13 -8
  51. package/server/src/Http/Controllers/Api/v1/PurchaseRateController.php +11 -3
  52. package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -2
  53. package/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php +4 -0
  54. package/server/src/Http/Controllers/Api/v1/ServiceRateController.php +6 -1
  55. package/server/src/Http/Controllers/Api/v1/VehicleController.php +88 -0
  56. package/server/src/Http/Controllers/Api/v1/ZoneController.php +2 -2
  57. package/server/src/Http/Controllers/Internal/v1/GeofenceController.php +7 -0
  58. package/server/src/Http/Requests/CreateServiceAreaRequest.php +8 -4
  59. package/server/src/Http/Requests/CreateServiceRateRequest.php +2 -2
  60. package/server/src/Http/Requests/CreateZoneRequest.php +8 -4
  61. package/server/src/Http/Resources/v1/Order.php +0 -1
  62. package/server/src/Http/Resources/v1/ServiceQuote.php +2 -2
  63. package/server/src/Jobs/CheckGeofenceDwell.php +139 -0
  64. package/server/src/Listeners/HandleGeofenceDwelled.php +56 -0
  65. package/server/src/Listeners/HandleGeofenceEntered.php +187 -0
  66. package/server/src/Listeners/HandleGeofenceExited.php +56 -0
  67. package/server/src/Models/Entity.php +7 -0
  68. package/server/src/Models/GeofenceEventLog.php +155 -0
  69. package/server/src/Models/Order.php +39 -7
  70. package/server/src/Models/ServiceArea.php +21 -2
  71. package/server/src/Models/ServiceQuote.php +3 -1
  72. package/server/src/Models/ServiceRate.php +122 -30
  73. package/server/src/Models/Zone.php +20 -2
  74. package/server/src/Notifications/DriverArrivedAtGeofence.php +86 -0
  75. package/server/src/Observers/PurchaseRateObserver.php +3 -5
  76. package/server/src/Providers/EventServiceProvider.php +19 -0
  77. package/server/src/Providers/FleetOpsServiceProvider.php +10 -0
  78. package/server/src/Rules/ComputableAlgo.php +1 -3
  79. package/server/src/Support/Algo.php +87 -1
  80. package/server/src/Support/GeofenceIntersectionService.php +206 -0
  81. package/server/src/Support/Utils.php +35 -0
  82. 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
- $input['order_uuid'] = Utils::getUuid('orders', [
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->update(['purchase_rate_uuid' => $purchaseRate->uuid]);
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
- 'per_km_flat_rate_fee',
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);
@@ -0,0 +1,7 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1;
4
+
5
+ class GeofenceController extends \Fleetbase\FleetOps\Http\Controllers\Api\v1\GeofenceController
6
+ {
7
+ }
@@ -46,10 +46,14 @@ class CreateServiceAreaRequest extends FleetbaseRequest
46
46
 
47
47
  return false;
48
48
  })],
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'],
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' => ['nullable', new ResolvablePoint()],
49
- 'latitude' => ['nullable', 'required_with:longitude'],
50
- 'longitude' => ['nullable', 'required_with:latitude'],
51
- 'status' => ['nullable', 'in:active,inactive'],
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.name'),
27
- 'service_name' => data_get($this, 'serviceRate.name'),
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
+ }