@fleetbase/fleetops-engine 0.6.49 → 0.6.51

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 (271) hide show
  1. package/README.md +185 -67
  2. package/addon/components/contact/panel-header.hbs +11 -48
  3. package/addon/components/customer/create-order-form.hbs +2 -2
  4. package/addon/components/customer/details.hbs +1 -2
  5. package/addon/components/customer/form.hbs +62 -50
  6. package/addon/components/customer/form.js +27 -1
  7. package/addon/components/customer/order-form.hbs +2 -2
  8. package/addon/components/customer/orders.hbs +50 -2
  9. package/addon/components/customer/orders.js +67 -5
  10. package/addon/components/display-place.hbs +2 -2
  11. package/addon/components/driver/form.hbs +1 -1
  12. package/addon/components/home/getting-started-guidance.hbs +113 -0
  13. package/addon/components/home/getting-started-guidance.js +107 -0
  14. package/addon/components/issue/details/comments.hbs +3 -0
  15. package/addon/components/issue/details/comments.js +3 -0
  16. package/addon/components/issue/details/correspondence.hbs +9 -0
  17. package/addon/components/issue/details/correspondence.js +83 -0
  18. package/addon/components/issue/details/documents.hbs +69 -0
  19. package/addon/components/issue/details/documents.js +85 -0
  20. package/addon/components/issue/details.hbs +220 -59
  21. package/addon/components/issue/details.js +245 -0
  22. package/addon/components/issue/form.hbs +19 -1
  23. package/addon/components/issue/panel-header.hbs +43 -0
  24. package/addon/components/issue/panel-header.js +22 -0
  25. package/addon/components/issue/timeline.hbs +40 -0
  26. package/addon/components/issue/timeline.js +45 -0
  27. package/addon/components/leaflet-tracking-marker.js +3 -0
  28. package/addon/components/map/drawer/position-listing.js +3 -0
  29. package/addon/components/map/google-live-map.js +6 -2
  30. package/addon/components/map/leaflet-live-map.hbs +48 -14
  31. package/addon/components/map/leaflet-live-map.js +201 -20
  32. package/addon/components/map/order-list-overlay.hbs +4 -4
  33. package/addon/components/modals/convert-customer-to-vendor.hbs +30 -0
  34. package/addon/components/modals/convert-customer-to-vendor.js +55 -0
  35. package/addon/components/modals/issue-assign.hbs +20 -0
  36. package/addon/components/modals/issue-assign.js +49 -0
  37. package/addon/components/modals/issue-close.hbs +30 -0
  38. package/addon/components/modals/issue-close.js +73 -0
  39. package/addon/components/modals/issue-status.hbs +20 -0
  40. package/addon/components/modals/issue-status.js +43 -0
  41. package/addon/components/modals/order-change-destination.hbs +46 -0
  42. package/addon/components/modals/order-change-destination.js +106 -0
  43. package/addon/components/modals/reset-customer-credentials.js +12 -6
  44. package/addon/components/modals/stop-activity.hbs +28 -0
  45. package/addon/components/modals/stop-activity.js +52 -0
  46. package/addon/components/modals/update-order-activity.hbs +78 -2
  47. package/addon/components/modals/update-order-activity.js +197 -5
  48. package/addon/components/orchestrator/resource-panel.hbs +0 -10
  49. package/addon/components/orchestrator/resource-panel.js +2 -4
  50. package/addon/components/order/activity-list.hbs +2 -2
  51. package/addon/components/order/activity-timeline.hbs +31 -29
  52. package/addon/components/order/details/activity.hbs +3 -3
  53. package/addon/components/order/details/activity.js +13 -2
  54. package/addon/components/order/details/documents.js +2 -0
  55. package/addon/components/order/details/proof.hbs +40 -38
  56. package/addon/components/order/details/proof.js +31 -4
  57. package/addon/components/order/details/purchase-rate.hbs +2 -2
  58. package/addon/components/order/details/route.hbs +11 -3
  59. package/addon/components/order/details/route.js +91 -14
  60. package/addon/components/order/details/tracking.hbs +101 -72
  61. package/addon/components/order/details/tracking.js +149 -2
  62. package/addon/components/order/details.hbs +5 -5
  63. package/addon/components/order/details.js +19 -1
  64. package/addon/components/order/form/details.hbs +3 -3
  65. package/addon/components/order/form/documents.js +11 -4
  66. package/addon/components/order/form/route.hbs +108 -104
  67. package/addon/components/order/form/route.js +37 -1
  68. package/addon/components/order/form/service-rate.hbs +2 -2
  69. package/addon/components/order/header.hbs +1 -1
  70. package/addon/components/order-config-manager/activity-flow.hbs +4 -1
  71. package/addon/components/order-config-manager/activity-flow.js +178 -12
  72. package/addon/components/place/address.js +1 -1
  73. package/addon/components/route-list.hbs +17 -29
  74. package/addon/components/route-list.js +42 -10
  75. package/addon/components/service-rate/details.hbs +42 -1
  76. package/addon/components/service-rate/form.hbs +119 -1
  77. package/addon/components/service-rate/form.js +59 -0
  78. package/addon/components/vehicle/form.js +1 -1
  79. package/addon/components/vehicle/pill.hbs +1 -1
  80. package/addon/components/vendor/personnel-manager.hbs +166 -0
  81. package/addon/components/vendor/personnel-manager.js +143 -0
  82. package/addon/components/widget/live-fleet.hbs +1 -0
  83. package/addon/components/widget/live-fleet.js +2 -7
  84. package/addon/components/widget/revenue-trend.hbs +1 -1
  85. package/addon/components/widget/revenue-trend.js +75 -2
  86. package/addon/controllers/management/contacts/customers/details.js +31 -13
  87. package/addon/controllers/management/contacts/customers.js +16 -5
  88. package/addon/controllers/management/contacts/index/details.js +31 -13
  89. package/addon/controllers/management/contacts/index.js +7 -0
  90. package/addon/controllers/management/issues/index/details.js +52 -6
  91. package/addon/controllers/management/vehicles/index/new.js +1 -1
  92. package/addon/controllers/management/vendors/index/details.js +4 -0
  93. package/addon/controllers/operations/orders/index/details.js +15 -7
  94. package/addon/controllers/operations/orders/index.js +15 -1
  95. package/addon/extension.js +19 -12
  96. package/addon/helpers/format-duration.js +1 -38
  97. package/addon/helpers/place-address-html.js +2 -2
  98. package/addon/initializers/load-leaflet-assets.js +5 -6
  99. package/addon/initializers/patch-ember-leaflet-tooltip-layer.js +51 -0
  100. package/addon/routes/management/issues/index/details.js +1 -1
  101. package/addon/routes/operations/service-rates/index.js +1 -1
  102. package/addon/routes.js +1 -0
  103. package/addon/services/contact-actions.js +231 -1
  104. package/addon/services/geofence.js +163 -19
  105. package/addon/services/getting-started.js +44 -0
  106. package/addon/services/issue-actions.js +71 -0
  107. package/addon/services/leaflet-contextmenu-manager.js +27 -0
  108. package/addon/services/leaflet-map-manager.js +2 -0
  109. package/addon/services/map-adapter/leaflet.js +19 -0
  110. package/addon/services/service-areas.js +3 -0
  111. package/addon/services/vehicle-actions.js +1 -1
  112. package/addon/styles/fleetops-engine.css +1127 -1
  113. package/addon/templates/management/contacts/customers/details.hbs +1 -1
  114. package/addon/templates/management/contacts/index/details.hbs +1 -1
  115. package/addon/templates/management/contacts.hbs +2 -2
  116. package/addon/templates/management/issues/index/details/index.hbs +3 -1
  117. package/addon/templates/management/issues/index/details.hbs +3 -2
  118. package/addon/templates/management/vendors/index/details/personnel.hbs +1 -2
  119. package/addon/templates/operations/orders/index/details/index.hbs +2 -2
  120. package/addon/templates/operations/orders/index.hbs +1 -1
  121. package/addon/templates/operations/service-rates/index/edit.hbs +1 -0
  122. package/addon/templates/operations/service-rates/index/new.hbs +1 -0
  123. package/addon/templates/settings/payments/onboard.hbs +8 -8
  124. package/addon/utils/fleet-ops-options.js +2 -1
  125. package/addon/utils/leaflet-draw-namespace-guard.js +22 -0
  126. package/addon/utils/leaflet-plugin-loader.js +219 -0
  127. package/addon/utils/normalize-order-config-flow.js +41 -0
  128. package/addon/utils/place-address-html.js +3 -2
  129. package/addon/utils/route-visualization.js +54 -7
  130. package/addon/utils/setup-customer-portal.js +4 -64
  131. package/app/components/home/getting-started-guidance.js +1 -0
  132. package/app/components/issue/details/comments.js +1 -0
  133. package/app/components/issue/details/correspondence.js +1 -0
  134. package/app/components/issue/details/documents.js +1 -0
  135. package/app/components/issue/panel-header.js +1 -0
  136. package/app/components/modals/convert-customer-to-vendor.js +1 -0
  137. package/app/components/modals/issue-assign.js +1 -0
  138. package/app/components/modals/issue-close.js +1 -0
  139. package/app/components/modals/issue-status.js +1 -0
  140. package/app/components/modals/order-change-destination.js +1 -0
  141. package/app/components/modals/stop-activity.js +1 -0
  142. package/app/components/vendor/personnel-manager.js +1 -0
  143. package/app/helpers/format-duration.js +1 -1
  144. package/app/services/getting-started.js +1 -0
  145. package/app/utils/leaflet-draw-namespace-guard.js +1 -0
  146. package/app/utils/leaflet-plugin-loader.js +1 -0
  147. package/composer.json +1 -1
  148. package/extension.json +1 -1
  149. package/package.json +5 -5
  150. package/server/migrations/2026_05_21_000001_add_geographic_rules_to_service_rate_fees_table.php +37 -0
  151. package/server/migrations/2026_05_25_000001_add_endpoint_tracking_refs_to_payloads_table.php +50 -0
  152. package/server/migrations/2026_05_27_000001_default_driver_vehicle_statuses_to_available.php +48 -0
  153. package/server/migrations/2026_05_31_000001_add_portal_fields_to_vendor_personnels_table.php +30 -0
  154. package/server/migrations/2026_06_03_000000_ensure_tags_column_on_issues_table.php +31 -0
  155. package/server/migrations/2026_06_03_000001_add_order_uuid_to_issues_table.php +25 -0
  156. package/server/migrations/2026_06_04_000001_backfill_order_transaction_subjects.php +39 -0
  157. package/server/migrations/2026_06_04_000002_backfill_order_transaction_directions.php +24 -0
  158. package/server/resources/views/labels/default.php +57 -49
  159. package/server/resources/views/labels/waypoint-label.php +27 -20
  160. package/server/resources/views/mail/customer-credentials.blade.php +16 -13
  161. package/server/seeders/Testing/ConnectivitySeeder.php +1 -0
  162. package/server/seeders/Testing/FleetSeeder.php +22 -22
  163. package/server/seeders/Testing/OrdersSeeder.php +6 -22
  164. package/server/seeders/Testing/VroomOrchestrationSeeder.php +21 -21
  165. package/server/src/Console/Commands/AuditCustomerUserConflicts.php +75 -0
  166. package/server/src/Exceptions/CustomerUserConflictException.php +13 -0
  167. package/server/src/Expansions/UserFilterExpansion.php +28 -0
  168. package/server/src/Http/Controllers/Api/v1/ContactController.php +17 -2
  169. package/server/src/Http/Controllers/Api/v1/CustomerController.php +876 -0
  170. package/server/src/Http/Controllers/Api/v1/DriverController.php +1 -1
  171. package/server/src/Http/Controllers/Api/v1/IssueController.php +2 -0
  172. package/server/src/Http/Controllers/Api/v1/OrderConfigController.php +54 -0
  173. package/server/src/Http/Controllers/Api/v1/OrderController.php +109 -80
  174. package/server/src/Http/Controllers/Api/v1/PlaceController.php +10 -58
  175. package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -2
  176. package/server/src/Http/Controllers/Api/v1/VehicleController.php +20 -3
  177. package/server/src/Http/Controllers/Internal/v1/ContactController.php +154 -0
  178. package/server/src/Http/Controllers/Internal/v1/CustomerController.php +116 -6
  179. package/server/src/Http/Controllers/Internal/v1/GettingStartedController.php +17 -0
  180. package/server/src/Http/Controllers/Internal/v1/IssueController.php +251 -0
  181. package/server/src/Http/Controllers/Internal/v1/LiveController.php +101 -66
  182. package/server/src/Http/Controllers/Internal/v1/OrderController.php +530 -52
  183. package/server/src/Http/Controllers/Internal/v1/PlaceController.php +10 -68
  184. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
  185. package/server/src/Http/Controllers/Internal/v1/SettingController.php +2 -2
  186. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +54 -1
  187. package/server/src/Http/Controllers/Internal/v1/VendorController.php +89 -0
  188. package/server/src/Http/Middleware/AuthenticateCustomerToken.php +34 -0
  189. package/server/src/Http/Requests/CreateCustomerOrderRequest.php +63 -0
  190. package/server/src/Http/Requests/CreateCustomerRequest.php +48 -0
  191. package/server/src/Http/Requests/CreateDriverRequest.php +1 -1
  192. package/server/src/Http/Requests/CreateIssueRequest.php +4 -0
  193. package/server/src/Http/Requests/CreateVehicleRequest.php +25 -1
  194. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +1 -1
  195. package/server/src/Http/Requests/UpdateIssueRequest.php +4 -0
  196. package/server/src/Http/Requests/VerifyCreateCustomerRequest.php +29 -0
  197. package/server/src/Http/Resources/Internal/v1/OrderConfig.php +41 -0
  198. package/server/src/Http/Resources/v1/Customer.php +79 -0
  199. package/server/src/Http/Resources/v1/Index/Order.php +12 -9
  200. package/server/src/Http/Resources/v1/Issue.php +25 -0
  201. package/server/src/Http/Resources/v1/Order.php +20 -18
  202. package/server/src/Http/Resources/v1/OrderConfig.php +74 -0
  203. package/server/src/Http/Resources/v1/Payload.php +23 -20
  204. package/server/src/Http/Resources/v1/ServiceArea.php +33 -19
  205. package/server/src/Http/Resources/v1/ServiceRateFee.php +7 -0
  206. package/server/src/Listeners/HandleOrderDispatched.php +2 -2
  207. package/server/src/Mail/CustomerCredentialsMail.php +5 -6
  208. package/server/src/Models/Contact.php +122 -66
  209. package/server/src/Models/Customer.php +64 -0
  210. package/server/src/Models/Driver.php +12 -3
  211. package/server/src/Models/Entity.php +2 -2
  212. package/server/src/Models/Issue.php +47 -6
  213. package/server/src/Models/Order.php +62 -10
  214. package/server/src/Models/Payload.php +17 -1
  215. package/server/src/Models/ServiceArea.php +1 -1
  216. package/server/src/Models/ServiceRate.php +273 -1
  217. package/server/src/Models/ServiceRateFee.php +39 -9
  218. package/server/src/Models/TrackingNumber.php +8 -2
  219. package/server/src/Models/Vehicle.php +18 -2
  220. package/server/src/Models/VendorPersonnel.php +10 -1
  221. package/server/src/Models/Waypoint.php +2 -2
  222. package/server/src/Models/Zone.php +1 -1
  223. package/server/src/Notifications/LateDeparture.php +2 -2
  224. package/server/src/Notifications/ProlongedStoppage.php +2 -2
  225. package/server/src/Notifications/RouteDeviation.php +2 -2
  226. package/server/src/Observers/ContactObserver.php +2 -0
  227. package/server/src/Observers/PurchaseRateObserver.php +20 -0
  228. package/server/src/Observers/ServiceRateObserver.php +6 -6
  229. package/server/src/Observers/ZoneObserver.php +56 -0
  230. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +1 -1
  231. package/server/src/Providers/FleetOpsServiceProvider.php +2 -0
  232. package/server/src/Support/Analytics/AbstractAnalytics.php +6 -6
  233. package/server/src/Support/Analytics/FuelEfficiency.php +2 -2
  234. package/server/src/Support/Analytics/OrdersByStatus.php +1 -1
  235. package/server/src/Support/Analytics/RevenueTrend.php +0 -1
  236. package/server/src/Support/Analytics/TopDrivers.php +10 -2
  237. package/server/src/Support/CustomerAuth.php +98 -0
  238. package/server/src/Support/GettingStarted.php +144 -0
  239. package/server/src/Support/LabelPdf.php +52 -0
  240. package/server/src/Support/Metrics/AbstractMetric.php +4 -4
  241. package/server/src/Support/PlaceSearch.php +282 -0
  242. package/server/src/Support/ResolvesOrderServiceStops.php +522 -0
  243. package/server/src/Tracking/TrackingContextBuilder.php +41 -38
  244. package/server/src/Tracking/TrackingIntelligenceService.php +109 -9
  245. package/server/src/Tracking/TrackingStop.php +13 -11
  246. package/server/src/routes.php +76 -10
  247. package/server/tests/AnalyticsRoutesTest.php +8 -0
  248. package/server/tests/CapacityAllocationEngineTest.php +10 -10
  249. package/server/tests/CustomerContactIdentitySafetyTest.php +95 -0
  250. package/server/tests/CustomerEndpointTest.php +199 -0
  251. package/server/tests/DriverVehicleStatusDefaultTest.php +39 -0
  252. package/server/tests/GettingStartedStatusTest.php +21 -0
  253. package/server/tests/LabelPdfRenderingTest.php +23 -0
  254. package/server/tests/LiveControllerViewportTest.php +60 -0
  255. package/server/tests/MetricsRegistryTest.php +4 -8
  256. package/server/tests/OrchestrationPayloadBuilderTest.php +2 -2
  257. package/server/tests/OrderActivityFlowRegressionTest.php +116 -0
  258. package/server/tests/OrderConfigEndpointTest.php +86 -0
  259. package/server/tests/OrderFileAttachmentTest.php +20 -0
  260. package/server/tests/ServiceAreaZoneApiShapeTest.php +78 -0
  261. package/server/tests/VehicleDriverAssignmentTest.php +18 -0
  262. package/translations/ar-ae.yml +9 -0
  263. package/translations/bg-bg.yaml +9 -0
  264. package/translations/en-us.yaml +12 -0
  265. package/translations/es-pa.yaml +9 -0
  266. package/translations/fr-fr.yaml +9 -0
  267. package/translations/mn-mn.yaml +9 -0
  268. package/translations/pt-br.yaml +9 -0
  269. package/translations/ru-ru.yaml +9 -0
  270. package/translations/vi-vn.yaml +9 -0
  271. package/server/migrations/2026_05_13_000001_repair_customer_contact_user_types.php +0 -86
@@ -2,7 +2,6 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Models;
4
4
 
5
- use Barryvdh\DomPDF\Facade\Pdf;
6
5
  use Fleetbase\Casts\Json;
7
6
  use Fleetbase\Casts\PolymorphicType;
8
7
  use Fleetbase\FleetOps\Events\OrderCanceled;
@@ -10,6 +9,7 @@ use Fleetbase\FleetOps\Events\OrderCompleted;
10
9
  use Fleetbase\FleetOps\Events\OrderDispatched;
11
10
  use Fleetbase\FleetOps\Events\OrderDriverAssigned;
12
11
  use Fleetbase\FleetOps\Flow\Activity;
12
+ use Fleetbase\FleetOps\Support\LabelPdf;
13
13
  use Fleetbase\FleetOps\Support\OrderTracker;
14
14
  use Fleetbase\FleetOps\Support\Utils;
15
15
  use Fleetbase\FleetOps\Traits\HasTrackingNumber;
@@ -169,6 +169,7 @@ class Order extends Model
169
169
  'tracking',
170
170
  'total_entities',
171
171
  'transaction_amount',
172
+ 'transaction_currency',
172
173
  'customer_name',
173
174
  'customer_phone',
174
175
  'facilitator_name',
@@ -275,7 +276,32 @@ class Order extends Model
275
276
  */
276
277
  public function pdfLabel()
277
278
  {
278
- return Pdf::loadHTML($this->label());
279
+ return LabelPdf::fromHtml($this->label());
280
+ }
281
+
282
+ public function attachFiles($uploads): self
283
+ {
284
+ $ids = collect($uploads)
285
+ ->map(function ($upload) {
286
+ if (is_string($upload)) {
287
+ return $upload;
288
+ }
289
+
290
+ return data_get($upload, 'uuid') ?? data_get($upload, 'id');
291
+ })
292
+ ->filter()
293
+ ->unique()
294
+ ->values();
295
+
296
+ if ($ids->isEmpty()) {
297
+ return $this;
298
+ }
299
+
300
+ \Fleetbase\Models\File::whereIn('uuid', $ids)->get()->each(function ($file) {
301
+ $file->setKey($this);
302
+ });
303
+
304
+ return $this;
279
305
  }
280
306
 
281
307
  /**
@@ -483,6 +509,16 @@ class Order extends Model
483
509
  return data_get($this, 'transaction.amount');
484
510
  }
485
511
 
512
+ /**
513
+ * The transaction amount for the order.
514
+ *
515
+ * @return string
516
+ */
517
+ public function getTransactionCurrencyAttribute()
518
+ {
519
+ return data_get($this, 'transaction.currency');
520
+ }
521
+
486
522
  /**
487
523
  * The customer name for the order.
488
524
  *
@@ -1118,7 +1154,17 @@ class Order extends Model
1118
1154
  $this->setRelation('purchaseRate', $purchaseRate);
1119
1155
 
1120
1156
  if ($purchaseRate->transaction_uuid) {
1121
- $this->setRelation('transaction', Transaction::where('uuid', $purchaseRate->transaction_uuid)->first());
1157
+ $transaction = Transaction::where('uuid', $purchaseRate->transaction_uuid)->first();
1158
+ if ($transaction instanceof Transaction) {
1159
+ $transaction->update([
1160
+ 'subject_uuid' => $this->uuid,
1161
+ 'subject_type' => static::class,
1162
+ 'context_uuid' => $purchaseRate->uuid,
1163
+ 'context_type' => PurchaseRate::class,
1164
+ 'direction' => Transaction::DIRECTION_CREDIT,
1165
+ ]);
1166
+ $this->setRelation('transaction', $transaction);
1167
+ }
1122
1168
  }
1123
1169
 
1124
1170
  if (
@@ -1151,12 +1197,17 @@ class Order extends Model
1151
1197
  'company_uuid' => session('company', $this->company_uuid),
1152
1198
  'customer_uuid' => $this->customer_uuid,
1153
1199
  'customer_type' => $this->customer_type,
1200
+ 'subject_uuid' => $this->uuid,
1201
+ 'subject_type' => static::class,
1202
+ 'context_uuid' => $this->uuid,
1203
+ 'context_type' => static::class,
1154
1204
  'gateway_transaction_id' => Transaction::generateNumber(),
1155
1205
  'gateway' => 'internal',
1156
1206
  'amount' => 0,
1157
1207
  'currency' => Utils::getCompanyTransactionCurrency($this->company ?? $this->company_uuid),
1158
1208
  'description' => 'Dispatch order',
1159
1209
  'type' => 'dispatch',
1210
+ 'direction' => Transaction::DIRECTION_CREDIT,
1160
1211
  'status' => 'success',
1161
1212
  ]);
1162
1213
 
@@ -1354,8 +1405,9 @@ class Order extends Model
1354
1405
  *
1355
1406
  * The process involves the following steps:
1356
1407
  * 1. Creating a new Activity instance with the 'completed' code and relevant details.
1357
- * 2. Notifying that the order has been completed via `notifyCompleted`.
1358
- * 3. Updating the order's activity with the new 'completed' activity through `updateActivity`.
1408
+ * 2. Updating the order's activity with the new 'completed' activity through `updateActivity`,
1409
+ * which applies and persists the 'completed' status.
1410
+ * 3. Notifying that the order has been completed via `notifyCompleted`.
1359
1411
  *
1360
1412
  * @param Proof|null $proof Optional. Additional proof or details for the activity update.
1361
1413
  *
@@ -1363,15 +1415,15 @@ class Order extends Model
1363
1415
  */
1364
1416
  public function complete(?Proof $proof = null): self
1365
1417
  {
1366
- $this->notifyCompleted();
1367
-
1368
1418
  $doesntHaveCompletedActivity = TrackingStatus::where(['tracking_number_uuid' => $this->tracking_number_uuid, 'code' => 'COMPLETED'])->doesntExist();
1369
1419
  if ($doesntHaveCompletedActivity) {
1370
1420
  $activity = $this->config()->getCompletedActivity();
1371
1421
 
1372
- return $this->updateActivity($activity, $proof);
1422
+ $this->updateActivity($activity, $proof);
1373
1423
  }
1374
1424
 
1425
+ $this->notifyCompleted();
1426
+
1375
1427
  return $this;
1376
1428
  }
1377
1429
 
@@ -1991,11 +2043,11 @@ class Order extends Model
1991
2043
  return collect();
1992
2044
  }
1993
2045
 
1994
- $drivers = Driver::where(['status' => 'active', 'online' => 1])
2046
+ $drivers = Driver::where(['status' => 'available', 'online' => 1])
1995
2047
  ->whereHas('company', function ($q) {
1996
2048
  $q->whereHas('users', function ($q) {
1997
2049
  $q->whereHas('driver', function ($q) {
1998
- $q->where(['status' => 'active', 'online' => 1]);
2050
+ $q->where(['status' => 'available', 'online' => 1]);
1999
2051
  $q->whereNull('deleted_at');
2000
2052
  });
2001
2053
  });
@@ -61,7 +61,23 @@ class Payload extends Model
61
61
  *
62
62
  * @var array
63
63
  */
64
- protected $fillable = ['_key', 'company_uuid', 'pickup_uuid', 'dropoff_uuid', 'return_uuid', 'current_waypoint_uuid', 'meta', 'payment_method', 'cod_amount', 'cod_currency', 'cod_payment_method', 'type'];
64
+ protected $fillable = [
65
+ '_key',
66
+ 'company_uuid',
67
+ 'pickup_uuid',
68
+ 'pickup_tracking_number_uuid',
69
+ 'dropoff_uuid',
70
+ 'dropoff_tracking_number_uuid',
71
+ 'return_uuid',
72
+ 'return_tracking_number_uuid',
73
+ 'current_waypoint_uuid',
74
+ 'meta',
75
+ 'payment_method',
76
+ 'cod_amount',
77
+ 'cod_currency',
78
+ 'cod_payment_method',
79
+ 'type',
80
+ ];
65
81
 
66
82
  /**
67
83
  * The attributes that should be cast to native types.
@@ -123,7 +123,7 @@ class ServiceArea extends Model
123
123
  */
124
124
  public function zones()
125
125
  {
126
- return $this->hasMany(Zone::class)->without(['serviceArea']);
126
+ return $this->hasMany(Zone::class, 'service_area_uuid', 'uuid')->without(['serviceArea']);
127
127
  }
128
128
 
129
129
  /**
@@ -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