@fleetbase/fleetops-engine 0.6.48 → 0.6.50

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 (266) hide show
  1. package/addon/components/customer/create-order-form.hbs +2 -2
  2. package/addon/components/customer/form.hbs +2 -2
  3. package/addon/components/customer/order-form.hbs +2 -2
  4. package/addon/components/customer/orders.hbs +2 -2
  5. package/addon/components/display-place.hbs +2 -2
  6. package/addon/components/driver/details.hbs +5 -1
  7. package/addon/components/driver/form.hbs +11 -1
  8. package/addon/components/fleet-listing-panel.hbs +2 -2
  9. package/addon/components/fleet-listing-panel.js +10 -0
  10. package/addon/components/map/google-live-map.js +18 -75
  11. package/addon/components/map/leaflet-live-map.hbs +187 -37
  12. package/addon/components/map/leaflet-live-map.js +184 -20
  13. package/addon/components/modals/order-change-destination.hbs +46 -0
  14. package/addon/components/modals/order-change-destination.js +106 -0
  15. package/addon/components/modals/stop-activity.hbs +28 -0
  16. package/addon/components/modals/stop-activity.js +52 -0
  17. package/addon/components/modals/update-order-activity.hbs +45 -3
  18. package/addon/components/modals/update-order-activity.js +143 -5
  19. package/addon/components/orchestrator/phase-builder.hbs +21 -0
  20. package/addon/components/orchestrator/phase-builder.js +13 -0
  21. package/addon/components/orchestrator/resource-panel.hbs +0 -10
  22. package/addon/components/orchestrator/resource-panel.js +2 -4
  23. package/addon/components/orchestrator-workbench.js +1 -0
  24. package/addon/components/order/activity-list.hbs +2 -2
  25. package/addon/components/order/activity-timeline.hbs +31 -29
  26. package/addon/components/order/details/activity.hbs +3 -3
  27. package/addon/components/order/details/activity.js +11 -0
  28. package/addon/components/order/details/documents.js +2 -0
  29. package/addon/components/order/details/purchase-rate.hbs +2 -2
  30. package/addon/components/order/details/route.hbs +11 -3
  31. package/addon/components/order/details/route.js +91 -14
  32. package/addon/components/order/details/tracking.hbs +101 -72
  33. package/addon/components/order/details/tracking.js +149 -2
  34. package/addon/components/order/form/details.hbs +3 -3
  35. package/addon/components/order/form/documents.js +11 -4
  36. package/addon/components/order/form/route.hbs +108 -104
  37. package/addon/components/order/form/route.js +37 -1
  38. package/addon/components/order/form/service-rate.hbs +2 -2
  39. package/addon/components/order/form.hbs +2 -2
  40. package/addon/components/order-config-manager/activity-flow.hbs +4 -1
  41. package/addon/components/order-config-manager/activity-flow.js +178 -12
  42. package/addon/components/place/address.js +1 -1
  43. package/addon/components/route-list.hbs +17 -17
  44. package/addon/components/route-list.js +42 -10
  45. package/addon/components/service-rate/details.hbs +41 -0
  46. package/addon/components/service-rate/form.hbs +118 -0
  47. package/addon/components/service-rate/form.js +55 -0
  48. package/addon/components/vehicle/form.js +1 -1
  49. package/addon/components/vehicle/pill.hbs +1 -1
  50. package/addon/components/widget/fleet-ops-key-metrics.hbs +19 -17
  51. package/addon/components/widget/fleet-ops-key-metrics.js +26 -0
  52. package/addon/components/widget/fuel-efficiency.hbs +26 -0
  53. package/addon/components/widget/fuel-efficiency.js +43 -0
  54. package/addon/components/widget/geofence-violations.hbs +44 -0
  55. package/addon/components/widget/geofence-violations.js +55 -0
  56. package/addon/components/widget/issues-insights.hbs +43 -0
  57. package/addon/components/widget/issues-insights.js +62 -0
  58. package/addon/components/widget/kpi-active-orders.hbs +10 -0
  59. package/addon/components/widget/kpi-active-orders.js +5 -0
  60. package/addon/components/widget/kpi-aov.hbs +10 -0
  61. package/addon/components/widget/kpi-aov.js +5 -0
  62. package/addon/components/widget/kpi-distance.hbs +10 -0
  63. package/addon/components/widget/kpi-distance.js +5 -0
  64. package/addon/components/widget/kpi-drivers-online.hbs +10 -0
  65. package/addon/components/widget/kpi-drivers-online.js +5 -0
  66. package/addon/components/widget/kpi-earnings.hbs +10 -0
  67. package/addon/components/widget/kpi-earnings.js +5 -0
  68. package/addon/components/widget/kpi-open-issues.hbs +11 -0
  69. package/addon/components/widget/kpi-open-issues.js +5 -0
  70. package/addon/components/widget/kpi-tile.hbs +59 -0
  71. package/addon/components/widget/kpi-tile.js +168 -0
  72. package/addon/components/widget/live-fleet.hbs +122 -0
  73. package/addon/components/widget/live-fleet.js +365 -0
  74. package/addon/components/widget/maintenance-overview.hbs +45 -0
  75. package/addon/components/widget/maintenance-overview.js +40 -0
  76. package/addon/components/widget/on-time-delivery.hbs +27 -0
  77. package/addon/components/widget/on-time-delivery.js +73 -0
  78. package/addon/components/widget/operations-pulse.hbs +51 -0
  79. package/addon/components/widget/operations-pulse.js +173 -0
  80. package/addon/components/widget/orders-by-status.hbs +26 -0
  81. package/addon/components/widget/orders-by-status.js +54 -0
  82. package/addon/components/widget/revenue-trend.hbs +34 -0
  83. package/addon/components/widget/revenue-trend.js +67 -0
  84. package/addon/components/widget/top-drivers.hbs +44 -0
  85. package/addon/components/widget/top-drivers.js +54 -0
  86. package/addon/controllers/management/vehicles/index/new.js +1 -1
  87. package/addon/controllers/operations/orders/index/details.js +4 -4
  88. package/addon/controllers/operations/orders/index.js +14 -0
  89. package/addon/controllers/settings/routing.js +26 -0
  90. package/addon/extension.js +172 -3
  91. package/addon/helpers/place-address-html.js +2 -2
  92. package/addon/routes/operations/service-rates/index.js +1 -1
  93. package/addon/services/geofence.js +163 -19
  94. package/addon/services/leaflet-contextmenu-manager.js +27 -0
  95. package/addon/services/map-adapter/google.js +13 -2
  96. package/addon/services/vehicle-actions.js +1 -1
  97. package/addon/styles/fleetops-engine.css +205 -88
  98. package/addon/templates/maintenance/equipment/index.hbs +1 -1
  99. package/addon/templates/maintenance/parts/index.hbs +1 -1
  100. package/addon/templates/maintenance/schedules/index.hbs +1 -1
  101. package/addon/templates/management/vehicles/index.hbs +1 -1
  102. package/addon/templates/settings/avatars.hbs +1 -1
  103. package/addon/templates/settings/custom-fields.hbs +1 -1
  104. package/addon/templates/settings/map.hbs +1 -1
  105. package/addon/templates/settings/navigator-app.hbs +1 -1
  106. package/addon/templates/settings/notifications.hbs +1 -1
  107. package/addon/templates/settings/orchestrator.hbs +1 -1
  108. package/addon/templates/settings/payments/index.hbs +1 -1
  109. package/addon/templates/settings/payments/onboard.hbs +1 -1
  110. package/addon/templates/settings/routing.hbs +22 -2
  111. package/addon/templates/settings/scheduling.hbs +1 -1
  112. package/addon/utils/fleet-ops-options.js +2 -1
  113. package/addon/utils/live-map-card-content.js +171 -0
  114. package/addon/utils/normalize-order-config-flow.js +41 -0
  115. package/addon/utils/place-address-html.js +3 -2
  116. package/addon/utils/route-visualization.js +54 -7
  117. package/app/components/modals/order-change-destination.js +1 -0
  118. package/app/components/modals/stop-activity.js +1 -0
  119. package/app/components/widget/fuel-efficiency.js +1 -0
  120. package/app/components/widget/geofence-violations.js +1 -0
  121. package/app/components/widget/issues-insights.js +1 -0
  122. package/app/components/widget/kpi-active-orders.js +1 -0
  123. package/app/components/widget/kpi-aov.js +1 -0
  124. package/app/components/widget/kpi-distance.js +1 -0
  125. package/app/components/widget/kpi-drivers-online.js +1 -0
  126. package/app/components/widget/kpi-earnings.js +1 -0
  127. package/app/components/widget/kpi-open-issues.js +1 -0
  128. package/app/components/widget/kpi-tile.js +1 -0
  129. package/app/components/widget/live-fleet.js +1 -0
  130. package/app/components/widget/maintenance-overview.js +1 -0
  131. package/app/components/widget/on-time-delivery.js +1 -0
  132. package/app/components/widget/operations-pulse.js +1 -0
  133. package/app/components/widget/orders-by-status.js +1 -0
  134. package/app/components/widget/revenue-trend.js +1 -0
  135. package/app/components/widget/top-drivers.js +1 -0
  136. package/app/utils/live-map-card-content.js +1 -0
  137. package/composer.json +1 -1
  138. package/extension.json +1 -1
  139. package/package.json +3 -3
  140. package/pnpm-workspace.yaml +1 -0
  141. package/server/config/telematics.php +111 -1
  142. package/server/migrations/2026_05_18_000001_add_license_expiry_to_drivers_table.php +25 -0
  143. package/server/migrations/2026_05_21_000001_add_geographic_rules_to_service_rate_fees_table.php +37 -0
  144. package/server/migrations/2026_05_25_000001_add_endpoint_tracking_refs_to_payloads_table.php +50 -0
  145. package/server/migrations/2026_05_27_000001_default_driver_vehicle_statuses_to_available.php +48 -0
  146. package/server/resources/views/labels/default.php +57 -49
  147. package/server/resources/views/labels/waypoint-label.php +27 -20
  148. package/server/seeders/Testing/Concerns/SeedsTestingData.php +183 -0
  149. package/server/seeders/Testing/ConnectivitySeeder.php +213 -0
  150. package/server/seeders/Testing/FleetSeeder.php +341 -0
  151. package/server/seeders/Testing/MaintenanceSeeder.php +238 -0
  152. package/server/seeders/Testing/NetworkSeeder.php +250 -0
  153. package/server/seeders/Testing/OrdersSeeder.php +198 -0
  154. package/server/seeders/Testing/TestingSeeder.php +40 -0
  155. package/server/seeders/{VroomOrchestrationSeeder.php → Testing/VroomOrchestrationSeeder.php} +29 -22
  156. package/server/src/Console/Commands/AuditCustomerUserConflicts.php +75 -0
  157. package/server/src/Console/Commands/ProcessOperationalAlerts.php +279 -0
  158. package/server/src/Exceptions/CustomerUserConflictException.php +13 -0
  159. package/server/src/Http/Controllers/Api/v1/ContactController.php +17 -2
  160. package/server/src/Http/Controllers/Api/v1/CustomerController.php +876 -0
  161. package/server/src/Http/Controllers/Api/v1/DriverController.php +1 -1
  162. package/server/src/Http/Controllers/Api/v1/OrderConfigController.php +54 -0
  163. package/server/src/Http/Controllers/Api/v1/OrderController.php +98 -70
  164. package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -2
  165. package/server/src/Http/Controllers/Api/v1/VehicleController.php +20 -3
  166. package/server/src/Http/Controllers/Internal/v1/AnalyticsController.php +119 -0
  167. package/server/src/Http/Controllers/Internal/v1/ContactController.php +19 -0
  168. package/server/src/Http/Controllers/Internal/v1/LiveController.php +101 -66
  169. package/server/src/Http/Controllers/Internal/v1/MetricsController.php +82 -3
  170. package/server/src/Http/Controllers/Internal/v1/OrderController.php +441 -32
  171. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
  172. package/server/src/Http/Controllers/Internal/v1/SettingController.php +46 -1
  173. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +54 -1
  174. package/server/src/Http/Middleware/AuthenticateCustomerToken.php +34 -0
  175. package/server/src/Http/Requests/CreateCustomerOrderRequest.php +63 -0
  176. package/server/src/Http/Requests/CreateCustomerRequest.php +48 -0
  177. package/server/src/Http/Requests/CreateDriverRequest.php +1 -1
  178. package/server/src/Http/Requests/CreateVehicleRequest.php +25 -1
  179. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +1 -1
  180. package/server/src/Http/Requests/VerifyCreateCustomerRequest.php +29 -0
  181. package/server/src/Http/Resources/Internal/v1/OrderConfig.php +41 -0
  182. package/server/src/Http/Resources/v1/Customer.php +79 -0
  183. package/server/src/Http/Resources/v1/Driver.php +2 -0
  184. package/server/src/Http/Resources/v1/Fleet.php +4 -0
  185. package/server/src/Http/Resources/v1/Index/Driver.php +48 -0
  186. package/server/src/Http/Resources/v1/Index/Vehicle.php +74 -1
  187. package/server/src/Http/Resources/v1/OrderConfig.php +74 -0
  188. package/server/src/Http/Resources/v1/Payload.php +23 -20
  189. package/server/src/Http/Resources/v1/ServiceArea.php +33 -19
  190. package/server/src/Http/Resources/v1/ServiceRateFee.php +7 -0
  191. package/server/src/Listeners/HandleOrderDispatched.php +2 -2
  192. package/server/src/Models/Contact.php +92 -15
  193. package/server/src/Models/Customer.php +64 -0
  194. package/server/src/Models/Driver.php +14 -3
  195. package/server/src/Models/Entity.php +2 -2
  196. package/server/src/Models/Order.php +29 -4
  197. package/server/src/Models/OrderConfig.php +14 -11
  198. package/server/src/Models/Payload.php +17 -1
  199. package/server/src/Models/ServiceRate.php +273 -1
  200. package/server/src/Models/ServiceRateFee.php +39 -9
  201. package/server/src/Models/TrackingNumber.php +8 -2
  202. package/server/src/Models/Vehicle.php +18 -2
  203. package/server/src/Models/Waypoint.php +2 -2
  204. package/server/src/Notifications/LateDeparture.php +48 -0
  205. package/server/src/Notifications/ProlongedStoppage.php +48 -0
  206. package/server/src/Notifications/RouteDeviation.php +48 -0
  207. package/server/src/Observers/ContactObserver.php +2 -0
  208. package/server/src/Observers/ServiceRateObserver.php +6 -6
  209. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +29 -2
  210. package/server/src/Providers/FleetOpsServiceProvider.php +6 -0
  211. package/server/src/Providers/NotificationServiceProvider.php +3 -0
  212. package/server/src/Support/Analytics/AbstractAnalytics.php +77 -0
  213. package/server/src/Support/Analytics/FuelEfficiency.php +83 -0
  214. package/server/src/Support/Analytics/GeofenceViolations.php +63 -0
  215. package/server/src/Support/Analytics/IssuesInsights.php +67 -0
  216. package/server/src/Support/Analytics/LiveFleet.php +127 -0
  217. package/server/src/Support/Analytics/MaintenanceOverview.php +70 -0
  218. package/server/src/Support/Analytics/OnTimeDelivery.php +75 -0
  219. package/server/src/Support/Analytics/OperationsPulse.php +86 -0
  220. package/server/src/Support/Analytics/OrdersByStatus.php +64 -0
  221. package/server/src/Support/Analytics/RevenueTrend.php +78 -0
  222. package/server/src/Support/Analytics/TopDrivers.php +92 -0
  223. package/server/src/Support/CustomerAuth.php +98 -0
  224. package/server/src/Support/LabelPdf.php +52 -0
  225. package/server/src/Support/Metrics/AbstractMetric.php +170 -0
  226. package/server/src/Support/Metrics/ActiveLiveOrdersMetric.php +28 -0
  227. package/server/src/Support/Metrics/AvgOrderValueMetric.php +53 -0
  228. package/server/src/Support/Metrics/DriversOnlineMetric.php +30 -0
  229. package/server/src/Support/Metrics/EarningsMetric.php +30 -0
  230. package/server/src/Support/Metrics/FuelCostsMetric.php +30 -0
  231. package/server/src/Support/Metrics/MoneyMetric.php +30 -0
  232. package/server/src/Support/Metrics/OpenIssuesMetric.php +35 -0
  233. package/server/src/Support/Metrics/OrdersCanceledMetric.php +35 -0
  234. package/server/src/Support/Metrics/OrdersCompletedMetric.php +35 -0
  235. package/server/src/Support/Metrics/OrdersInProgressMetric.php +51 -0
  236. package/server/src/Support/Metrics/OrdersScheduledMetric.php +37 -0
  237. package/server/src/Support/Metrics/Registry.php +46 -0
  238. package/server/src/Support/Metrics/ResolvedIssuesMetric.php +35 -0
  239. package/server/src/Support/Metrics/TotalCustomersMetric.php +29 -0
  240. package/server/src/Support/Metrics/TotalDistanceTraveledMetric.php +35 -0
  241. package/server/src/Support/Metrics/TotalDriversMetric.php +28 -0
  242. package/server/src/Support/Metrics/TotalTimeTraveledMetric.php +35 -0
  243. package/server/src/Support/Metrics.php +46 -249
  244. package/server/src/Support/ResolvesOrderServiceStops.php +512 -0
  245. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +290 -0
  246. package/server/src/Support/Telematics/Providers/SafeeProvider.php +333 -0
  247. package/server/src/Tracking/TrackingContextBuilder.php +41 -38
  248. package/server/src/Tracking/TrackingIntelligenceService.php +109 -9
  249. package/server/src/Tracking/TrackingStop.php +13 -11
  250. package/server/src/routes.php +50 -0
  251. package/server/tests/AnalyticsRoutesTest.php +41 -0
  252. package/server/tests/CapacityAllocationEngineTest.php +10 -10
  253. package/server/tests/CustomerContactIdentitySafetyTest.php +54 -0
  254. package/server/tests/CustomerEndpointTest.php +199 -0
  255. package/server/tests/DriverVehicleStatusDefaultTest.php +39 -0
  256. package/server/tests/LabelPdfRenderingTest.php +23 -0
  257. package/server/tests/LiveControllerViewportTest.php +60 -0
  258. package/server/tests/MetricsRegistryTest.php +84 -0
  259. package/server/tests/OrchestrationPayloadBuilderTest.php +2 -2
  260. package/server/tests/OrderConfigEndpointTest.php +86 -0
  261. package/server/tests/OrderFileAttachmentTest.php +20 -0
  262. package/server/tests/ServiceAreaZoneApiShapeTest.php +34 -0
  263. package/server/tests/VehicleDriverAssignmentTest.php +18 -0
  264. package/server/tests/VroomOrchestrationEngineTest.php +91 -0
  265. package/translations/en-us.yaml +38 -0
  266. package/server/migrations/2026_05_13_000001_repair_customer_contact_user_types.php +0 -86
@@ -216,6 +216,14 @@ class ServiceRate extends Model
216
216
  return $this->rate_calculation_method === 'per_meter';
217
217
  }
218
218
 
219
+ /**
220
+ * Check if the rate calculation method is "multi_zone_distance".
221
+ */
222
+ public function isMultiZoneDistance(): bool
223
+ {
224
+ return $this->rate_calculation_method === 'multi_zone_distance';
225
+ }
226
+
219
227
  /**
220
228
  * Check if the rate calculation method is "per_drop".
221
229
  */
@@ -329,6 +337,16 @@ class ServiceRate extends Model
329
337
  return $this;
330
338
  }
331
339
 
340
+ $serviceRateFees = collect($serviceRateFees)
341
+ ->map(fn ($fee) => $this->normalizeServiceRateFeePayload($fee))
342
+ ->filter()
343
+ ->values()
344
+ ->toArray();
345
+
346
+ if (!$serviceRateFees) {
347
+ return $this;
348
+ }
349
+
332
350
  $iterate = count($serviceRateFees);
333
351
 
334
352
  for ($i = 0; $i < $iterate; $i++) {
@@ -356,6 +374,43 @@ class ServiceRate extends Model
356
374
  return $this;
357
375
  }
358
376
 
377
+ /**
378
+ * Normalize embedded service-rate fee payloads before persistence.
379
+ */
380
+ public function normalizeServiceRateFeePayload($fee): ?array
381
+ {
382
+ if (!is_array($fee)) {
383
+ return null;
384
+ }
385
+
386
+ $fee['service_area_uuid'] ??= data_get($fee, 'service_area.uuid');
387
+ $fee['zone_uuid'] ??= data_get($fee, 'zone.uuid');
388
+
389
+ if (Utils::castBoolean($fee['is_fallback'] ?? false)) {
390
+ $fee['service_area_uuid'] = null;
391
+ $fee['zone_uuid'] = null;
392
+ }
393
+
394
+ return collect($fee)
395
+ ->only([
396
+ 'uuid',
397
+ 'service_rate_uuid',
398
+ 'service_area_uuid',
399
+ 'zone_uuid',
400
+ 'label',
401
+ 'priority',
402
+ 'is_fallback',
403
+ 'distance',
404
+ 'distance_unit',
405
+ 'min',
406
+ 'max',
407
+ 'unit',
408
+ 'fee',
409
+ 'currency',
410
+ ])
411
+ ->toArray();
412
+ }
413
+
359
414
  /**
360
415
  * Set the service rate parcel fees for the current object.
361
416
  *
@@ -506,7 +561,7 @@ class ServiceRate extends Model
506
561
  public static function getServicableForPlaces($places = [], $service = null, $currency = null, ?\Closure $queryCallback = null): array
507
562
  {
508
563
  $reader = new GeoJSONReader();
509
- $serviceRatesQuery = static::with(['zone', 'serviceArea', 'rateFees', 'parcelFees']);
564
+ $serviceRatesQuery = static::with(['zone', 'serviceArea', 'rateFees.zone', 'rateFees.serviceArea', 'parcelFees']);
510
565
 
511
566
  if ($currency) {
512
567
  $serviceRatesQuery->whereRaw('lower(currency) = ?', [strtolower($currency)]);
@@ -738,6 +793,12 @@ class ServiceRate extends Model
738
793
  ]);
739
794
  }
740
795
 
796
+ if ($this->isMultiZoneDistance()) {
797
+ [$multiZoneFee, $multiZoneLines] = $this->quoteMultiZoneDistance($waypoints, $totalDistance);
798
+ $subTotal += $multiZoneFee;
799
+ $lines = $lines->merge($multiZoneLines);
800
+ }
801
+
741
802
  if ($this->isAlgorithm()) {
742
803
  $resolvedEndpointCount = $endpointCount ?? $this->inferEndpointCountFromStops($waypoints);
743
804
  $rateFee = $this->normalizeCalculatedMoney(Algo::exec(
@@ -927,6 +988,12 @@ class ServiceRate extends Model
927
988
  ]);
928
989
  }
929
990
 
991
+ if ($this->isMultiZoneDistance()) {
992
+ [$multiZoneFee, $multiZoneLines] = $this->quoteMultiZoneDistance($waypoints, $totalDistance);
993
+ $subTotal += $multiZoneFee;
994
+ $lines = $lines->merge($multiZoneLines);
995
+ }
996
+
930
997
  if ($this->isAlgorithm()) {
931
998
  $rateFee = $this->normalizeCalculatedMoney(Algo::exec(
932
999
  $this->algorithm,
@@ -1086,6 +1153,211 @@ class ServiceRate extends Model
1086
1153
  return 2;
1087
1154
  }
1088
1155
 
1156
+ protected function quoteMultiZoneDistance($waypoints = [], ?int $totalDistance = 0): array
1157
+ {
1158
+ $this->loadMissing(['rateFees.zone', 'rateFees.serviceArea']);
1159
+
1160
+ $rules = $this->rateFees
1161
+ ->filter(fn ($rule) => !$rule->is_fallback && ($rule->zone || $rule->serviceArea))
1162
+ ->sortByDesc(fn ($rule) => (int) ($rule->priority ?? 0))
1163
+ ->values();
1164
+
1165
+ $fallbackRule = $this->rateFees
1166
+ ->filter(fn ($rule) => (bool) $rule->is_fallback)
1167
+ ->sortByDesc(fn ($rule) => (int) ($rule->priority ?? 0))
1168
+ ->first();
1169
+
1170
+ if ($rules->isEmpty() && !$fallbackRule) {
1171
+ return [0, collect()];
1172
+ }
1173
+
1174
+ $pricedDistances = $this->calculateMultiZoneDistances($waypoints, $rules, $fallbackRule, $totalDistance);
1175
+ $lines = collect();
1176
+ $subTotal = 0;
1177
+
1178
+ foreach ($pricedDistances as $entry) {
1179
+ $rule = $entry['rule'];
1180
+ if (!$rule instanceof ServiceRateFee) {
1181
+ continue;
1182
+ }
1183
+
1184
+ $distanceInMeters = (float) ($entry['distance_m'] ?? 0);
1185
+ if ($distanceInMeters <= 0) {
1186
+ continue;
1187
+ }
1188
+
1189
+ $unit = strtolower($rule->distance_unit ?: 'km');
1190
+ $distance = $this->normalizeDistanceForUnit($distanceInMeters, $unit);
1191
+ $fee = $this->normalizeCalculatedMoney($distance * Utils::numbersOnly($rule->fee ?? 0));
1192
+ $subTotal += $fee;
1193
+
1194
+ if ($rule->is_fallback) {
1195
+ $label = 'Out-of-zone distance charge';
1196
+ } else {
1197
+ $label = data_get($rule, 'zone.name') ?: data_get($rule, 'serviceArea.name') ?: $rule->label ?: 'Geographic';
1198
+ }
1199
+
1200
+ if (!$rule->is_fallback && !str_ends_with(strtolower($label), 'distance charge')) {
1201
+ $label .= ' distance charge';
1202
+ }
1203
+
1204
+ $lines->push([
1205
+ 'details' => sprintf('%s (%s %s x %s)', $label, round($distance, 2), $unit, Utils::moneyFormat($rule->fee, $this->currency)),
1206
+ 'amount' => $fee,
1207
+ 'formatted_amount' => Utils::moneyFormat($fee, $this->currency),
1208
+ 'currency' => $this->currency,
1209
+ 'code' => 'MULTI_ZONE_DISTANCE_FEE',
1210
+ ]);
1211
+ }
1212
+
1213
+ return [$subTotal, $lines];
1214
+ }
1215
+
1216
+ protected function calculateMultiZoneDistances($waypoints, $rules, ?ServiceRateFee $fallbackRule = null, ?int $totalDistance = 0): array
1217
+ {
1218
+ $places = collect($waypoints)->map(fn ($place) => Place::createFromMixed($place))->filter()->values();
1219
+
1220
+ if ($places->count() < 2 || (int) $totalDistance <= 0) {
1221
+ return $fallbackRule ? [['rule' => $fallbackRule, 'distance_m' => (float) ($totalDistance ?? 0)]] : [];
1222
+ }
1223
+
1224
+ $reader = new GeoJSONReader();
1225
+ $ruleGeometries = $rules->map(function ($rule) use ($reader) {
1226
+ return [
1227
+ 'rule' => $rule,
1228
+ 'geometry' => $this->readRateRuleGeometry($rule, $reader),
1229
+ ];
1230
+ })->filter(fn ($entry) => $entry['geometry'])->values();
1231
+
1232
+ if ($ruleGeometries->isEmpty()) {
1233
+ return $fallbackRule ? [['rule' => $fallbackRule, 'distance_m' => (float) $totalDistance]] : [];
1234
+ }
1235
+
1236
+ $distances = [];
1237
+ $sampledTotal = 0;
1238
+
1239
+ for ($i = 0; $i < $places->count() - 1; $i++) {
1240
+ $start = $this->getLngLatFromPlace($places[$i]);
1241
+ $end = $this->getLngLatFromPlace($places[$i + 1]);
1242
+
1243
+ if (!$start || !$end) {
1244
+ continue;
1245
+ }
1246
+
1247
+ $legDistance = $this->haversineDistanceInMeters($start['lat'], $start['lng'], $end['lat'], $end['lng']);
1248
+ $steps = max(1, min(500, (int) ceil($legDistance / 1000)));
1249
+
1250
+ for ($step = 0; $step < $steps; $step++) {
1251
+ $fromRatio = $step / $steps;
1252
+ $toRatio = ($step + 1) / $steps;
1253
+ $midRatio = ($fromRatio + $toRatio) / 2;
1254
+
1255
+ $from = $this->interpolateLngLat($start, $end, $fromRatio);
1256
+ $to = $this->interpolateLngLat($start, $end, $toRatio);
1257
+ $mid = $this->interpolateLngLat($start, $end, $midRatio);
1258
+
1259
+ $sampleDistance = $this->haversineDistanceInMeters($from['lat'], $from['lng'], $to['lat'], $to['lng']);
1260
+ $sampledTotal += $sampleDistance;
1261
+
1262
+ $matchedRule = $this->matchMultiZoneRule($mid, $ruleGeometries) ?: $fallbackRule;
1263
+ if (!$matchedRule instanceof ServiceRateFee) {
1264
+ continue;
1265
+ }
1266
+
1267
+ $key = $matchedRule->uuid ?? spl_object_hash($matchedRule);
1268
+ if (!isset($distances[$key])) {
1269
+ $distances[$key] = ['rule' => $matchedRule, 'distance_m' => 0];
1270
+ }
1271
+
1272
+ $distances[$key]['distance_m'] += $sampleDistance;
1273
+ }
1274
+ }
1275
+
1276
+ if ($sampledTotal > 0 && $totalDistance > 0) {
1277
+ $scale = $totalDistance / $sampledTotal;
1278
+ foreach ($distances as &$entry) {
1279
+ $entry['distance_m'] *= $scale;
1280
+ }
1281
+ unset($entry);
1282
+ }
1283
+
1284
+ return array_values($distances);
1285
+ }
1286
+
1287
+ protected function readRateRuleGeometry(ServiceRateFee $rule, GeoJSONReader $reader)
1288
+ {
1289
+ $border = data_get($rule, 'zone.border') ?: data_get($rule, 'serviceArea.border');
1290
+
1291
+ if (!$border) {
1292
+ return null;
1293
+ }
1294
+
1295
+ try {
1296
+ if (is_object($border) && method_exists($border, 'toJson')) {
1297
+ return $reader->read($border->toJson());
1298
+ }
1299
+
1300
+ if (is_array($border) || is_object($border)) {
1301
+ return $reader->read(json_encode($border, JSON_UNESCAPED_UNICODE));
1302
+ }
1303
+
1304
+ if (is_string($border)) {
1305
+ return $reader->read($border);
1306
+ }
1307
+ } catch (\Throwable $e) {
1308
+ return null;
1309
+ }
1310
+
1311
+ return null;
1312
+ }
1313
+
1314
+ protected function matchMultiZoneRule(array $point, $ruleGeometries): ?ServiceRateFee
1315
+ {
1316
+ $brickPoint = \Brick\Geo\Point::fromText(sprintf('POINT (%F %F)', $point['lng'], $point['lat']), 4326);
1317
+
1318
+ foreach ($ruleGeometries as $entry) {
1319
+ if ($entry['geometry']->contains($brickPoint)) {
1320
+ return $entry['rule'];
1321
+ }
1322
+ }
1323
+
1324
+ return null;
1325
+ }
1326
+
1327
+ protected function getLngLatFromPlace(?Place $place): ?array
1328
+ {
1329
+ if (!$place instanceof Place) {
1330
+ return null;
1331
+ }
1332
+
1333
+ $point = $place->getLocationAsPoint();
1334
+
1335
+ if (!$point || !method_exists($point, 'getLat') || !method_exists($point, 'getLng')) {
1336
+ return null;
1337
+ }
1338
+
1339
+ return ['lat' => (float) $point->getLat(), 'lng' => (float) $point->getLng()];
1340
+ }
1341
+
1342
+ protected function interpolateLngLat(array $start, array $end, float $ratio): array
1343
+ {
1344
+ return [
1345
+ 'lat' => $start['lat'] + (($end['lat'] - $start['lat']) * $ratio),
1346
+ 'lng' => $start['lng'] + (($end['lng'] - $start['lng']) * $ratio),
1347
+ ];
1348
+ }
1349
+
1350
+ protected function haversineDistanceInMeters(float $lat1, float $lng1, float $lat2, float $lng2): float
1351
+ {
1352
+ $earthRadius = 6371000;
1353
+ $latDelta = deg2rad($lat2 - $lat1);
1354
+ $lngDelta = deg2rad($lng2 - $lng1);
1355
+
1356
+ $a = sin($latDelta / 2) ** 2 + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($lngDelta / 2) ** 2;
1357
+
1358
+ return $earthRadius * 2 * atan2(sqrt($a), sqrt(1 - $a));
1359
+ }
1360
+
1089
1361
  /**
1090
1362
  * Find the ServiceRateFee based on the total distance.
1091
1363
  *
@@ -7,6 +7,7 @@ use Fleetbase\FleetOps\Support\Utils;
7
7
  use Fleetbase\Models\Model;
8
8
  use Fleetbase\Traits\HasUuid;
9
9
  use Fleetbase\Traits\TracksApiCredential;
10
+ use Illuminate\Database\Eloquent\Relations\BelongsTo;
10
11
 
11
12
  class ServiceRateFee extends Model
12
13
  {
@@ -32,7 +33,22 @@ class ServiceRateFee extends Model
32
33
  *
33
34
  * @var array
34
35
  */
35
- protected $fillable = ['_key', 'service_rate_uuid', 'distance', 'distance_unit', 'min', 'max', 'unit', 'fee', 'currency'];
36
+ protected $fillable = [
37
+ '_key',
38
+ 'service_rate_uuid',
39
+ 'service_area_uuid',
40
+ 'zone_uuid',
41
+ 'label',
42
+ 'priority',
43
+ 'is_fallback',
44
+ 'distance',
45
+ 'distance_unit',
46
+ 'min',
47
+ 'max',
48
+ 'unit',
49
+ 'fee',
50
+ 'currency',
51
+ ];
36
52
 
37
53
  /**
38
54
  * The attributes that should be cast to native types.
@@ -40,10 +56,12 @@ class ServiceRateFee extends Model
40
56
  * @var array
41
57
  */
42
58
  protected $casts = [
43
- 'min' => 'integer',
44
- 'max' => 'integer',
45
- 'fee' => Money::class,
46
- 'distance' => 'integer',
59
+ 'min' => 'integer',
60
+ 'max' => 'integer',
61
+ 'fee' => Money::class,
62
+ 'distance' => 'integer',
63
+ 'priority' => 'integer',
64
+ 'is_fallback' => 'boolean',
47
65
  ];
48
66
 
49
67
  /**
@@ -62,14 +80,26 @@ class ServiceRateFee extends Model
62
80
 
63
81
  public static function onRowInsert($row)
64
82
  {
65
- $row['fee'] = Money::apply($row['fee'] ?? 0);
66
- $row['distance'] = Utils::numbersOnly($row['distance'] ?? null);
67
- $row['min'] = Utils::numbersOnly($row['min'] ?? null);
68
- $row['max'] = Utils::numbersOnly($row['max'] ?? null);
83
+ $row['fee'] = Money::apply($row['fee'] ?? 0);
84
+ $row['distance'] = Utils::numbersOnly($row['distance'] ?? null);
85
+ $row['min'] = Utils::numbersOnly($row['min'] ?? null);
86
+ $row['max'] = Utils::numbersOnly($row['max'] ?? null);
87
+ $row['priority'] = Utils::numbersOnly($row['priority'] ?? 0);
88
+ $row['is_fallback'] = Utils::castBoolean($row['is_fallback'] ?? false);
69
89
 
70
90
  return $row;
71
91
  }
72
92
 
93
+ public function serviceArea(): BelongsTo
94
+ {
95
+ return $this->belongsTo(ServiceArea::class, 'service_area_uuid', 'uuid');
96
+ }
97
+
98
+ public function zone(): BelongsTo
99
+ {
100
+ return $this->belongsTo(Zone::class, 'zone_uuid', 'uuid');
101
+ }
102
+
73
103
  public function setDistanceAttribute($value)
74
104
  {
75
105
  $this->attributes['distance'] = Utils::numbersOnly($value);
@@ -14,6 +14,7 @@ use Fleetbase\Traits\SendsWebhooks;
14
14
  use Fleetbase\Traits\TracksApiCredential;
15
15
  use Illuminate\Support\Carbon;
16
16
  use Illuminate\Support\Facades\DB;
17
+ use Illuminate\Support\Facades\Schema;
17
18
  use Illuminate\Support\Str;
18
19
  use Milon\Barcode\Facades\DNS2DFacade as DNS2D;
19
20
 
@@ -242,7 +243,7 @@ class TrackingNumber extends Model
242
243
  $status = strtolower($trackingStatus->code);
243
244
  $owner = $this->load(['owner'])->getRelationValue('owner');
244
245
 
245
- if ($owner && $owner->isFillable('status') && $owner->status !== $status) {
246
+ if ($owner && static::ownerHasStatusColumn($owner) && $owner->isFillable('status') && $owner->status !== $status) {
246
247
  $this->owner->status = $status;
247
248
 
248
249
  $this->owner->save();
@@ -303,7 +304,7 @@ class TrackingNumber extends Model
303
304
  TrackingNumber::where('uuid', $uuid)->update(['status_uuid' => $trackingStatusId]);
304
305
 
305
306
  // update owner status
306
- if ($owner && $owner instanceof Model && $owner->isFillable('status')) {
307
+ if ($owner && $owner instanceof Model && static::ownerHasStatusColumn($owner) && $owner->isFillable('status')) {
307
308
  // runs event cycles
308
309
  // $model->update([ 'status' => 'created' ]);
309
310
 
@@ -313,4 +314,9 @@ class TrackingNumber extends Model
313
314
 
314
315
  return $uuid;
315
316
  }
317
+
318
+ protected static function ownerHasStatusColumn(Model $owner): bool
319
+ {
320
+ return Schema::hasColumn($owner->getTable(), 'status');
321
+ }
316
322
  }
@@ -223,6 +223,7 @@ class Vehicle extends Model
223
223
  */
224
224
  protected $attributes = [
225
225
  'avatar_url' => 'https://flb-assets.s3-ap-southeast-1.amazonaws.com/static/vehicle-icons/mini_bus.svg',
226
+ 'status' => 'available',
226
227
  ];
227
228
 
228
229
  /**
@@ -324,6 +325,14 @@ class Vehicle extends Model
324
325
  return $this->hasOne(Driver::class, 'vehicle_uuid', 'uuid')->without(['vehicle']);
325
326
  }
326
327
 
328
+ /**
329
+ * Set the vehicle status attribute.
330
+ */
331
+ public function setStatusAttribute(?string $status = 'available'): void
332
+ {
333
+ $this->attributes['status'] = $status === null || $status === 'active' ? 'available' : $status;
334
+ }
335
+
327
336
  public function category(): BelongsTo
328
337
  {
329
338
  return $this->belongsTo(Category::class, 'category_uuid', 'uuid');
@@ -597,6 +606,14 @@ class Vehicle extends Model
597
606
  return $this;
598
607
  }
599
608
 
609
+ public function unassignDriver(): self
610
+ {
611
+ Driver::where('vehicle_uuid', $this->uuid)->update(['vehicle_uuid' => null]);
612
+ $this->unsetRelation('driver');
613
+
614
+ return $this;
615
+ }
616
+
600
617
  /**
601
618
  * Retrieves the last known position of the driver within the current company context.
602
619
  *
@@ -727,9 +744,8 @@ class Vehicle extends Model
727
744
  'plate_number' => $plateNumber,
728
745
  'vin' => $vin,
729
746
  'type' => $type,
730
- 'status' => 'active',
747
+ 'status' => 'available',
731
748
  'online' => 0,
732
- 'status' => 'active',
733
749
  ]);
734
750
 
735
751
  // If driver was resolved assign driver to vehicle
@@ -2,8 +2,8 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Models;
4
4
 
5
- use Barryvdh\DomPDF\Facade\Pdf;
6
5
  use Fleetbase\Casts\PolymorphicType;
6
+ use Fleetbase\FleetOps\Support\LabelPdf;
7
7
  use Fleetbase\FleetOps\Support\Utils;
8
8
  use Fleetbase\FleetOps\Traits\HasTrackingNumber;
9
9
  use Fleetbase\FleetOps\Traits\PayloadAccessors;
@@ -94,7 +94,7 @@ class Waypoint extends Model
94
94
  */
95
95
  public function pdfLabel()
96
96
  {
97
- return Pdf::loadHTML($this->label());
97
+ return LabelPdf::fromHtml($this->label());
98
98
  }
99
99
 
100
100
  /**
@@ -0,0 +1,48 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Notifications;
4
+
5
+ use Fleetbase\FleetOps\Models\Order;
6
+ use Illuminate\Bus\Queueable;
7
+ use Illuminate\Contracts\Queue\ShouldQueue;
8
+ use Illuminate\Notifications\Messages\MailMessage;
9
+ use Illuminate\Notifications\Notification;
10
+
11
+ class LateDeparture extends Notification implements ShouldQueue
12
+ {
13
+ use Queueable;
14
+
15
+ public static string $name = 'Late Departure';
16
+ public static string $description = 'Notify when an order has not departed after the configured grace period.';
17
+ public static string $package = 'fleet-ops';
18
+
19
+ public function __construct(public Order $order, public array $context = [])
20
+ {
21
+ }
22
+
23
+ public function via($notifiable): array
24
+ {
25
+ return ['mail', 'database'];
26
+ }
27
+
28
+ public function toMail($notifiable): MailMessage
29
+ {
30
+ $orderId = $this->order->tracking ?? $this->order->public_id;
31
+
32
+ return (new MailMessage())
33
+ ->subject("Late departure detected for {$orderId}")
34
+ ->line("Order {$orderId} has not departed within the configured grace period.")
35
+ ->line('Please review the order and dispatch status.');
36
+ }
37
+
38
+ public function toArray($notifiable): array
39
+ {
40
+ return [
41
+ 'event' => 'order.late_departure',
42
+ 'order_id' => $this->order->public_id,
43
+ 'order_uuid' => $this->order->uuid,
44
+ 'context' => $this->context,
45
+ 'message' => sprintf('Order %s has not departed within the configured grace period.', $this->order->tracking ?? $this->order->public_id),
46
+ ];
47
+ }
48
+ }
@@ -0,0 +1,48 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Notifications;
4
+
5
+ use Fleetbase\FleetOps\Models\Order;
6
+ use Illuminate\Bus\Queueable;
7
+ use Illuminate\Contracts\Queue\ShouldQueue;
8
+ use Illuminate\Notifications\Messages\MailMessage;
9
+ use Illuminate\Notifications\Notification;
10
+
11
+ class ProlongedStoppage extends Notification implements ShouldQueue
12
+ {
13
+ use Queueable;
14
+
15
+ public static string $name = 'Prolonged Stoppage';
16
+ public static string $description = 'Notify when a vehicle remains stopped during an active trip beyond the configured threshold.';
17
+ public static string $package = 'fleet-ops';
18
+
19
+ public function __construct(public Order $order, public array $context = [])
20
+ {
21
+ }
22
+
23
+ public function via($notifiable): array
24
+ {
25
+ return ['mail', 'database'];
26
+ }
27
+
28
+ public function toMail($notifiable): MailMessage
29
+ {
30
+ $orderId = $this->order->tracking ?? $this->order->public_id;
31
+
32
+ return (new MailMessage())
33
+ ->subject("Prolonged stoppage detected for {$orderId}")
34
+ ->line("The vehicle assigned to order {$orderId} has remained stopped beyond the configured threshold.")
35
+ ->line('Please review the active trip and vehicle location.');
36
+ }
37
+
38
+ public function toArray($notifiable): array
39
+ {
40
+ return [
41
+ 'event' => 'order.prolonged_stoppage',
42
+ 'order_id' => $this->order->public_id,
43
+ 'order_uuid' => $this->order->uuid,
44
+ 'context' => $this->context,
45
+ 'message' => sprintf('The vehicle assigned to order %s has remained stopped beyond the configured threshold.', $this->order->tracking ?? $this->order->public_id),
46
+ ];
47
+ }
48
+ }
@@ -0,0 +1,48 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Notifications;
4
+
5
+ use Fleetbase\FleetOps\Models\Order;
6
+ use Illuminate\Bus\Queueable;
7
+ use Illuminate\Contracts\Queue\ShouldQueue;
8
+ use Illuminate\Notifications\Messages\MailMessage;
9
+ use Illuminate\Notifications\Notification;
10
+
11
+ class RouteDeviation extends Notification implements ShouldQueue
12
+ {
13
+ use Queueable;
14
+
15
+ public static string $name = 'Route Deviation';
16
+ public static string $description = 'Notify when tracked vehicle location deviates from the active route beyond the configured threshold.';
17
+ public static string $package = 'fleet-ops';
18
+
19
+ public function __construct(public Order $order, public array $context = [])
20
+ {
21
+ }
22
+
23
+ public function via($notifiable): array
24
+ {
25
+ return ['mail', 'database'];
26
+ }
27
+
28
+ public function toMail($notifiable): MailMessage
29
+ {
30
+ $orderId = $this->order->tracking ?? $this->order->public_id;
31
+
32
+ return (new MailMessage())
33
+ ->subject("Route deviation detected for {$orderId}")
34
+ ->line("The vehicle assigned to order {$orderId} has deviated from the active route beyond the configured threshold.")
35
+ ->line('Please review the route and current vehicle position.');
36
+ }
37
+
38
+ public function toArray($notifiable): array
39
+ {
40
+ return [
41
+ 'event' => 'order.route_deviation',
42
+ 'order_id' => $this->order->public_id,
43
+ 'order_uuid' => $this->order->uuid,
44
+ 'context' => $this->context,
45
+ 'message' => sprintf('The vehicle assigned to order %s has deviated from the active route beyond the configured threshold.', $this->order->tracking ?? $this->order->public_id),
46
+ ];
47
+ }
48
+ }
@@ -31,6 +31,8 @@ class ContactObserver
31
31
  throw new \Exception('Customer contact type cannot be changed.');
32
32
  }
33
33
 
34
+ $contact->assertCustomerIdentityIsAvailable();
35
+
34
36
  // Get the contacts assosciated user
35
37
  if ($contact->doesntHaveUser()) {
36
38
  $contact->createUser();