@fleetbase/fleetops-engine 0.6.42 → 0.6.44
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/map/drawer/geofence-event-listing.hbs +123 -0
- package/addon/components/map/drawer/geofence-event-listing.js +100 -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/service-area/form.hbs +59 -0
- package/addon/components/zone/form.hbs +60 -1
- package/addon/services/geofence-event-bus.js +223 -0
- package/app/components/{modals/set-password.js → map/drawer/geofence-event-listing.js} +1 -1
- 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_08_000001_add_orchestrator_columns_to_vehicles_table.php +3 -0
- package/server/migrations/2026_04_08_000004_add_orchestrator_columns_to_payloads_table.php +3 -0
- package/server/migrations/2026_04_14_000001_drop_redundant_capacity_columns_from_payloads_table.php +2 -0
- package/server/migrations/2026_04_14_000002_rename_capacity_columns_on_vehicles_table.php +2 -0
- package/server/migrations/2026_04_21_081353_add_internal_id_column_to_vehicles_table.php +27 -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 +8 -3
- package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -2
- 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/CreateZoneRequest.php +8 -4
- 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/GeofenceEventLog.php +155 -0
- package/server/src/Models/ServiceArea.php +21 -2
- package/server/src/Models/ServiceQuote.php +3 -1
- package/server/src/Models/Zone.php +20 -2
- package/server/src/Notifications/DriverArrivedAtGeofence.php +86 -0
- package/server/src/Orchestration/Support/OrchestrationPayloadBuilder.php +2 -2
- package/server/src/Providers/EventServiceProvider.php +19 -0
- package/server/src/Providers/FleetOpsServiceProvider.php +10 -0
- package/server/src/Support/GeofenceIntersectionService.php +206 -0
- package/server/src/routes.php +16 -0
- package/addon/components/modals/set-password.hbs +0 -27
|
@@ -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
|
}
|
|
@@ -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
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Listeners;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Events\GeofenceEntered;
|
|
6
|
+
use Fleetbase\FleetOps\Models\GeofenceEventLog;
|
|
7
|
+
use Fleetbase\FleetOps\Models\Order;
|
|
8
|
+
use Fleetbase\FleetOps\Notifications\DriverArrivedAtGeofence;
|
|
9
|
+
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
10
|
+
use Illuminate\Queue\InteractsWithQueue;
|
|
11
|
+
use Illuminate\Support\Facades\Log;
|
|
12
|
+
use Illuminate\Support\Str;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* HandleGeofenceEntered.
|
|
16
|
+
*
|
|
17
|
+
* Handles the business logic triggered when a driver enters a geofence:
|
|
18
|
+
* 1. Writes a record to the geofence_events_log table.
|
|
19
|
+
* 2. Checks if the entered geofence corresponds to the driver's current
|
|
20
|
+
* order destination and auto-transitions the order to "arrived" status.
|
|
21
|
+
* 3. Sends a customer notification if the order destination matches.
|
|
22
|
+
*/
|
|
23
|
+
class HandleGeofenceEntered implements ShouldQueue
|
|
24
|
+
{
|
|
25
|
+
use InteractsWithQueue;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The number of times the job may be attempted.
|
|
29
|
+
*/
|
|
30
|
+
public int $tries = 3;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Handle the event.
|
|
34
|
+
*/
|
|
35
|
+
public function handle(GeofenceEntered $event): void
|
|
36
|
+
{
|
|
37
|
+
$driver = $event->driver;
|
|
38
|
+
$vehicle = $event->vehicle;
|
|
39
|
+
$geofence = $event->geofence;
|
|
40
|
+
|
|
41
|
+
// Set company session context for any subsequent queries
|
|
42
|
+
session(['company' => $event->getCompanyUuid()]);
|
|
43
|
+
|
|
44
|
+
// ----------------------------------------------------------------
|
|
45
|
+
// 1. Write to the geofence event log
|
|
46
|
+
// ----------------------------------------------------------------
|
|
47
|
+
$order = $driver?->getCurrentOrder();
|
|
48
|
+
$subject = $event->subjectType === 'vehicle' ? $vehicle : $driver;
|
|
49
|
+
|
|
50
|
+
GeofenceEventLog::create([
|
|
51
|
+
'uuid' => Str::uuid()->toString(),
|
|
52
|
+
'company_uuid' => $event->getCompanyUuid(),
|
|
53
|
+
'driver_uuid' => $driver?->uuid,
|
|
54
|
+
'vehicle_uuid' => $vehicle?->uuid,
|
|
55
|
+
'order_uuid' => $order?->uuid,
|
|
56
|
+
'subject_uuid' => $subject?->uuid,
|
|
57
|
+
'subject_type' => $event->subjectType,
|
|
58
|
+
'subject_name' => $event->subjectType === 'vehicle' ? ($vehicle?->display_name ?? $vehicle?->plate_number) : $driver?->name,
|
|
59
|
+
'geofence_uuid' => $geofence->uuid,
|
|
60
|
+
'geofence_type' => $event->geofenceType,
|
|
61
|
+
'geofence_name' => $geofence->name,
|
|
62
|
+
'event_type' => 'entered',
|
|
63
|
+
'latitude' => $event->location->getLat(),
|
|
64
|
+
'longitude' => $event->location->getLng(),
|
|
65
|
+
'occurred_at' => $event->timestamp,
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
// ----------------------------------------------------------------
|
|
69
|
+
// 2. Order status automation
|
|
70
|
+
//
|
|
71
|
+
// If the driver has an active order and the geofence is in
|
|
72
|
+
// proximity to the current destination waypoint, auto-transition
|
|
73
|
+
// the order to "arrived" status and notify the customer.
|
|
74
|
+
// ----------------------------------------------------------------
|
|
75
|
+
if ($driver && $order) {
|
|
76
|
+
$this->handleOrderArrival($driver, $geofence, $order, $event);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Attempt to auto-transition an order to "arrived" status when the
|
|
82
|
+
* driver enters a geofence near the order's current destination.
|
|
83
|
+
*/
|
|
84
|
+
private function handleOrderArrival($driver, $geofence, Order $order, GeofenceEntered $event): void
|
|
85
|
+
{
|
|
86
|
+
// Do not re-trigger if the order is already arrived or completed
|
|
87
|
+
if (in_array($order->status, ['arrived', 'completed', 'canceled'])) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Get the current destination waypoint
|
|
92
|
+
$destination = null;
|
|
93
|
+
try {
|
|
94
|
+
$destination = $order->payload?->getPickupOrCurrentWaypoint();
|
|
95
|
+
} catch (\Throwable $e) {
|
|
96
|
+
Log::warning('GeofenceEntered: Could not resolve order destination', [
|
|
97
|
+
'order_uuid' => $order->uuid,
|
|
98
|
+
'driver_uuid' => $driver->uuid,
|
|
99
|
+
'error' => $e->getMessage(),
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!$destination) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Resolve the destination place
|
|
110
|
+
$place = null;
|
|
111
|
+
try {
|
|
112
|
+
$place = $destination->place ?? $destination->getPlace();
|
|
113
|
+
} catch (\Throwable $e) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!$place || !$place->location) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Check proximity: is the geofence centroid within 500m of the destination place?
|
|
122
|
+
try {
|
|
123
|
+
$geofenceLat = $geofence->getLatitudeAttribute();
|
|
124
|
+
$geofenceLng = $geofence->getLongitudeAttribute();
|
|
125
|
+
} catch (\Throwable $e) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
$placeLat = $place->location->getLat();
|
|
130
|
+
$placeLng = $place->location->getLng();
|
|
131
|
+
|
|
132
|
+
$distanceMeters = $this->haversineDistance($geofenceLat, $geofenceLng, $placeLat, $placeLng);
|
|
133
|
+
|
|
134
|
+
// Only trigger if the geofence is within 500m of the destination
|
|
135
|
+
if ($distanceMeters > 500) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Auto-transition order to "arrived"
|
|
140
|
+
try {
|
|
141
|
+
$order->setStatus('arrived');
|
|
142
|
+
$order->createActivity(
|
|
143
|
+
[
|
|
144
|
+
'status' => 'arrived',
|
|
145
|
+
'details' => sprintf('Driver entered destination geofence "%s".', $geofence->name),
|
|
146
|
+
],
|
|
147
|
+
$event->location
|
|
148
|
+
);
|
|
149
|
+
} catch (\Throwable $e) {
|
|
150
|
+
Log::error('GeofenceEntered: Failed to set order status to arrived', [
|
|
151
|
+
'order_uuid' => $order->uuid,
|
|
152
|
+
'error' => $e->getMessage(),
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Notify the customer
|
|
159
|
+
if ($order->customer) {
|
|
160
|
+
try {
|
|
161
|
+
$order->customer->notify(new DriverArrivedAtGeofence($order, $geofence));
|
|
162
|
+
} catch (\Throwable $e) {
|
|
163
|
+
// Notification failure must not interrupt the geofence pipeline
|
|
164
|
+
Log::warning('GeofenceEntered: Failed to notify customer', [
|
|
165
|
+
'order_uuid' => $order->uuid,
|
|
166
|
+
'error' => $e->getMessage(),
|
|
167
|
+
]);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Calculate the haversine distance in metres between two lat/lng points.
|
|
174
|
+
*
|
|
175
|
+
* @return float Distance in metres
|
|
176
|
+
*/
|
|
177
|
+
private function haversineDistance(float $lat1, float $lng1, float $lat2, float $lng2): float
|
|
178
|
+
{
|
|
179
|
+
$earthRadius = 6371000; // metres
|
|
180
|
+
$dLat = deg2rad($lat2 - $lat1);
|
|
181
|
+
$dLng = deg2rad($lng2 - $lng1);
|
|
182
|
+
$a = sin($dLat / 2) ** 2
|
|
183
|
+
+ cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLng / 2) ** 2;
|
|
184
|
+
|
|
185
|
+
return $earthRadius * 2 * atan2(sqrt($a), sqrt(1 - $a));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Listeners;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Events\GeofenceExited;
|
|
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
|
+
* HandleGeofenceExited.
|
|
13
|
+
*
|
|
14
|
+
* Handles the business logic triggered when a driver exits a geofence:
|
|
15
|
+
* 1. Writes a record to the geofence_events_log table, including the
|
|
16
|
+
* dwell duration calculated from the entry timestamp.
|
|
17
|
+
*/
|
|
18
|
+
class HandleGeofenceExited 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(GeofenceExited $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' => 'exited',
|
|
50
|
+
'latitude' => $event->location->getLat(),
|
|
51
|
+
'longitude' => $event->location->getLng(),
|
|
52
|
+
'dwell_duration_minutes' => $event->dwellDurationMinutes,
|
|
53
|
+
'occurred_at' => $event->timestamp,
|
|
54
|
+
]);
|
|
55
|
+
}
|
|
56
|
+
}
|