@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
@@ -0,0 +1,206 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support;
4
+
5
+ use Fleetbase\FleetOps\Models\Driver;
6
+ use Fleetbase\FleetOps\Models\ServiceArea;
7
+ use Fleetbase\FleetOps\Models\Vehicle;
8
+ use Fleetbase\FleetOps\Models\Zone;
9
+ use Fleetbase\LaravelMysqlSpatial\Types\Point;
10
+ use Illuminate\Support\Facades\DB;
11
+
12
+ /**
13
+ * GeofenceIntersectionService.
14
+ *
15
+ * The core spatial engine for the active geofencing system.
16
+ *
17
+ * On every driver location update, this service:
18
+ * 1. Queries all zones and service areas containing the new point using
19
+ * a two-pass MySQL spatial strategy (MBRContains for index-assisted
20
+ * bounding-box pre-filter, then ST_Contains for precise polygon check).
21
+ * 2. Compares the result against the driver's current state in the
22
+ * driver_geofence_states table.
23
+ * 3. Returns a list of crossing events (entered / exited) for the caller
24
+ * to dispatch as Laravel events.
25
+ *
26
+ * This class is registered as a singleton in FleetOpsServiceProvider.
27
+ */
28
+ class GeofenceIntersectionService
29
+ {
30
+ /**
31
+ * Detect geofence boundary crossings for a driver given their new location.
32
+ *
33
+ * Returns an array of crossing events. Each element is an associative array:
34
+ * - 'type': 'entered' | 'exited'
35
+ * - 'geofence': Zone or ServiceArea model instance
36
+ * - 'geofence_type': 'zone' | 'service_area'
37
+ */
38
+ public function detectCrossings(Driver|Vehicle $subject, Point $newLocation): array
39
+ {
40
+ if ($subject instanceof Driver) {
41
+ return $this->detectDriverCrossings($subject, $newLocation);
42
+ }
43
+
44
+ return $this->detectVehicleCrossings($subject, $newLocation);
45
+ }
46
+
47
+ public function detectDriverCrossings(Driver $driver, Point $newLocation): array
48
+ {
49
+ return $this->detectSubjectCrossings($driver->company_uuid, $newLocation, 'driver_geofence_states', 'driver_uuid', $driver->uuid);
50
+ }
51
+
52
+ public function detectVehicleCrossings(Vehicle $vehicle, Point $newLocation): array
53
+ {
54
+ return $this->detectSubjectCrossings($vehicle->company_uuid, $newLocation, 'vehicle_geofence_states', 'vehicle_uuid', $vehicle->uuid);
55
+ }
56
+
57
+ protected function detectSubjectCrossings(string $companyUuid, Point $newLocation, string $stateTable, string $subjectColumn, string $subjectUuid): array
58
+ {
59
+ $crossings = [];
60
+
61
+ // Build the WKT point string. MySQL ST_GeomFromText expects (lng lat) order.
62
+ $wkt = sprintf('POINT(%s %s)', $newLocation->getLng(), $newLocation->getLat());
63
+
64
+ // ----------------------------------------------------------------
65
+ // 1. Find all Zones the driver is currently inside.
66
+ //
67
+ // Two-pass strategy for MySQL performance:
68
+ // - MBRContains uses the SPATIAL index to filter by minimum
69
+ // bounding rectangle (fast, may include false positives).
70
+ // - ST_Contains performs the precise polygon containment check
71
+ // on the reduced candidate set (accurate, no index needed).
72
+ // ----------------------------------------------------------------
73
+ $insideZones = Zone::where('company_uuid', $companyUuid)
74
+ ->whereNotNull('border')
75
+ ->where(function ($q) {
76
+ $q->where('trigger_on_entry', true)
77
+ ->orWhere('trigger_on_exit', true)
78
+ ->orWhereNotNull('dwell_threshold_minutes');
79
+ })
80
+ ->whereRaw('MBRContains(`border`, ST_GeomFromText(?))', [$wkt])
81
+ ->whereRaw('ST_Contains(`border`, ST_GeomFromText(?))', [$wkt])
82
+ ->get();
83
+
84
+ // ----------------------------------------------------------------
85
+ // 2. Find all Service Areas the driver is currently inside.
86
+ //
87
+ // Service areas use MultiPolygon borders, so we use
88
+ // ST_Contains which handles both Polygon and MultiPolygon.
89
+ // ----------------------------------------------------------------
90
+ $insideServiceAreas = ServiceArea::where('company_uuid', $companyUuid)
91
+ ->whereNotNull('border')
92
+ ->where(function ($q) {
93
+ $q->where('trigger_on_entry', true)
94
+ ->orWhere('trigger_on_exit', true)
95
+ ->orWhereNotNull('dwell_threshold_minutes');
96
+ })
97
+ ->whereRaw('MBRContains(`border`, ST_GeomFromText(?))', [$wkt])
98
+ ->whereRaw('ST_Contains(`border`, ST_GeomFromText(?))', [$wkt])
99
+ ->get();
100
+
101
+ // Merge into a unified collection with a type discriminator
102
+ $currentlyInside = collect()
103
+ ->merge($insideZones->map(fn ($z) => ['model' => $z, 'geofence_type' => 'zone']))
104
+ ->merge($insideServiceAreas->map(fn ($sa) => ['model' => $sa, 'geofence_type' => 'service_area']));
105
+
106
+ $currentlyInsideUuids = $currentlyInside->pluck('model.uuid')->toArray();
107
+
108
+ // ----------------------------------------------------------------
109
+ // 3. Load the driver's current geofence state records.
110
+ // ----------------------------------------------------------------
111
+ $currentStates = DB::table($stateTable)
112
+ ->where($subjectColumn, $subjectUuid)
113
+ ->get()
114
+ ->keyBy('geofence_uuid');
115
+
116
+ // ----------------------------------------------------------------
117
+ // 4. Detect ENTRIES: geofences the driver is now inside but
118
+ // was not inside before (or has no state record yet).
119
+ // ----------------------------------------------------------------
120
+ foreach ($currentlyInside as $item) {
121
+ $geofence = $item['model'];
122
+ $state = $currentStates->get($geofence->uuid);
123
+
124
+ if (!$state || !$state->is_inside) {
125
+ $crossings[] = [
126
+ 'type' => 'entered',
127
+ 'geofence' => $geofence,
128
+ 'geofence_type' => $item['geofence_type'],
129
+ ];
130
+ }
131
+ }
132
+
133
+ // ----------------------------------------------------------------
134
+ // 5. Detect EXITS: geofences the driver was inside but is no
135
+ // longer inside (not in the current ST_Contains result set).
136
+ // ----------------------------------------------------------------
137
+ foreach ($currentStates as $geofenceUuid => $state) {
138
+ if ($state->is_inside && !in_array($geofenceUuid, $currentlyInsideUuids)) {
139
+ $geofence = $state->geofence_type === 'service_area'
140
+ ? ServiceArea::where('uuid', $geofenceUuid)->first()
141
+ : Zone::where('uuid', $geofenceUuid)->first();
142
+
143
+ if ($geofence) {
144
+ $crossings[] = [
145
+ 'type' => 'exited',
146
+ 'geofence' => $geofence,
147
+ 'geofence_type' => $state->geofence_type,
148
+ ];
149
+ }
150
+ }
151
+ }
152
+
153
+ return $crossings;
154
+ }
155
+
156
+ /**
157
+ * Check if a driver is currently recorded as inside a specific geofence.
158
+ *
159
+ * @param mixed $geofence Zone or ServiceArea
160
+ */
161
+ public function isDriverInsideGeofence(Driver $driver, $geofence): bool
162
+ {
163
+ return DB::table('driver_geofence_states')
164
+ ->where('driver_uuid', $driver->uuid)
165
+ ->where('geofence_uuid', $geofence->uuid)
166
+ ->where('is_inside', true)
167
+ ->exists();
168
+ }
169
+
170
+ public function isVehicleInsideGeofence(Vehicle $vehicle, $geofence): bool
171
+ {
172
+ return DB::table('vehicle_geofence_states')
173
+ ->where('vehicle_uuid', $vehicle->uuid)
174
+ ->where('geofence_uuid', $geofence->uuid)
175
+ ->where('is_inside', true)
176
+ ->exists();
177
+ }
178
+
179
+ /**
180
+ * Clear all geofence state records for a driver.
181
+ * Called when a driver goes offline or completes a shift.
182
+ */
183
+ public function clearDriverState(Driver $driver): void
184
+ {
185
+ DB::table('driver_geofence_states')
186
+ ->where('driver_uuid', $driver->uuid)
187
+ ->update([
188
+ 'is_inside' => false,
189
+ 'exited_at' => now(),
190
+ 'dwell_job_id' => null,
191
+ 'updated_at' => now(),
192
+ ]);
193
+ }
194
+
195
+ public function clearVehicleState(Vehicle $vehicle): void
196
+ {
197
+ DB::table('vehicle_geofence_states')
198
+ ->where('vehicle_uuid', $vehicle->uuid)
199
+ ->update([
200
+ 'is_inside' => false,
201
+ 'exited_at' => now(),
202
+ 'dwell_job_id' => null,
203
+ 'updated_at' => now(),
204
+ ]);
205
+ }
206
+ }
@@ -3,6 +3,8 @@
3
3
  namespace Fleetbase\FleetOps\Support;
4
4
 
5
5
  use Fleetbase\FleetOps\Flow\Activity;
6
+ use Fleetbase\Models\Company;
7
+ use Fleetbase\Models\Setting;
6
8
  use Fleetbase\LaravelMysqlSpatial\Types\Point;
7
9
  use Fleetbase\Support\Utils as FleetbaseUtils;
8
10
  use Illuminate\Support\Arr;
@@ -20,6 +22,39 @@ class Utils extends FleetbaseUtils
20
22
  */
21
23
  public const DRIVING_TIME_MULTIPLIER = 7.2;
22
24
 
25
+ /**
26
+ * Resolve the preferred transaction currency for a company.
27
+ *
28
+ * Resolution order:
29
+ * 1. Organization/company configured currency
30
+ * 2. Ledger accounting base currency
31
+ * 3. USD
32
+ */
33
+ public static function getCompanyTransactionCurrency(string|Company|null $company = null): string
34
+ {
35
+ $companyModel = $company instanceof Company
36
+ ? $company
37
+ : (filled($company) ? Company::where('uuid', $company)->first() : null);
38
+
39
+ $companyUuid = $companyModel?->uuid ?? (is_string($company) ? $company : null);
40
+
41
+ $companyCurrency = data_get($companyModel, 'currency');
42
+ if (filled($companyCurrency)) {
43
+ return strtoupper($companyCurrency);
44
+ }
45
+
46
+ if (filled($companyUuid)) {
47
+ $accountingSettings = Setting::lookup('company.' . $companyUuid . '.ledger.accounting-settings', []);
48
+ $baseCurrency = data_get($accountingSettings, 'base_currency');
49
+
50
+ if (filled($baseCurrency)) {
51
+ return strtoupper($baseCurrency);
52
+ }
53
+ }
54
+
55
+ return 'USD';
56
+ }
57
+
23
58
  /**
24
59
  * Get a formatted string representation of a place's address.
25
60
  *
@@ -145,6 +145,13 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
145
145
  $router->put('{id}', 'ServiceAreaController@update');
146
146
  $router->delete('{id}', 'ServiceAreaController@delete');
147
147
  });
148
+ // geofences routes
149
+ $router->group(['prefix' => 'geofences'], function () use ($router) {
150
+ $router->get('events', 'GeofenceController@events');
151
+ $router->get('inventory', 'GeofenceController@inventory');
152
+ $router->get('dwell-report', 'GeofenceController@dwellReport');
153
+ $router->get('driver/{driverUuid}/history', 'GeofenceController@driverHistory');
154
+ });
148
155
  // service-rates routes
149
156
  $router->group(['prefix' => 'service-rates'], function () use ($router) {
150
157
  $router->post('/', 'ServiceRateController@create');
@@ -375,6 +382,15 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
375
382
  $router->delete('bulk-delete', $controller('bulkDelete'));
376
383
  }
377
384
  );
385
+ $router->group(
386
+ ['prefix' => 'geofences'],
387
+ function () use ($router) {
388
+ $router->get('events', 'GeofenceController@events');
389
+ $router->get('inventory', 'GeofenceController@inventory');
390
+ $router->get('dwell-report', 'GeofenceController@dwellReport');
391
+ $router->get('driver/{driverUuid}/history', 'GeofenceController@driverHistory');
392
+ }
393
+ );
378
394
  $router->fleetbaseRoutes('zones');
379
395
  $router->fleetbaseRoutes(
380
396
  'service-quotes',