@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
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Console\Commands;
|
|
4
|
+
|
|
5
|
+
use Carbon\Carbon;
|
|
6
|
+
use Fleetbase\FleetOps\Events\GeofenceDwelled;
|
|
7
|
+
use Fleetbase\FleetOps\Events\GeofenceEntered;
|
|
8
|
+
use Fleetbase\FleetOps\Events\GeofenceExited;
|
|
9
|
+
use Fleetbase\FleetOps\Models\Driver;
|
|
10
|
+
use Fleetbase\FleetOps\Models\ServiceArea;
|
|
11
|
+
use Fleetbase\FleetOps\Models\Vehicle;
|
|
12
|
+
use Fleetbase\FleetOps\Models\Zone;
|
|
13
|
+
use Fleetbase\LaravelMysqlSpatial\Types\Point;
|
|
14
|
+
use Illuminate\Console\Command;
|
|
15
|
+
use Illuminate\Support\Facades\DB;
|
|
16
|
+
use Illuminate\Support\Str;
|
|
17
|
+
|
|
18
|
+
class SimulateGeofenceEvents extends Command
|
|
19
|
+
{
|
|
20
|
+
/**
|
|
21
|
+
* The name and signature of the console command.
|
|
22
|
+
*
|
|
23
|
+
* @var string
|
|
24
|
+
*/
|
|
25
|
+
protected $signature = 'fleetops:simulate-geofence
|
|
26
|
+
{subject : Subject public_id or uuid}
|
|
27
|
+
{geofence : Geofence public_id or uuid}
|
|
28
|
+
{events=sequence : entered|exited|dwelled|sequence or comma-separated list}
|
|
29
|
+
{--repeat=1 : Number of times to repeat the event sequence}
|
|
30
|
+
{--sleep=0 : Seconds to sleep between events}
|
|
31
|
+
{--dwell-minutes=10 : Minutes to use for simulated dwell timing}
|
|
32
|
+
{--reset-state : Reset the geofence state before running}
|
|
33
|
+
{--no-log : Skip dispatching Laravel geofence events, update state only}';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The console command description.
|
|
37
|
+
*
|
|
38
|
+
* @var string
|
|
39
|
+
*/
|
|
40
|
+
protected $description = 'Simulate geofence events for a driver or vehicle against a zone or service area';
|
|
41
|
+
|
|
42
|
+
public function handle(): int
|
|
43
|
+
{
|
|
44
|
+
$events = $this->parseEvents($this->argument('events'));
|
|
45
|
+
$repeat = max(1, (int) $this->option('repeat'));
|
|
46
|
+
$sleep = max(0, (int) $this->option('sleep'));
|
|
47
|
+
$dwellMinutes = max(1, (int) $this->option('dwell-minutes'));
|
|
48
|
+
$dispatchEvent = !$this->option('no-log');
|
|
49
|
+
$subjectId = $this->argument('subject');
|
|
50
|
+
$geofenceId = $this->argument('geofence');
|
|
51
|
+
|
|
52
|
+
if (empty($events)) {
|
|
53
|
+
$this->error('Invalid arguments. Events must be entered|exited|dwelled|sequence or a comma-separated combination.');
|
|
54
|
+
|
|
55
|
+
return Command::FAILURE;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
[$subjectType, $subject] = $this->resolveSubject($subjectId);
|
|
59
|
+
[$geofenceType, $geofence] = $this->resolveGeofence($geofenceId);
|
|
60
|
+
|
|
61
|
+
if (!$subject) {
|
|
62
|
+
$this->error("Unable to resolve subject [{$subjectId}]. Expected a `driver_` or `vehicle_` public ID, or a UUID matching one of those resources.");
|
|
63
|
+
|
|
64
|
+
return Command::FAILURE;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!$geofence) {
|
|
68
|
+
$this->error("Unable to resolve geofence [{$geofenceId}]. Expected a `zone_` or `sa_` public ID, or a UUID matching one of those resources.");
|
|
69
|
+
|
|
70
|
+
return Command::FAILURE;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
session(['company' => $subject->company_uuid]);
|
|
74
|
+
$location = new Point($geofence->getLatitudeAttribute(), $geofence->getLongitudeAttribute());
|
|
75
|
+
|
|
76
|
+
if ($this->option('reset-state')) {
|
|
77
|
+
$this->resetState($subjectType, $subject, $geofence);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
$this->info(sprintf(
|
|
81
|
+
'Testing geofence events: %s [%s] -> %s [%s] | events=%s | repeat=%d',
|
|
82
|
+
$subjectType,
|
|
83
|
+
$subject->public_id ?? $subject->uuid,
|
|
84
|
+
$geofenceType,
|
|
85
|
+
$geofence->public_id ?? $geofence->uuid,
|
|
86
|
+
implode(',', $events),
|
|
87
|
+
$repeat
|
|
88
|
+
));
|
|
89
|
+
|
|
90
|
+
for ($iteration = 1; $iteration <= $repeat; $iteration++) {
|
|
91
|
+
$this->line("Run {$iteration}/{$repeat}");
|
|
92
|
+
|
|
93
|
+
foreach ($events as $eventName) {
|
|
94
|
+
$enteredAt = Carbon::now()->subMinutes($dwellMinutes);
|
|
95
|
+
|
|
96
|
+
switch ($eventName) {
|
|
97
|
+
case 'entered':
|
|
98
|
+
$this->markInside($subjectType, $subject, $geofence, Carbon::now());
|
|
99
|
+
if ($dispatchEvent) {
|
|
100
|
+
event(new GeofenceEntered($subject, $geofence, $geofenceType, $location));
|
|
101
|
+
}
|
|
102
|
+
$this->info(' -> dispatched geofence.entered');
|
|
103
|
+
break;
|
|
104
|
+
|
|
105
|
+
case 'dwelled':
|
|
106
|
+
$this->markInside($subjectType, $subject, $geofence, $enteredAt);
|
|
107
|
+
if ($dispatchEvent) {
|
|
108
|
+
event(new GeofenceDwelled($subject, $geofence, $geofenceType, $enteredAt));
|
|
109
|
+
}
|
|
110
|
+
$this->info(" -> dispatched geofence.dwelled ({$dwellMinutes} min)");
|
|
111
|
+
break;
|
|
112
|
+
|
|
113
|
+
case 'exited':
|
|
114
|
+
$this->ensureEnteredAt($subjectType, $subject, $geofence, $enteredAt);
|
|
115
|
+
$dwellDuration = $this->calculateDwellMinutes($subjectType, $subject, $geofence, $dwellMinutes);
|
|
116
|
+
$this->markOutside($subjectType, $subject, $geofence);
|
|
117
|
+
if ($dispatchEvent) {
|
|
118
|
+
event(new GeofenceExited($subject, $geofence, $geofenceType, $location, $dwellDuration));
|
|
119
|
+
}
|
|
120
|
+
$this->info(" -> dispatched geofence.exited ({$dwellDuration} min)");
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if ($sleep > 0) {
|
|
125
|
+
sleep($sleep);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
$this->newLine();
|
|
131
|
+
$this->info('Geofence simulation complete.');
|
|
132
|
+
|
|
133
|
+
return Command::SUCCESS;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
protected function parseEvents(string $events): array
|
|
137
|
+
{
|
|
138
|
+
$events = Str::lower(trim($events));
|
|
139
|
+
|
|
140
|
+
if ($events === 'sequence') {
|
|
141
|
+
return ['entered', 'dwelled', 'exited'];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
$parsed = collect(explode(',', $events))
|
|
145
|
+
->map(fn ($event) => trim($event))
|
|
146
|
+
->filter(fn ($event) => in_array($event, ['entered', 'exited', 'dwelled'], true))
|
|
147
|
+
->values()
|
|
148
|
+
->all();
|
|
149
|
+
|
|
150
|
+
return $parsed;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
protected function resolveSubject(string $identifier): array
|
|
154
|
+
{
|
|
155
|
+
if (Str::startsWith($identifier, 'driver_')) {
|
|
156
|
+
return ['driver', Driver::withoutGlobalScopes()->where('public_id', $identifier)->first()];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (Str::startsWith($identifier, 'vehicle_')) {
|
|
160
|
+
return ['vehicle', Vehicle::withoutGlobalScopes()->where('public_id', $identifier)->first()];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (Str::isUuid($identifier)) {
|
|
164
|
+
$driver = Driver::withoutGlobalScopes()->where('uuid', $identifier)->first();
|
|
165
|
+
if ($driver) {
|
|
166
|
+
return ['driver', $driver];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
$vehicle = Vehicle::withoutGlobalScopes()->where('uuid', $identifier)->first();
|
|
170
|
+
if ($vehicle) {
|
|
171
|
+
return ['vehicle', $vehicle];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return [null, null];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
protected function resolveGeofence(string $identifier): array
|
|
179
|
+
{
|
|
180
|
+
if (Str::startsWith($identifier, 'zone_')) {
|
|
181
|
+
return ['zone', Zone::withoutGlobalScopes()->where('public_id', $identifier)->first()];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (Str::startsWith($identifier, 'sa_')) {
|
|
185
|
+
return ['service_area', ServiceArea::withoutGlobalScopes()->where('public_id', $identifier)->first()];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (Str::isUuid($identifier)) {
|
|
189
|
+
$zone = Zone::withoutGlobalScopes()->where('uuid', $identifier)->first();
|
|
190
|
+
if ($zone) {
|
|
191
|
+
return ['zone', $zone];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
$serviceArea = ServiceArea::withoutGlobalScopes()->where('uuid', $identifier)->first();
|
|
195
|
+
if ($serviceArea) {
|
|
196
|
+
return ['service_area', $serviceArea];
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return [null, null];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
protected function stateTable(string $subjectType): string
|
|
204
|
+
{
|
|
205
|
+
return $subjectType === 'vehicle' ? 'vehicle_geofence_states' : 'driver_geofence_states';
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
protected function subjectColumn(string $subjectType): string
|
|
209
|
+
{
|
|
210
|
+
return $subjectType === 'vehicle' ? 'vehicle_uuid' : 'driver_uuid';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
protected function resetState(string $subjectType, Driver|Vehicle $subject, Zone|ServiceArea $geofence): void
|
|
214
|
+
{
|
|
215
|
+
DB::table($this->stateTable($subjectType))
|
|
216
|
+
->where($this->subjectColumn($subjectType), $subject->uuid)
|
|
217
|
+
->where('geofence_uuid', $geofence->uuid)
|
|
218
|
+
->delete();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
protected function markInside(string $subjectType, Driver|Vehicle $subject, Zone|ServiceArea $geofence, Carbon $enteredAt): void
|
|
222
|
+
{
|
|
223
|
+
DB::table($this->stateTable($subjectType))->upsert(
|
|
224
|
+
[
|
|
225
|
+
$this->subjectColumn($subjectType) => $subject->uuid,
|
|
226
|
+
'geofence_uuid' => $geofence->uuid,
|
|
227
|
+
'geofence_type' => $geofence instanceof ServiceArea ? 'service_area' : 'zone',
|
|
228
|
+
'is_inside' => true,
|
|
229
|
+
'entered_at' => $enteredAt,
|
|
230
|
+
'exited_at' => null,
|
|
231
|
+
'dwell_job_id' => null,
|
|
232
|
+
'created_at' => now(),
|
|
233
|
+
'updated_at' => now(),
|
|
234
|
+
],
|
|
235
|
+
[$this->subjectColumn($subjectType), 'geofence_uuid'],
|
|
236
|
+
['is_inside', 'entered_at', 'exited_at', 'dwell_job_id', 'updated_at']
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
protected function ensureEnteredAt(string $subjectType, Driver|Vehicle $subject, Zone|ServiceArea $geofence, Carbon $enteredAt): void
|
|
241
|
+
{
|
|
242
|
+
$state = DB::table($this->stateTable($subjectType))
|
|
243
|
+
->where($this->subjectColumn($subjectType), $subject->uuid)
|
|
244
|
+
->where('geofence_uuid', $geofence->uuid)
|
|
245
|
+
->first();
|
|
246
|
+
|
|
247
|
+
if (!$state || !$state->entered_at) {
|
|
248
|
+
$this->markInside($subjectType, $subject, $geofence, $enteredAt);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
protected function markOutside(string $subjectType, Driver|Vehicle $subject, Zone|ServiceArea $geofence): void
|
|
253
|
+
{
|
|
254
|
+
DB::table($this->stateTable($subjectType))
|
|
255
|
+
->where($this->subjectColumn($subjectType), $subject->uuid)
|
|
256
|
+
->where('geofence_uuid', $geofence->uuid)
|
|
257
|
+
->update([
|
|
258
|
+
'is_inside' => false,
|
|
259
|
+
'exited_at' => now(),
|
|
260
|
+
'dwell_job_id' => null,
|
|
261
|
+
'updated_at' => now(),
|
|
262
|
+
]);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
protected function calculateDwellMinutes(string $subjectType, Driver|Vehicle $subject, Zone|ServiceArea $geofence, int $fallback): int
|
|
266
|
+
{
|
|
267
|
+
$state = DB::table($this->stateTable($subjectType))
|
|
268
|
+
->where($this->subjectColumn($subjectType), $subject->uuid)
|
|
269
|
+
->where('geofence_uuid', $geofence->uuid)
|
|
270
|
+
->first();
|
|
271
|
+
|
|
272
|
+
if (!$state || !$state->entered_at) {
|
|
273
|
+
return $fallback;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return max(1, Carbon::parse($state->entered_at)->diffInMinutes(now()));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Events;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Models\Driver;
|
|
6
|
+
use Fleetbase\FleetOps\Models\Vehicle;
|
|
7
|
+
use Illuminate\Broadcasting\Channel;
|
|
8
|
+
use Illuminate\Broadcasting\InteractsWithSockets;
|
|
9
|
+
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
|
10
|
+
use Illuminate\Foundation\Events\Dispatchable;
|
|
11
|
+
use Illuminate\Queue\SerializesModels;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* GeofenceDwelled.
|
|
15
|
+
*
|
|
16
|
+
* Fired by the CheckGeofenceDwell queue job when a driver has remained
|
|
17
|
+
* inside a geofence for at least the configured dwell_threshold_minutes.
|
|
18
|
+
*
|
|
19
|
+
* Listeners:
|
|
20
|
+
* - HandleGeofenceDwelled (event log)
|
|
21
|
+
* - SendResourceLifecycleWebhook (webhook delivery)
|
|
22
|
+
*/
|
|
23
|
+
class GeofenceDwelled implements ShouldBroadcast
|
|
24
|
+
{
|
|
25
|
+
use Dispatchable;
|
|
26
|
+
use InteractsWithSockets;
|
|
27
|
+
use SerializesModels;
|
|
28
|
+
|
|
29
|
+
public string $modelHumanName = 'Geofence';
|
|
30
|
+
public ?string $modelRecordName = null;
|
|
31
|
+
public string $eventName = 'dwelled';
|
|
32
|
+
public $userSession;
|
|
33
|
+
public $companySession;
|
|
34
|
+
public $requestMethod;
|
|
35
|
+
public $apiCredential;
|
|
36
|
+
public $apiSecret;
|
|
37
|
+
public $apiKey;
|
|
38
|
+
public $apiEnvironment;
|
|
39
|
+
public $isSandbox;
|
|
40
|
+
public array $data = [];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The driver who has been dwelling inside the geofence.
|
|
44
|
+
*/
|
|
45
|
+
public ?Driver $driver = null;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The vehicle associated with the geofence event, if any.
|
|
49
|
+
*/
|
|
50
|
+
public ?Vehicle $vehicle = null;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The geofence where the driver is dwelling (Zone or ServiceArea).
|
|
54
|
+
*
|
|
55
|
+
* @var \Fleetbase\FleetOps\Models\Zone|\Fleetbase\FleetOps\Models\ServiceArea
|
|
56
|
+
*/
|
|
57
|
+
public $geofence;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The type of geofence: 'zone' or 'service_area'.
|
|
61
|
+
*/
|
|
62
|
+
public string $geofenceType;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The timestamp when the driver entered the geofence.
|
|
66
|
+
*/
|
|
67
|
+
public \Carbon\Carbon $enteredAt;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The number of minutes the driver has been inside the geofence.
|
|
71
|
+
*/
|
|
72
|
+
public int $dwellDurationMinutes;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The timestamp when this dwell event was fired.
|
|
76
|
+
*/
|
|
77
|
+
public \Carbon\Carbon $timestamp;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The subject that triggered the event: 'driver' or 'vehicle'.
|
|
81
|
+
*/
|
|
82
|
+
public string $subjectType;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Create a new GeofenceDwelled event.
|
|
86
|
+
*
|
|
87
|
+
* @param mixed $geofence Zone or ServiceArea
|
|
88
|
+
* @param string $geofenceType 'zone' | 'service_area'
|
|
89
|
+
*/
|
|
90
|
+
public function __construct(Driver|Vehicle $subject, $geofence, string $geofenceType, \Carbon\Carbon $enteredAt)
|
|
91
|
+
{
|
|
92
|
+
if ($subject instanceof Driver) {
|
|
93
|
+
$this->driver = $subject;
|
|
94
|
+
$this->vehicle = $subject->vehicle;
|
|
95
|
+
$this->subjectType = 'driver';
|
|
96
|
+
} else {
|
|
97
|
+
$subject->loadMissing('driver');
|
|
98
|
+
$this->vehicle = $subject;
|
|
99
|
+
$this->driver = $subject->driver;
|
|
100
|
+
$this->subjectType = 'vehicle';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
$this->geofence = $geofence;
|
|
104
|
+
$this->geofenceType = $geofenceType;
|
|
105
|
+
$this->enteredAt = $enteredAt;
|
|
106
|
+
$this->dwellDurationMinutes = (int) $enteredAt->diffInMinutes(now());
|
|
107
|
+
$this->timestamp = now();
|
|
108
|
+
$this->modelRecordName = $geofence->name ?? $geofence->public_id ?? $geofence->uuid ?? null;
|
|
109
|
+
$this->userSession = session('user');
|
|
110
|
+
$this->companySession = session('company', $this->getCompanyUuid());
|
|
111
|
+
$this->requestMethod = request()?->method() ?? 'CLI';
|
|
112
|
+
$this->apiCredential = session('api_credential', 'console');
|
|
113
|
+
$this->apiSecret = session('api_secret', 'internal');
|
|
114
|
+
$this->apiKey = session('api_key');
|
|
115
|
+
$this->apiEnvironment = session('api_environment', 'live');
|
|
116
|
+
$this->isSandbox = session('is_sandbox', false);
|
|
117
|
+
$this->data = $this->getEventData();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Returns the company UUID for this event.
|
|
122
|
+
*/
|
|
123
|
+
public function getCompanyUuid(): string
|
|
124
|
+
{
|
|
125
|
+
return $this->vehicle?->company_uuid ?? $this->driver->company_uuid;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
public function broadcastOn(): array
|
|
129
|
+
{
|
|
130
|
+
$channels = [new Channel('company.' . $this->getCompanyUuid())];
|
|
131
|
+
|
|
132
|
+
if ($this->driver) {
|
|
133
|
+
$channels[] = new Channel('driver.' . $this->driver->public_id);
|
|
134
|
+
$channels[] = new Channel('driver.' . $this->driver->uuid);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if ($this->vehicle) {
|
|
138
|
+
$channels[] = new Channel('vehicle.' . $this->vehicle->public_id);
|
|
139
|
+
$channels[] = new Channel('vehicle.' . $this->vehicle->uuid);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return $channels;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
public function broadcastAs(): string
|
|
146
|
+
{
|
|
147
|
+
return 'geofence.dwelled';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Returns the standardised webhook payload for this event.
|
|
152
|
+
*/
|
|
153
|
+
public function broadcastWith(): array
|
|
154
|
+
{
|
|
155
|
+
return $this->getEventData();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
public function getEventData(): array
|
|
159
|
+
{
|
|
160
|
+
$driver = $this->driver;
|
|
161
|
+
$vehicle = $this->vehicle;
|
|
162
|
+
$geofence = $this->geofence;
|
|
163
|
+
$subject = $this->subjectType === 'vehicle' ? $vehicle : $driver;
|
|
164
|
+
|
|
165
|
+
$payload = [
|
|
166
|
+
'event' => $this->broadcastAs(),
|
|
167
|
+
'event_type' => 'geofence.dwelled',
|
|
168
|
+
'occurred_at' => $this->timestamp->toIso8601String(),
|
|
169
|
+
'entered_at' => $this->enteredAt->toIso8601String(),
|
|
170
|
+
'dwell_duration_minutes' => $this->dwellDurationMinutes,
|
|
171
|
+
'subject' => [
|
|
172
|
+
'type' => $this->subjectType,
|
|
173
|
+
'id' => $subject?->public_id ?? null,
|
|
174
|
+
'uuid' => $subject?->uuid ?? null,
|
|
175
|
+
'name' => $this->subjectType === 'vehicle' ? ($vehicle?->display_name ?? $vehicle?->plate_number) : $driver?->name,
|
|
176
|
+
],
|
|
177
|
+
'driver' => $driver ? [
|
|
178
|
+
'id' => $driver->public_id,
|
|
179
|
+
'uuid' => $driver->uuid,
|
|
180
|
+
'name' => $driver->name,
|
|
181
|
+
'phone' => $driver->phone,
|
|
182
|
+
] : null,
|
|
183
|
+
'vehicle' => $vehicle ? [
|
|
184
|
+
'id' => $vehicle->public_id,
|
|
185
|
+
'uuid' => $vehicle->uuid,
|
|
186
|
+
'name' => $vehicle->display_name ?? null,
|
|
187
|
+
'plate' => $vehicle->plate_number ?? null,
|
|
188
|
+
] : null,
|
|
189
|
+
'geofence' => [
|
|
190
|
+
'id' => $geofence->public_id,
|
|
191
|
+
'uuid' => $geofence->uuid,
|
|
192
|
+
'name' => $geofence->name,
|
|
193
|
+
'type' => $this->geofenceType,
|
|
194
|
+
],
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
$order = $driver?->getCurrentOrder();
|
|
198
|
+
if ($order) {
|
|
199
|
+
$payload['order'] = [
|
|
200
|
+
'id' => $order->public_id,
|
|
201
|
+
'uuid' => $order->uuid,
|
|
202
|
+
'status' => $order->status,
|
|
203
|
+
];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return $payload;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Events;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Models\Driver;
|
|
6
|
+
use Fleetbase\FleetOps\Models\Vehicle;
|
|
7
|
+
use Fleetbase\LaravelMysqlSpatial\Types\Point;
|
|
8
|
+
use Illuminate\Broadcasting\Channel;
|
|
9
|
+
use Illuminate\Broadcasting\InteractsWithSockets;
|
|
10
|
+
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
|
11
|
+
use Illuminate\Foundation\Events\Dispatchable;
|
|
12
|
+
use Illuminate\Queue\SerializesModels;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* GeofenceEntered.
|
|
16
|
+
*
|
|
17
|
+
* Fired when a driver's location update causes them to cross into a
|
|
18
|
+
* geofence boundary (Zone or ServiceArea) for the first time.
|
|
19
|
+
*
|
|
20
|
+
* Listeners:
|
|
21
|
+
* - HandleGeofenceEntered (order automation, customer notification, event log)
|
|
22
|
+
* - SendResourceLifecycleWebhook (webhook delivery to configured endpoints)
|
|
23
|
+
*/
|
|
24
|
+
class GeofenceEntered implements ShouldBroadcast
|
|
25
|
+
{
|
|
26
|
+
use Dispatchable;
|
|
27
|
+
use InteractsWithSockets;
|
|
28
|
+
use SerializesModels;
|
|
29
|
+
|
|
30
|
+
public string $modelHumanName = 'Geofence';
|
|
31
|
+
public ?string $modelRecordName = null;
|
|
32
|
+
public string $eventName = 'entered';
|
|
33
|
+
public $userSession;
|
|
34
|
+
public $companySession;
|
|
35
|
+
public $requestMethod;
|
|
36
|
+
public $apiCredential;
|
|
37
|
+
public $apiSecret;
|
|
38
|
+
public $apiKey;
|
|
39
|
+
public $apiEnvironment;
|
|
40
|
+
public $isSandbox;
|
|
41
|
+
public array $data = [];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The driver who entered the geofence.
|
|
45
|
+
*/
|
|
46
|
+
public ?Driver $driver = null;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The vehicle associated with the geofence event, if any.
|
|
50
|
+
*/
|
|
51
|
+
public ?Vehicle $vehicle = null;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The geofence that was entered (Zone or ServiceArea model instance).
|
|
55
|
+
*
|
|
56
|
+
* @var \Fleetbase\FleetOps\Models\Zone|\Fleetbase\FleetOps\Models\ServiceArea
|
|
57
|
+
*/
|
|
58
|
+
public $geofence;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The type of geofence: 'zone' or 'service_area'.
|
|
62
|
+
*/
|
|
63
|
+
public string $geofenceType;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The driver's location at the time of entry.
|
|
67
|
+
*/
|
|
68
|
+
public Point $location;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The timestamp when the entry was detected.
|
|
72
|
+
*/
|
|
73
|
+
public \Carbon\Carbon $timestamp;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The subject that triggered the event: 'driver' or 'vehicle'.
|
|
77
|
+
*/
|
|
78
|
+
public string $subjectType;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Create a new GeofenceEntered event.
|
|
82
|
+
*
|
|
83
|
+
* @param mixed $geofence Zone or ServiceArea
|
|
84
|
+
* @param string $geofenceType 'zone' | 'service_area'
|
|
85
|
+
*/
|
|
86
|
+
public function __construct(Driver|Vehicle $subject, $geofence, string $geofenceType, Point $location)
|
|
87
|
+
{
|
|
88
|
+
if ($subject instanceof Driver) {
|
|
89
|
+
$this->driver = $subject;
|
|
90
|
+
$this->vehicle = $subject->vehicle;
|
|
91
|
+
$this->subjectType = 'driver';
|
|
92
|
+
} else {
|
|
93
|
+
$subject->loadMissing('driver');
|
|
94
|
+
$this->vehicle = $subject;
|
|
95
|
+
$this->driver = $subject->driver;
|
|
96
|
+
$this->subjectType = 'vehicle';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
$this->geofence = $geofence;
|
|
100
|
+
$this->geofenceType = $geofenceType;
|
|
101
|
+
$this->location = $location;
|
|
102
|
+
$this->timestamp = now();
|
|
103
|
+
$this->modelRecordName = $geofence->name ?? $geofence->public_id ?? $geofence->uuid ?? null;
|
|
104
|
+
$this->userSession = session('user');
|
|
105
|
+
$this->companySession = session('company', $this->getCompanyUuid());
|
|
106
|
+
$this->requestMethod = request()?->method() ?? 'CLI';
|
|
107
|
+
$this->apiCredential = session('api_credential', 'console');
|
|
108
|
+
$this->apiSecret = session('api_secret', 'internal');
|
|
109
|
+
$this->apiKey = session('api_key');
|
|
110
|
+
$this->apiEnvironment = session('api_environment', 'live');
|
|
111
|
+
$this->isSandbox = session('is_sandbox', false);
|
|
112
|
+
$this->data = $this->getEventData();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Returns the company UUID for this event.
|
|
117
|
+
* Used by the webhook infrastructure to scope delivery.
|
|
118
|
+
*/
|
|
119
|
+
public function getCompanyUuid(): string
|
|
120
|
+
{
|
|
121
|
+
return $this->vehicle?->company_uuid ?? $this->driver->company_uuid;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
public function broadcastOn(): array
|
|
125
|
+
{
|
|
126
|
+
$channels = [new Channel('company.' . $this->getCompanyUuid())];
|
|
127
|
+
|
|
128
|
+
if ($this->driver) {
|
|
129
|
+
$channels[] = new Channel('driver.' . $this->driver->public_id);
|
|
130
|
+
$channels[] = new Channel('driver.' . $this->driver->uuid);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if ($this->vehicle) {
|
|
134
|
+
$channels[] = new Channel('vehicle.' . $this->vehicle->public_id);
|
|
135
|
+
$channels[] = new Channel('vehicle.' . $this->vehicle->uuid);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return $channels;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
public function broadcastAs(): string
|
|
142
|
+
{
|
|
143
|
+
return 'geofence.entered';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Returns the standardised webhook payload for this event.
|
|
148
|
+
*/
|
|
149
|
+
public function broadcastWith(): array
|
|
150
|
+
{
|
|
151
|
+
return $this->getEventData();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
public function getEventData(): array
|
|
155
|
+
{
|
|
156
|
+
$driver = $this->driver;
|
|
157
|
+
$vehicle = $this->vehicle;
|
|
158
|
+
$geofence = $this->geofence;
|
|
159
|
+
$subject = $this->subjectType === 'vehicle' ? $vehicle : $driver;
|
|
160
|
+
|
|
161
|
+
$payload = [
|
|
162
|
+
'event' => $this->broadcastAs(),
|
|
163
|
+
'event_type' => 'geofence.entered',
|
|
164
|
+
'occurred_at' => $this->timestamp->toIso8601String(),
|
|
165
|
+
'subject' => [
|
|
166
|
+
'type' => $this->subjectType,
|
|
167
|
+
'id' => $subject?->public_id ?? null,
|
|
168
|
+
'uuid' => $subject?->uuid ?? null,
|
|
169
|
+
'name' => $this->subjectType === 'vehicle' ? ($vehicle?->display_name ?? $vehicle?->plate_number) : $driver?->name,
|
|
170
|
+
],
|
|
171
|
+
'driver' => $driver ? [
|
|
172
|
+
'id' => $driver->public_id,
|
|
173
|
+
'uuid' => $driver->uuid,
|
|
174
|
+
'name' => $driver->name,
|
|
175
|
+
'phone' => $driver->phone,
|
|
176
|
+
] : null,
|
|
177
|
+
'vehicle' => $vehicle ? [
|
|
178
|
+
'id' => $vehicle->public_id,
|
|
179
|
+
'uuid' => $vehicle->uuid,
|
|
180
|
+
'name' => $vehicle->display_name ?? null,
|
|
181
|
+
'plate' => $vehicle->plate_number ?? null,
|
|
182
|
+
] : null,
|
|
183
|
+
'geofence' => [
|
|
184
|
+
'id' => $geofence->public_id,
|
|
185
|
+
'uuid' => $geofence->uuid,
|
|
186
|
+
'name' => $geofence->name,
|
|
187
|
+
'type' => $this->geofenceType,
|
|
188
|
+
],
|
|
189
|
+
'location' => [
|
|
190
|
+
'latitude' => $this->location->getLat(),
|
|
191
|
+
'longitude' => $this->location->getLng(),
|
|
192
|
+
],
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
// Attach active order context if the driver has one
|
|
196
|
+
$order = $driver?->getCurrentOrder();
|
|
197
|
+
if ($order) {
|
|
198
|
+
$payload['order'] = [
|
|
199
|
+
'id' => $order->public_id,
|
|
200
|
+
'uuid' => $order->uuid,
|
|
201
|
+
'status' => $order->status,
|
|
202
|
+
];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return $payload;
|
|
206
|
+
}
|
|
207
|
+
}
|