@fleetbase/fleetops-engine 0.6.70 → 0.6.72

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 (510) hide show
  1. package/RELEASE.md +18 -47
  2. package/addon/components/admin/tracking-page-settings.hbs +56 -0
  3. package/addon/components/admin/tracking-page-settings.js +67 -0
  4. package/addon/components/device/form.hbs +2 -2
  5. package/addon/components/driver/details.hbs +1 -1
  6. package/addon/components/driver/form.hbs +1 -1
  7. package/addon/components/equipment/form.hbs +1 -1
  8. package/addon/components/layout/fleet-ops-sidebar.js +11 -3
  9. package/addon/components/maintenance/form.hbs +3 -3
  10. package/addon/components/map/google-live-map.js +2 -2
  11. package/addon/components/map/leaflet-live-map.js +45 -32
  12. package/addon/components/orchestrator/phase-builder.js +2 -1
  13. package/addon/components/orchestrator-workbench.hbs +7 -1
  14. package/addon/components/orchestrator-workbench.js +16 -2
  15. package/addon/components/order/details/activity.hbs +8 -6
  16. package/addon/components/order/details/activity.js +9 -0
  17. package/addon/components/order/details.hbs +29 -0
  18. package/addon/components/order/details.js +26 -0
  19. package/addon/components/order/form/details.hbs +30 -4
  20. package/addon/components/order/form/details.js +46 -0
  21. package/addon/components/order/form/route.hbs +5 -3
  22. package/addon/components/order/form/route.js +24 -1
  23. package/addon/components/order/form.hbs +12 -0
  24. package/addon/components/order/form.js +21 -1
  25. package/addon/components/order/kanban.js +72 -0
  26. package/addon/components/order-config-manager/activity-flow.js +45 -10
  27. package/addon/components/order-config-manager/lifecycle.hbs +250 -0
  28. package/addon/components/order-config-manager/lifecycle.js +358 -0
  29. package/addon/components/order-config-manager.js +5 -1
  30. package/addon/components/service-rate/details.hbs +1 -1
  31. package/addon/components/service-rate/form.hbs +1 -1
  32. package/addon/components/tracking-page/full-view.hbs +276 -0
  33. package/addon/components/tracking-page/full-view.js +474 -0
  34. package/addon/components/tracking-page/locale-selector.js +30 -0
  35. package/addon/components/tracking-page/map.hbs +44 -0
  36. package/addon/components/tracking-page/map.js +328 -0
  37. package/addon/components/tracking-page/order-list.hbs +35 -0
  38. package/addon/components/tracking-page/public-view.hbs +60 -0
  39. package/addon/components/tracking-page/settings-preview.hbs +21 -0
  40. package/addon/components/tracking-page/settings-preview.js +23 -0
  41. package/addon/components/tracking-page/verify-panel.hbs +109 -0
  42. package/addon/components/tracking-page.hbs +142 -0
  43. package/addon/components/tracking-page.js +564 -0
  44. package/addon/components/trailer/form.hbs +2 -2
  45. package/addon/components/vehicle/form.hbs +3 -3
  46. package/addon/components/warranty/form.hbs +2 -2
  47. package/addon/components/widget/live-fleet.js +54 -6
  48. package/addon/components/widget/proof-of-delivery.hbs +49 -0
  49. package/addon/components/widget/proof-of-delivery.js +62 -0
  50. package/addon/components/work-order/form.hbs +3 -3
  51. package/addon/controllers/analytics/reports/index.js +10 -1
  52. package/addon/controllers/connectivity/devices/index/details.js +1 -0
  53. package/addon/controllers/connectivity/devices/index.js +8 -2
  54. package/addon/controllers/connectivity/events/index.js +31 -1
  55. package/addon/controllers/connectivity/sensors/index/details.js +1 -1
  56. package/addon/controllers/connectivity/sensors/index.js +37 -1
  57. package/addon/controllers/maintenance/equipment/index/details.js +2 -2
  58. package/addon/controllers/maintenance/equipment/index.js +28 -9
  59. package/addon/controllers/maintenance/inspection-forms/index.js +32 -12
  60. package/addon/controllers/maintenance/inspection-submissions/index/details.js +2 -1
  61. package/addon/controllers/maintenance/inspection-submissions/index.js +49 -9
  62. package/addon/controllers/maintenance/maintenances/index/details.js +2 -0
  63. package/addon/controllers/maintenance/maintenances/index.js +20 -1
  64. package/addon/controllers/maintenance/parts/index/details.js +2 -2
  65. package/addon/controllers/maintenance/parts/index.js +28 -8
  66. package/addon/controllers/maintenance/schedules/index.js +33 -4
  67. package/addon/controllers/maintenance/work-orders/index/details.js +3 -3
  68. package/addon/controllers/maintenance/work-orders/index.js +39 -8
  69. package/addon/controllers/management/contacts/customers.js +13 -0
  70. package/addon/controllers/management/contacts/index.js +43 -1
  71. package/addon/controllers/management/drivers/index/details.js +1 -0
  72. package/addon/controllers/management/drivers/index.js +32 -2
  73. package/addon/controllers/management/fleets/index/details.js +1 -0
  74. package/addon/controllers/management/fleets/index.js +39 -1
  75. package/addon/controllers/management/fuel-reports/index.js +22 -2
  76. package/addon/controllers/management/fuel-transactions/index.js +43 -7
  77. package/addon/controllers/management/issues/index/details.js +1 -0
  78. package/addon/controllers/management/issues/index.js +22 -2
  79. package/addon/controllers/management/places/index.js +36 -1
  80. package/addon/controllers/management/trailers/index/details.js +1 -0
  81. package/addon/controllers/management/trailers/index.js +50 -2
  82. package/addon/controllers/management/vehicles/index/details.js +1 -0
  83. package/addon/controllers/management/vehicles/index.js +33 -2
  84. package/addon/controllers/management/vendors/index.js +26 -2
  85. package/addon/controllers/operations/orders/index/details.js +50 -7
  86. package/addon/controllers/operations/orders/index.js +59 -4
  87. package/addon/controllers/operations/scheduler/index.js +45 -14
  88. package/addon/controllers/operations/service-rates/index.js +15 -1
  89. package/addon/controllers/settings/index.js +6 -0
  90. package/addon/controllers/settings/map.js +75 -0
  91. package/addon/controllers/settings/orchestrator.js +8 -6
  92. package/addon/controllers/settings/tracking-page.js +212 -0
  93. package/addon/extension.js +97 -1
  94. package/addon/helpers/parcel-box-image.js +16 -0
  95. package/addon/routes/operations/orders/index/details.js +1 -0
  96. package/addon/routes/operations/orders/index/new.js +5 -4
  97. package/addon/routes/operations/scheduler/index.js +1 -1
  98. package/addon/routes/settings/tracking-page.js +31 -0
  99. package/addon/routes.js +1 -0
  100. package/addon/services/contact-actions.js +1 -0
  101. package/addon/services/customer-actions.js +2 -1
  102. package/addon/services/device-actions.js +1 -0
  103. package/addon/services/device-event-actions.js +1 -0
  104. package/addon/services/driver-actions.js +4 -13
  105. package/addon/services/equipment-actions.js +1 -0
  106. package/addon/services/fleet-actions.js +1 -0
  107. package/addon/services/fuel-report-actions.js +1 -0
  108. package/addon/services/inspection-form-actions.js +1 -0
  109. package/addon/services/inspection-submission-actions.js +1 -0
  110. package/addon/services/integrated-vendor-actions.js +1 -0
  111. package/addon/services/issue-actions.js +1 -0
  112. package/addon/services/location.js +69 -21
  113. package/addon/services/maintenance-actions.js +1 -0
  114. package/addon/services/maintenance-schedule-actions.js +2 -1
  115. package/addon/services/map-settings.js +28 -0
  116. package/addon/services/order-actions.js +7 -0
  117. package/addon/services/order-allocation.js +1 -1
  118. package/addon/services/order-presentation.js +129 -0
  119. package/addon/services/part-actions.js +1 -0
  120. package/addon/services/place-actions.js +1 -0
  121. package/addon/services/sensor-actions.js +1 -0
  122. package/addon/services/service-area-actions.js +1 -0
  123. package/addon/services/service-rate-actions.js +1 -0
  124. package/addon/services/telematic-actions.js +1 -0
  125. package/addon/services/trailer-actions.js +4 -14
  126. package/addon/services/vehicle-actions.js +4 -13
  127. package/addon/services/vendor-actions.js +1 -0
  128. package/addon/services/work-order-actions.js +1 -0
  129. package/addon/services/zone-actions.js +1 -0
  130. package/addon/styles/fleetops-engine.css +1391 -0
  131. package/addon/templates/analytics/reports/index/details.hbs +1 -0
  132. package/addon/templates/analytics/reports/index.hbs +1 -0
  133. package/addon/templates/connectivity/devices/index/details.hbs +1 -0
  134. package/addon/templates/connectivity/devices/index.hbs +1 -0
  135. package/addon/templates/connectivity/events/index.hbs +1 -0
  136. package/addon/templates/connectivity/sensors/index/details.hbs +1 -0
  137. package/addon/templates/connectivity/sensors/index.hbs +1 -0
  138. package/addon/templates/maintenance/equipment/index/details.hbs +1 -0
  139. package/addon/templates/maintenance/equipment/index.hbs +2 -0
  140. package/addon/templates/maintenance/inspection-forms/index/details.hbs +1 -0
  141. package/addon/templates/maintenance/inspection-forms/index.hbs +1 -0
  142. package/addon/templates/maintenance/inspection-submissions/index/details.hbs +1 -0
  143. package/addon/templates/maintenance/inspection-submissions/index.hbs +1 -0
  144. package/addon/templates/maintenance/maintenances/index/details.hbs +1 -0
  145. package/addon/templates/maintenance/maintenances/index.hbs +1 -0
  146. package/addon/templates/maintenance/parts/index/details.hbs +1 -0
  147. package/addon/templates/maintenance/parts/index.hbs +2 -0
  148. package/addon/templates/maintenance/schedules/index/details.hbs +1 -0
  149. package/addon/templates/maintenance/schedules/index.hbs +2 -0
  150. package/addon/templates/maintenance/work-orders/index/details.hbs +1 -0
  151. package/addon/templates/maintenance/work-orders/index.hbs +1 -0
  152. package/addon/templates/management/contacts/customers/details.hbs +1 -0
  153. package/addon/templates/management/contacts/customers.hbs +1 -0
  154. package/addon/templates/management/contacts/index/details.hbs +1 -0
  155. package/addon/templates/management/contacts/index.hbs +1 -0
  156. package/addon/templates/management/contacts.hbs +1 -0
  157. package/addon/templates/management/drivers/index/details.hbs +1 -0
  158. package/addon/templates/management/drivers/index.hbs +2 -0
  159. package/addon/templates/management/fleets/index/details.hbs +1 -0
  160. package/addon/templates/management/fleets/index.hbs +1 -0
  161. package/addon/templates/management/fuel-reports/index/details.hbs +1 -0
  162. package/addon/templates/management/fuel-reports/index.hbs +1 -0
  163. package/addon/templates/management/fuel-transactions/index.hbs +1 -0
  164. package/addon/templates/management/issues/index/details.hbs +1 -0
  165. package/addon/templates/management/issues/index.hbs +1 -0
  166. package/addon/templates/management/places/index/details.hbs +1 -0
  167. package/addon/templates/management/places/index.hbs +1 -0
  168. package/addon/templates/management/trailers/index/details.hbs +1 -0
  169. package/addon/templates/management/trailers/index.hbs +2 -0
  170. package/addon/templates/management/vehicles/index/details.hbs +1 -0
  171. package/addon/templates/management/vehicles/index.hbs +2 -0
  172. package/addon/templates/management/vendors/index/details.hbs +1 -0
  173. package/addon/templates/management/vendors/index.hbs +2 -0
  174. package/addon/templates/management/vendors/integrated.hbs +1 -0
  175. package/addon/templates/operations/orders/index/details.hbs +1 -0
  176. package/addon/templates/operations/orders/index.hbs +1 -0
  177. package/addon/templates/operations/service-rates/index/details.hbs +1 -0
  178. package/addon/templates/operations/service-rates/index.hbs +1 -0
  179. package/addon/templates/settings/map.hbs +46 -0
  180. package/addon/templates/settings/tracking-page.hbs +318 -0
  181. package/addon/utils/identity-cell-resource.js +2 -56
  182. package/addon/utils/map-default-view.js +132 -0
  183. package/addon/utils/normalize-order-config-flow.js +6 -1
  184. package/addon/utils/placeholder-images.js +4 -69
  185. package/addon/utils/resource-descriptors/assets.js +2 -57
  186. package/addon/utils/resource-descriptors/helpers.js +23 -122
  187. package/addon/utils/resource-descriptors/operations.js +2 -61
  188. package/addon/utils/resource-descriptors/people.js +3 -142
  189. package/addon/utils/resource-descriptors/shared.js +18 -0
  190. package/addon/utils/tracking-live-source.js +123 -0
  191. package/addon/utils/tracking-page-theme.js +123 -0
  192. package/app/components/admin/tracking-page-settings.js +1 -0
  193. package/app/components/tracking-page/full-view.js +1 -0
  194. package/app/components/tracking-page/locale-selector.js +1 -0
  195. package/app/components/tracking-page/map.js +1 -0
  196. package/app/components/tracking-page/order-list.js +1 -0
  197. package/app/components/tracking-page/public-view.js +1 -0
  198. package/app/components/tracking-page/settings-preview.js +1 -0
  199. package/app/components/tracking-page/verify-panel.js +1 -0
  200. package/app/components/tracking-page.js +1 -0
  201. package/app/components/widget/proof-of-delivery.js +1 -0
  202. package/app/controllers/settings/tracking-page.js +1 -0
  203. package/app/helpers/parcel-box-image.js +1 -0
  204. package/app/routes/settings/tracking-page.js +1 -0
  205. package/app/services/order-presentation.js +1 -0
  206. package/app/templates/settings/tracking-page.js +1 -0
  207. package/app/utils/map-default-view.js +1 -0
  208. package/app/utils/tracking-live-source.js +1 -0
  209. package/app/utils/tracking-page-theme.js +1 -0
  210. package/composer.json +2 -2
  211. package/extension.json +1 -1
  212. package/package.json +4 -4
  213. package/scripts/pest-bootstrap.php +4 -0
  214. package/server/config/fleetops.php +4 -0
  215. package/server/migrations/2026_10_08_000001_create_tracking_sessions_tables.php +51 -0
  216. package/server/migrations/2026_10_10_000001_add_trashed_index_to_device_events.php +40 -0
  217. package/server/migrations/2026_10_10_000002_add_trashed_index_to_positions.php +40 -0
  218. package/server/migrations/2026_10_12_000001_add_deleted_at_to_tracking_sessions_tables.php +32 -0
  219. package/server/resources/views/labels/default.php +2 -2
  220. package/server/resources/views/labels/entity-label.php +2 -2
  221. package/server/resources/views/labels/waypoint-label.php +2 -2
  222. package/server/src/Auth/Schemas/FleetOps.php +5 -0
  223. package/server/src/Console/Commands/ProcessOperationalAlerts.php +1 -0
  224. package/server/src/Console/Commands/PruneTelematicsData.php +101 -26
  225. package/server/src/Events/OrderChanged.php +257 -0
  226. package/server/src/Events/OrderDriverUnassigned.php +68 -0
  227. package/server/src/Exceptions/OrderEditLockedException.php +32 -0
  228. package/server/src/Expansions/CompanyExpansion.php +3 -2
  229. package/server/src/Expansions/UserExpansion.php +12 -10
  230. package/server/src/Http/Controllers/Api/v1/CustomerController.php +108 -18
  231. package/server/src/Http/Controllers/Api/v1/DriverController.php +95 -10
  232. package/server/src/Http/Controllers/Api/v1/EntityController.php +66 -0
  233. package/server/src/Http/Controllers/Api/v1/FleetController.php +10 -4
  234. package/server/src/Http/Controllers/Api/v1/OrderController.php +139 -36
  235. package/server/src/Http/Controllers/Api/v1/PayloadController.php +76 -8
  236. package/server/src/Http/Controllers/Api/v1/PlaceController.php +3 -3
  237. package/server/src/Http/Controllers/Api/v1/TrackingNumberController.php +6 -32
  238. package/server/src/Http/Controllers/Api/v1/VehicleController.php +5 -0
  239. package/server/src/Http/Controllers/Internal/v1/ContactController.php +3 -0
  240. package/server/src/Http/Controllers/Internal/v1/DriverController.php +10 -0
  241. package/server/src/Http/Controllers/Internal/v1/FleetController.php +16 -2
  242. package/server/src/Http/Controllers/Internal/v1/InspectionSubmissionController.php +13 -8
  243. package/server/src/Http/Controllers/Internal/v1/IssueController.php +2 -0
  244. package/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +4 -2
  245. package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +93 -11
  246. package/server/src/Http/Controllers/Internal/v1/OrderController.php +118 -28
  247. package/server/src/Http/Controllers/Internal/v1/PositionController.php +22 -0
  248. package/server/src/Http/Controllers/Internal/v1/ProofController.php +55 -4
  249. package/server/src/Http/Controllers/Internal/v1/SettingController.php +303 -10
  250. package/server/src/Http/Controllers/Internal/v1/TrailerController.php +1 -0
  251. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +1 -0
  252. package/server/src/Http/Controllers/Public/TrackingController.php +518 -0
  253. package/server/src/Http/Filter/ContactFilter.php +1 -1
  254. package/server/src/Http/Filter/InspectionFormFilter.php +23 -0
  255. package/server/src/Http/Filter/InspectionSubmissionFilter.php +23 -0
  256. package/server/src/Http/Filter/MaintenanceFilter.php +23 -0
  257. package/server/src/Http/Filter/OrderFilter.php +39 -4
  258. package/server/src/Http/Filter/ProofFilter.php +18 -0
  259. package/server/src/Http/Filter/RouteFilter.php +18 -0
  260. package/server/src/Http/Filter/TelematicFilter.php +23 -0
  261. package/server/src/Http/Filter/WarrantyFilter.php +23 -0
  262. package/server/src/Http/Requests/CancelOrderRequest.php +2 -1
  263. package/server/src/Http/Resources/v1/Concerns/ResolvesPublicRelationFields.php +14 -1
  264. package/server/src/Http/Resources/v1/Index/Customer.php +18 -0
  265. package/server/src/Http/Resources/v1/Index/Order.php +11 -2
  266. package/server/src/Http/Resources/v1/PublicOrderTracking.php +153 -0
  267. package/server/src/Http/Resources/v1/TrackingNumber.php +8 -34
  268. package/server/src/Http/Resources/v1/Zone.php +31 -23
  269. package/server/src/Jobs/FinalizeApiOrderCreation.php +3 -1
  270. package/server/src/Jobs/ProcessAllocationJob.php +16 -6
  271. package/server/src/Jobs/PublishTrackingUpdate.php +50 -0
  272. package/server/src/Listeners/HandleGeofenceEntered.php +31 -15
  273. package/server/src/Listeners/HandleOrderCanceled.php +9 -1
  274. package/server/src/Listeners/HandleUserRemovedFromCompany.php +2 -1
  275. package/server/src/Listeners/PublishTrackingUpdates.php +27 -0
  276. package/server/src/Mail/CustomerCredentialsMail.php +4 -2
  277. package/server/src/Models/AssetConnection.php +3 -0
  278. package/server/src/Models/Contact.php +3 -0
  279. package/server/src/Models/Device.php +28 -0
  280. package/server/src/Models/DeviceEvent.php +7 -0
  281. package/server/src/Models/Driver.php +20 -7
  282. package/server/src/Models/Entity.php +12 -4
  283. package/server/src/Models/Fleet.php +11 -32
  284. package/server/src/Models/FleetDriver.php +3 -0
  285. package/server/src/Models/FleetVehicle.php +3 -0
  286. package/server/src/Models/FuelReport.php +5 -13
  287. package/server/src/Models/Issue.php +4 -16
  288. package/server/src/Models/Order.php +234 -11
  289. package/server/src/Models/OrderConfig.php +129 -0
  290. package/server/src/Models/Payload.php +227 -35
  291. package/server/src/Models/Place.php +7 -1
  292. package/server/src/Models/ServiceArea.php +7 -0
  293. package/server/src/Models/ServiceRate.php +7 -17
  294. package/server/src/Models/TrackingNumber.php +109 -6
  295. package/server/src/Models/TrackingSession.php +33 -0
  296. package/server/src/Models/TrackingSessionGrant.php +22 -0
  297. package/server/src/Models/TrackingStatus.php +6 -0
  298. package/server/src/Models/Vehicle.php +21 -2
  299. package/server/src/Models/Vendor.php +7 -0
  300. package/server/src/Models/Waypoint.php +13 -4
  301. package/server/src/Notifications/OrderAssigned.php +2 -1
  302. package/server/src/Notifications/OrderCanceled.php +7 -1
  303. package/server/src/Notifications/OrderChanged.php +87 -0
  304. package/server/src/Notifications/OrderCompleted.php +2 -1
  305. package/server/src/Notifications/OrderDispatchFailed.php +2 -1
  306. package/server/src/Notifications/OrderDispatched.php +2 -1
  307. package/server/src/Notifications/OrderFailed.php +2 -1
  308. package/server/src/Notifications/OrderUnassigned.php +66 -0
  309. package/server/src/Notifications/WaypointCompleted.php +2 -1
  310. package/server/src/Observers/CompanyUserObserver.php +8 -7
  311. package/server/src/Observers/DriverObserver.php +6 -1
  312. package/server/src/Observers/OrderObserver.php +141 -1
  313. package/server/src/Observers/TrackingNumberObserver.php +18 -6
  314. package/server/src/Observers/UserObserver.php +2 -1
  315. package/server/src/Observers/VehicleObserver.php +8 -3
  316. package/server/src/Orchestration/Engines/RouteSequencingEngine.php +21 -3
  317. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +38 -8
  318. package/server/src/Orchestration/OrchestrationEngineRegistry.php +61 -1
  319. package/server/src/Orchestration/Support/OrchestratorSettings.php +61 -0
  320. package/server/src/Orchestration/Support/RouteMetrics.php +165 -0
  321. package/server/src/Providers/EventServiceProvider.php +11 -0
  322. package/server/src/Providers/FleetOpsServiceProvider.php +13 -0
  323. package/server/src/Support/ApiCacheThrottle.php +76 -0
  324. package/server/src/Support/LiveOrderQuery.php +3 -1
  325. package/server/src/Support/OrderEdit.php +365 -0
  326. package/server/src/Support/OrderEditPolicy.php +292 -0
  327. package/server/src/Support/ProfileAccountManager.php +54 -1
  328. package/server/src/Support/ResolvesOrderServiceStops.php +5 -0
  329. package/server/src/Support/SocketChannels.php +346 -0
  330. package/server/src/Support/Telematics/TelematicService.php +7 -2
  331. package/server/src/Support/Telematics/Telemetry/Inbox.php +8 -1
  332. package/server/src/Support/TrackingChannel.php +55 -0
  333. package/server/src/Support/TrackingCode.php +242 -0
  334. package/server/src/Support/TrackingPage/TrackingAccess.php +356 -0
  335. package/server/src/Support/TrackingPage/TrackingPageConfig.php +264 -0
  336. package/server/src/Support/TrackingPage/TrackingRecipient.php +111 -0
  337. package/server/src/Support/TrackingPage/TrackingResolver.php +201 -0
  338. package/server/src/Support/TrackingPage/TrackingTarget.php +40 -0
  339. package/server/src/Support/TrackingPage/TrackingView.php +543 -0
  340. package/server/src/Support/TrackingPublisher.php +261 -0
  341. package/server/src/Support/TrackingScope.php +182 -0
  342. package/server/src/Support/TrackingUpdate.php +402 -0
  343. package/server/src/Support/Utils.php +15 -3
  344. package/server/src/Traits/HasTrackingNumber.php +2 -0
  345. package/server/src/routes.php +39 -7
  346. package/server/tests/ApiCustomerControllerContractsTest.php +54 -7
  347. package/server/tests/ApiDriverPasswordContractsTest.php +25 -21
  348. package/server/tests/ApiEntityControllerContractsTest.php +84 -0
  349. package/server/tests/ApiOrderControllerContractsTest.php +245 -0
  350. package/server/tests/ApiPayloadControllerContractsTest.php +121 -0
  351. package/server/tests/ApiPlaceControllerContractsTest.php +1 -1
  352. package/server/tests/ApiTrackingNumberControllerContractsTest.php +6 -9
  353. package/server/tests/CompactResourceSerializationTest.php +23 -59
  354. package/server/tests/ControllerFilterContractsTest.php +1 -1
  355. package/server/tests/CustomerContactIdentitySafetyTest.php +1 -1
  356. package/server/tests/EventContractsTest.php +26 -60
  357. package/server/tests/ExpansionContractsTest.php +15 -2
  358. package/server/tests/Feature/Http/Api/CustomerControllerHelperSeamsTest.php +29 -3
  359. package/server/tests/Feature/Http/Api/DriverControllerAuthFlowsTest.php +23 -0
  360. package/server/tests/Feature/Http/Api/OrderControllerNearbyFiltersTest.php +16 -16
  361. package/server/tests/Feature/Http/Api/OrderControllerQueryFiltersTest.php +41 -14
  362. package/server/tests/Feature/Http/Api/TrackingNumberControllerHelpersTest.php +57 -22
  363. package/server/tests/Feature/Http/Internal/DriverControllerCreateRecordTest.php +23 -9
  364. package/server/tests/Feature/Http/Internal/OrchestrationControllerContractsTest.php +171 -1
  365. package/server/tests/Feature/Http/Internal/OrderControllerActivityFlowsTest.php +72 -0
  366. package/server/tests/Feature/Http/Internal/OrderControllerContractsTest.php +110 -14
  367. package/server/tests/Feature/Http/Internal/OrderControllerCreateRecordTest.php +26 -0
  368. package/server/tests/Feature/Http/Internal/OrderControllerHelpersTest.php +7 -5
  369. package/server/tests/Feature/Http/Internal/PositionControllerReplayChannelTest.php +135 -0
  370. package/server/tests/Feature/Http/Internal/ProofControllerContractsTest.php +3 -3
  371. package/server/tests/Feature/Http/Internal/ProofControllerRecentTest.php +170 -0
  372. package/server/tests/Feature/Http/Public/TrackingControllerTest.php +495 -0
  373. package/server/tests/IndexCustomerPhotoTest.php +151 -0
  374. package/server/tests/ModelAccessorContractsTest.php +4 -3
  375. package/server/tests/NotificationAndMailContractsTest.php +5 -8
  376. package/server/tests/ObserverContractsTest.php +72 -10
  377. package/server/tests/OrderActivityFlowRegressionTest.php +3 -2
  378. package/server/tests/OrderConfigLifecycleTest.php +120 -0
  379. package/server/tests/OrderFilterExecutionTest.php +167 -2
  380. package/server/tests/ProcessAllocationJobContractsTest.php +20 -0
  381. package/server/tests/PublicOrderTrackingResourceTest.php +221 -0
  382. package/server/tests/QueueJobContractsTest.php +2 -1
  383. package/server/tests/RequestContractsTest.php +1 -1
  384. package/server/tests/RouteRegistrationExecutionTest.php +26 -0
  385. package/server/tests/RouteSequencingEngineTest.php +17 -0
  386. package/server/tests/SettingControllerContractsTest.php +110 -0
  387. package/server/tests/Support/TrackingPageDatabase.php +200 -0
  388. package/server/tests/TenantScopingTest.php +206 -0
  389. package/server/tests/TrackingPageSettingsTest.php +343 -0
  390. package/server/tests/Unit/Console/PruneTelematicsDataCommandTest.php +51 -0
  391. package/server/tests/Unit/Events/OrderDispatchFailedTest.php +2 -1
  392. package/server/tests/Unit/Http/Filter/ContactFilterAddressTest.php +61 -0
  393. package/server/tests/Unit/Http/Resources/IndexOrderResourceTest.php +30 -0
  394. package/server/tests/Unit/Jobs/FinalizeApiOrderCreationDispatchGuardTest.php +266 -0
  395. package/server/tests/Unit/Listeners/HandleGeofenceEnteredTest.php +182 -22
  396. package/server/tests/Unit/Listeners/HandleUserRemovedFromCompanyTest.php +5 -0
  397. package/server/tests/Unit/Models/DriverAssignVehicleTest.php +81 -0
  398. package/server/tests/Unit/Models/FleetTest.php +4 -3
  399. package/server/tests/Unit/Models/OrderCancelReasonTest.php +143 -0
  400. package/server/tests/Unit/Models/OrderPayloadConfigAndDistanceTest.php +60 -0
  401. package/server/tests/Unit/Models/OrderPayloadRouteTest.php +4 -2
  402. package/server/tests/Unit/Models/PayloadTest.php +18 -4
  403. package/server/tests/Unit/Models/ServiceRatePersistenceAndServicabilityTest.php +2 -2
  404. package/server/tests/Unit/Models/TrackingNumberTest.php +67 -6
  405. package/server/tests/Unit/Models/VehicleTest.php +16 -2
  406. package/server/tests/Unit/Models/WaypointTest.php +4 -3
  407. package/server/tests/Unit/Notifications/OrderChangeNotificationsTest.php +59 -0
  408. package/server/tests/Unit/Notifications/TerminalOrderNotificationsTest.php +3 -3
  409. package/server/tests/Unit/Observers/DriverDeletionCleanupTest.php +158 -0
  410. package/server/tests/Unit/Observers/FleetOpsCompanyUserAndServiceAreaObserversTest.php +20 -0
  411. package/server/tests/Unit/Observers/ObserverListenerSeamsTest.php +3 -3
  412. package/server/tests/Unit/Observers/OrderAssignmentObserverTest.php +100 -0
  413. package/server/tests/Unit/Observers/OrderDeleteCascadeTest.php +213 -0
  414. package/server/tests/Unit/Observers/VehicleObserverDeleteTest.php +67 -0
  415. package/server/tests/Unit/Orchestration/EngineSelectionAndRouteMetricsTest.php +336 -0
  416. package/server/tests/Unit/Orchestration/Engines/VroomOrchestrationEngineLifecycleTest.php +8 -1
  417. package/server/tests/Unit/Support/ApiCacheDependenciesTest.php +347 -0
  418. package/server/tests/Unit/Support/OrderEditPolicyTest.php +156 -0
  419. package/server/tests/Unit/Support/OrderOperationsTest.php +514 -0
  420. package/server/tests/Unit/Support/OsrmProofAndIssueFilterTest.php +4 -3
  421. package/server/tests/Unit/Support/ProfileAccountManagerTest.php +141 -0
  422. package/server/tests/Unit/Support/QuerySeamsTest.php +5 -1
  423. package/server/tests/Unit/Support/SocketChannelsTest.php +396 -0
  424. package/server/tests/Unit/Support/TrackingChannelTest.php +232 -0
  425. package/server/tests/Unit/Support/TrackingCodeTest.php +186 -0
  426. package/server/tests/Unit/Support/TrackingPage/TrackingAccessTest.php +200 -0
  427. package/server/tests/Unit/Support/TrackingPage/TrackingRecipientTest.php +51 -0
  428. package/server/tests/Unit/Support/TrackingPage/TrackingResolverTest.php +181 -0
  429. package/server/tests/Unit/Support/TrackingPage/TrackingViewTest.php +479 -0
  430. package/server/tests/Unit/Support/TrackingPublisherTest.php +425 -0
  431. package/server/tests/Unit/Support/TrackingUpdateTest.php +363 -0
  432. package/server/tests/Unit/Support/UtilsAdditionalTest.php +9 -0
  433. package/server/tests/Unit/Support/UtilsGeoMatrixAndVendorTest.php +25 -0
  434. package/server/tests/VehicleResourceContractsTest.php +46 -0
  435. package/server/tests/VroomOrchestrationEngineTest.php +34 -1
  436. package/server/tests/ZoneGeofenceSettingsResourceTest.php +121 -0
  437. package/translations/ar-ae.yml +274 -0
  438. package/translations/bg-bg.yaml +274 -0
  439. package/translations/en-us.yaml +347 -0
  440. package/translations/es-pa.yaml +274 -0
  441. package/translations/fr-fr.yaml +274 -0
  442. package/translations/mn-mn.yaml +274 -0
  443. package/translations/pt-br.yaml +274 -0
  444. package/translations/ru-ru.yaml +276 -2
  445. package/translations/uk-ua.yaml +275 -1
  446. package/translations/vi-vn.yaml +274 -0
  447. package/addon/components/cell/contact-identity.hbs +0 -1
  448. package/addon/components/cell/customer-identity.hbs +0 -1
  449. package/addon/components/cell/driver-identity.hbs +0 -1
  450. package/addon/components/cell/fleet-identity.hbs +0 -1
  451. package/addon/components/cell/order-identity.hbs +0 -1
  452. package/addon/components/cell/place-identity.hbs +0 -1
  453. package/addon/components/cell/vehicle-identity.hbs +0 -1
  454. package/addon/components/cell/vendor-identity.hbs +0 -1
  455. package/addon/components/contact/pill.hbs +0 -24
  456. package/addon/components/contact/summary.hbs +0 -1
  457. package/addon/components/customer/pill.hbs +0 -24
  458. package/addon/components/customer/summary.hbs +0 -1
  459. package/addon/components/driver/pill.hbs +0 -24
  460. package/addon/components/driver/summary.hbs +0 -1
  461. package/addon/components/fleet/pill.hbs +0 -24
  462. package/addon/components/fleet/summary.hbs +0 -1
  463. package/addon/components/order/pill.hbs +0 -24
  464. package/addon/components/order/summary.hbs +0 -1
  465. package/addon/components/place/pill.hbs +0 -24
  466. package/addon/components/place/summary.hbs +0 -1
  467. package/addon/components/select-option/contact.hbs +0 -12
  468. package/addon/components/select-option/customer.hbs +0 -12
  469. package/addon/components/select-option/driver.hbs +0 -12
  470. package/addon/components/select-option/fleet.hbs +0 -12
  471. package/addon/components/select-option/order.hbs +0 -12
  472. package/addon/components/select-option/place.hbs +0 -12
  473. package/addon/components/select-option/vehicle.hbs +0 -12
  474. package/addon/components/select-option/vendor.hbs +0 -12
  475. package/addon/components/vehicle/pill.hbs +0 -24
  476. package/addon/components/vehicle/summary.hbs +0 -1
  477. package/addon/components/vendor/pill.hbs +0 -24
  478. package/addon/components/vendor/summary.hbs +0 -1
  479. package/app/components/cell/contact-identity.js +0 -1
  480. package/app/components/cell/customer-identity.js +0 -1
  481. package/app/components/cell/driver-identity.js +0 -1
  482. package/app/components/cell/fleet-identity.js +0 -1
  483. package/app/components/cell/order-identity.js +0 -1
  484. package/app/components/cell/place-identity.js +0 -1
  485. package/app/components/cell/vehicle-identity.js +0 -1
  486. package/app/components/cell/vendor-identity.js +0 -1
  487. package/app/components/contact/pill.js +0 -1
  488. package/app/components/contact/summary.js +0 -1
  489. package/app/components/customer/pill.js +0 -1
  490. package/app/components/customer/summary.js +0 -1
  491. package/app/components/driver/pill.js +0 -1
  492. package/app/components/driver/summary.js +0 -1
  493. package/app/components/fleet/pill.js +0 -1
  494. package/app/components/fleet/summary.js +0 -1
  495. package/app/components/order/pill.js +0 -1
  496. package/app/components/order/summary.js +0 -1
  497. package/app/components/place/pill.js +0 -1
  498. package/app/components/place/summary.js +0 -1
  499. package/app/components/select-option/contact.js +0 -1
  500. package/app/components/select-option/customer.js +0 -1
  501. package/app/components/select-option/driver.js +0 -1
  502. package/app/components/select-option/fleet.js +0 -1
  503. package/app/components/select-option/order.js +0 -1
  504. package/app/components/select-option/place.js +0 -1
  505. package/app/components/select-option/vehicle.js +0 -1
  506. package/app/components/select-option/vendor.js +0 -1
  507. package/app/components/vehicle/pill.js +0 -1
  508. package/app/components/vehicle/summary.js +0 -1
  509. package/app/components/vendor/pill.js +0 -1
  510. package/app/components/vendor/summary.js +0 -1
@@ -12,11 +12,16 @@ use Fleetbase\FleetOps\Http\Resources\v1\Payload as PayloadResource;
12
12
  use Fleetbase\FleetOps\Support\Utils;
13
13
  use Fleetbase\LaravelMysqlSpatial\Types\Point;
14
14
  use Fleetbase\Models\Model;
15
+ use Fleetbase\Support\ApiModelCache;
15
16
  use Fleetbase\Traits\HasApiModelBehavior;
16
17
  use Fleetbase\Traits\HasMetaAttributes;
17
18
  use Fleetbase\Traits\HasPublicId;
18
19
  use Fleetbase\Traits\HasUuid;
19
20
  use Fleetbase\Traits\TracksApiCredential;
21
+ use Illuminate\Database\Eloquent\Relations\BelongsTo;
22
+ use Illuminate\Database\Eloquent\Relations\HasMany;
23
+ use Illuminate\Database\Eloquent\Relations\HasManyThrough;
24
+ use Illuminate\Support\Arr;
20
25
  use Illuminate\Support\Facades\DB;
21
26
  use Illuminate\Support\Str;
22
27
 
@@ -140,7 +145,7 @@ class Payload extends Model
140
145
  /**
141
146
  * Entities in the payload.
142
147
  */
143
- public function entities()
148
+ public function entities(): HasMany
144
149
  {
145
150
  return $this->hasMany(Entity::class);
146
151
  }
@@ -194,7 +199,7 @@ class Payload extends Model
194
199
  /**
195
200
  * The address the shipment will be delivered to.
196
201
  */
197
- public function dropoff()
202
+ public function dropoff(): BelongsTo
198
203
  {
199
204
  return $this->belongsTo(Place::class, 'dropoff_uuid')->whereNull('deleted_at')->withoutGlobalScopes();
200
205
  }
@@ -202,7 +207,7 @@ class Payload extends Model
202
207
  /**
203
208
  * The address the shipment will be delivered from.
204
209
  */
205
- public function pickup()
210
+ public function pickup(): BelongsTo
206
211
  {
207
212
  return $this->belongsTo(Place::class, 'pickup_uuid')->whereNull('deleted_at')->withoutGlobalScopes();
208
213
  }
@@ -210,7 +215,7 @@ class Payload extends Model
210
215
  /**
211
216
  * The address the shipment will be sent to upon failed delivery.
212
217
  */
213
- public function return()
218
+ public function return(): BelongsTo
214
219
  {
215
220
  return $this->belongsTo(Place::class)->withoutGlobalScopes();
216
221
  }
@@ -233,10 +238,8 @@ class Payload extends Model
233
238
 
234
239
  /**
235
240
  * Waypoints between start and end.
236
- *
237
- * @return \Illuminate\Database\Eloquent\Concerns\HasManyThrough
238
241
  */
239
- public function waypoints()
242
+ public function waypoints(): HasManyThrough
240
243
  {
241
244
  return $this->hasManyThrough(Place::class, Waypoint::class, 'payload_uuid', 'uuid', 'uuid', 'place_uuid')->whereNull('waypoints.deleted_at')->withoutGlobalScopes();
242
245
  }
@@ -249,6 +252,11 @@ class Payload extends Model
249
252
  $this->attributes['cod_amount'] = Utils::numbersOnly($value);
250
253
  }
251
254
 
255
+ /**
256
+ * Add entities to the payload. Every entry creates a new entity, so sending
257
+ * the payload's current entities again duplicates them; use syncEntities()
258
+ * to replace the list.
259
+ */
252
260
  public function setEntities($entities = [])
253
261
  {
254
262
  if (empty($entities) || !is_array($entities)) {
@@ -256,49 +264,228 @@ class Payload extends Model
256
264
  }
257
265
 
258
266
  foreach ($entities as $attributes) {
259
- if (isset($attributes['_import_id'])) {
260
- $waypoint = $this->waypoints->firstWhere('_import_id', $attributes['_import_id']);
267
+ $entity = new Entity($this->prepareEntityAttributes($attributes));
268
+ $this->entities()->save($entity);
269
+ }
261
270
 
262
- if ($waypoint) {
263
- $attributes['destination_uuid'] = $waypoint->uuid;
264
- }
271
+ return $this;
272
+ }
273
+
274
+ /**
275
+ * Replace the payload's entities with the given list: entries naming an
276
+ * existing entity of this payload (by `id` public id or `uuid`) update it,
277
+ * entries without one create a new entity, and entities left out are deleted.
278
+ *
279
+ * @return array{created: string[], updated: string[], deleted: string[]} entity uuids by outcome
280
+ */
281
+ public function syncEntities($entities = []): array
282
+ {
283
+ $result = ['created' => [], 'updated' => [], 'deleted' => []];
284
+ if (!is_array($entities)) {
285
+ return $result;
286
+ }
287
+
288
+ $existing = $this->entities()->get();
289
+ $kept = [];
290
+
291
+ foreach ($entities as $attributes) {
292
+ if (!is_array($attributes)) {
293
+ continue;
265
294
  }
266
295
 
267
- // if a destination or waypoint is explicitly set
268
- if (empty($attributes['destination_uuid'])) {
269
- $destinationKey = Utils::or($attributes, ['waypoint', 'destination']);
270
- $destination = $this->findDestinationFromKey($destinationKey);
271
- if ($destination instanceof Place) {
272
- $attributes['destination_uuid'] = $destination->uuid;
296
+ $entity = $this->findOwnEntity($existing, $attributes);
297
+ $values = $this->prepareEntityAttributes(Arr::except($attributes, ['id', 'uuid', 'public_id', 'payload_uuid', 'company_uuid']));
298
+
299
+ if ($entity) {
300
+ $entity->fill(Arr::only($values, $entity->getFillable()));
301
+ if ($entity->isDirty()) {
302
+ $entity->save();
303
+ $result['updated'][] = $entity->uuid;
273
304
  }
305
+ $kept[] = $entity->uuid;
306
+
307
+ continue;
274
308
  }
275
309
 
276
- // Validate destination actually exists
277
- if (isset($attributes['destination_uuid']) && Place::where('uuid', $attributes['destination_uuid'])->doesntExist()) {
278
- $validDestination = $this->_findCorrectDestinationForEntity($attributes);
279
- if ($validDestination) {
280
- $attributes['destination_uuid'] = $validDestination->uuid;
281
- }
310
+ $entity = new Entity($values);
311
+ $this->entities()->save($entity);
312
+ $result['created'][] = $entity->uuid;
313
+ $kept[] = $entity->uuid;
314
+ }
315
+
316
+ foreach ($existing as $entity) {
317
+ if (!in_array($entity->uuid, $kept, true)) {
318
+ $entity->delete();
319
+ $result['deleted'][] = $entity->uuid;
282
320
  }
321
+ }
283
322
 
284
- // Handle entity photo upload
285
- if (isset($attributes['photo'])) {
286
- $path = 'uploads/' . session('company') . '/entities';
287
- $file = app(\Fleetbase\Services\FileResolverService::class)->resolve($attributes['photo'], $path);
323
+ $this->unsetRelation('entities');
288
324
 
289
- if ($file) {
290
- $attributes['photo_uuid'] = $file->uuid;
325
+ return $result;
326
+ }
327
+
328
+ /**
329
+ * Whether every entry names an entity this payload already has, i.e. applying the
330
+ * list with syncEntities() would only update or remove entities, never add one.
331
+ */
332
+ public function entitiesListOnlyNamesExisting(array $entities): bool
333
+ {
334
+ $existing = $this->entities()->get();
335
+
336
+ foreach ($entities as $attributes) {
337
+ if (!is_array($attributes) || !$this->findOwnEntity($existing, $attributes)) {
338
+ return false;
339
+ }
340
+ }
341
+
342
+ return true;
343
+ }
344
+
345
+ /**
346
+ * Find the payload entity an entity entry refers to by `id` (public id) or `uuid`.
347
+ */
348
+ protected function findOwnEntity($existing, array $attributes): ?Entity
349
+ {
350
+ $key = $attributes['id'] ?? $attributes['public_id'] ?? $attributes['uuid'] ?? null;
351
+ if (!is_string($key) || $key === '') {
352
+ return null;
353
+ }
354
+
355
+ return $existing->first(fn (Entity $entity) => $entity->public_id === $key || $entity->uuid === $key);
356
+ }
357
+
358
+ /**
359
+ * Resolve an entity entry's destination and photo into storable attributes.
360
+ */
361
+ protected function prepareEntityAttributes(array $attributes): array
362
+ {
363
+ if (isset($attributes['_import_id'])) {
364
+ $waypoint = $this->waypoints->firstWhere('_import_id', $attributes['_import_id']);
365
+
366
+ if ($waypoint) {
367
+ $attributes['destination_uuid'] = $waypoint->uuid;
368
+ }
369
+ }
370
+
371
+ // if a destination or waypoint is explicitly set
372
+ if (empty($attributes['destination_uuid'])) {
373
+ $destinationKey = Utils::or($attributes, ['waypoint', 'destination']);
374
+ $destination = $this->findDestinationFromKey($destinationKey);
375
+ if ($destination instanceof Place) {
376
+ $attributes['destination_uuid'] = $destination->uuid;
377
+ }
378
+ }
379
+
380
+ // Validate destination actually exists
381
+ if (isset($attributes['destination_uuid']) && Place::where('uuid', $attributes['destination_uuid'])->doesntExist()) {
382
+ $validDestination = $this->_findCorrectDestinationForEntity($attributes);
383
+ if ($validDestination) {
384
+ $attributes['destination_uuid'] = $validDestination->uuid;
385
+ }
386
+ }
387
+
388
+ // Handle entity photo upload
389
+ if (isset($attributes['photo'])) {
390
+ $path = 'uploads/' . session('company') . '/entities';
391
+ $file = app(\Fleetbase\Services\FileResolverService::class)->resolve($attributes['photo'], $path);
392
+
393
+ if ($file) {
394
+ $attributes['photo_uuid'] = $file->uuid;
395
+ }
396
+
397
+ // Clean up raw photo data
398
+ unset($attributes['photo']);
399
+ }
400
+
401
+ return $attributes;
402
+ }
403
+
404
+ /**
405
+ * The payload's stops as stored: pickup, dropoff and return place uuids, the
406
+ * waypoint place uuids in order and the current waypoint. Compare two
407
+ * signatures with routeChanges() to see what an edit changed.
408
+ */
409
+ public function routeSignature(): array
410
+ {
411
+ return [
412
+ 'payload' => $this->uuid,
413
+ 'pickup' => $this->pickup_uuid,
414
+ 'dropoff' => $this->dropoff_uuid,
415
+ 'return' => $this->return_uuid,
416
+ 'waypoints' => $this->uuid ? Waypoint::where('payload_uuid', $this->uuid)->whereNull('deleted_at')->orderBy('order')->pluck('place_uuid')->values()->all() : [],
417
+ 'current' => $this->current_waypoint_uuid,
418
+ ];
419
+ }
420
+
421
+ /**
422
+ * The route parts (pickup, dropoff, return, waypoints) that differ between two route signatures.
423
+ */
424
+ public static function routeChanges(array $before, array $after): array
425
+ {
426
+ $changes = [];
427
+ foreach (['pickup', 'dropoff', 'return', 'waypoints'] as $part) {
428
+ if (($before[$part] ?? null) !== ($after[$part] ?? null)) {
429
+ $changes[] = $part;
430
+ }
431
+ }
432
+
433
+ return $changes;
434
+ }
435
+
436
+ /**
437
+ * Point the payload at the right stop after its route changed.
438
+ *
439
+ * Before the order is under way this is the first stop, as it always was. Once
440
+ * the driver is under way the stop they are heading to is kept when it is still
441
+ * on the route; when it was replaced the stop that took its place is used (the
442
+ * new pickup, dropoff or return), and a removed waypoint moves them on to the
443
+ * next stop they have not completed.
444
+ *
445
+ * @param array $before the routeSignature() taken before the edit
446
+ */
447
+ public function reconcileCurrentWaypoint(array $before, bool $inProgress): ?Place
448
+ {
449
+ $this->unsetRelation('waypoints');
450
+ $this->unsetRelation('waypointMarkers');
451
+ $this->loadMissing(['pickup', 'dropoff', 'return']);
452
+
453
+ $destination = null;
454
+
455
+ if ($inProgress) {
456
+ $after = $this->routeSignature();
457
+ $current = $before['current'] ?? null;
458
+ $stops = array_filter(array_merge([$after['pickup'], $after['dropoff'], $after['return']], $after['waypoints']));
459
+
460
+ if ($current && in_array($current, $stops, true)) {
461
+ $destination = Place::where('uuid', $current)->first();
462
+ } elseif ($current) {
463
+ foreach (['pickup', 'dropoff', 'return'] as $role) {
464
+ if ($current === ($before[$role] ?? null) && $this->{$role} instanceof Place) {
465
+ $destination = $this->{$role};
466
+ break;
467
+ }
291
468
  }
292
469
 
293
- // Clean up raw photo data
294
- unset($attributes['photo']);
470
+ if (!$destination) {
471
+ $next = $this->waypointMarkers()->orderBy('order')->get()->first(function (Waypoint $waypoint) {
472
+ return !$waypoint->complete;
473
+ });
474
+ $destination = $next ? $next->getPlace() : ($this->dropoff instanceof Place ? $this->dropoff : null);
475
+ }
295
476
  }
477
+ }
296
478
 
297
- $entity = new Entity($attributes);
298
- $this->entities()->save($entity);
479
+ if (!$destination) {
480
+ $destination = $this->getPickupOrFirstWaypoint() ?? $this->getDropoffOrLastWaypoint();
299
481
  }
300
482
 
301
- return $this;
483
+ if ($destination instanceof Place) {
484
+ $this->setCurrentWaypoint($destination);
485
+ $this->setRelation('currentWaypoint', $destination);
486
+ }
487
+
488
+ return $destination instanceof Place ? $destination : null;
302
489
  }
303
490
 
304
491
  public function insertEntities($entities = [])
@@ -365,6 +552,7 @@ class Payload extends Model
365
552
  $this->current_waypoint_uuid = $destination->uuid;
366
553
  if ($save) {
367
554
  DB::table($this->getTable())->where('uuid', $this->uuid)->update(['current_waypoint_uuid' => $destination->uuid]);
555
+ ApiModelCache::invalidateModelCache($this, $this->company_uuid);
368
556
  }
369
557
 
370
558
  return $this;
@@ -868,6 +1056,7 @@ class Payload extends Model
868
1056
  public function removeWaypoints()
869
1057
  {
870
1058
  Waypoint::where('payload_uuid', $this->uuid)->delete();
1059
+ Waypoint::invalidateApiCacheManually($this->company_uuid);
871
1060
  $this->setRelation('waypoints', collect());
872
1061
 
873
1062
  return $this;
@@ -895,6 +1084,7 @@ class Payload extends Model
895
1084
 
896
1085
  if ($save) {
897
1086
  $this->updateQuietly([$attr => null]);
1087
+ ApiModelCache::invalidateModelCache($this, $this->company_uuid);
898
1088
  }
899
1089
 
900
1090
  if (is_callable($callback)) {
@@ -928,6 +1118,7 @@ class Payload extends Model
928
1118
  // If optioned to save
929
1119
  if ($save) {
930
1120
  $this->updateQuietly([$attr => $id]);
1121
+ ApiModelCache::invalidateModelCache($this, $this->company_uuid);
931
1122
  }
932
1123
 
933
1124
  if (is_callable($callback)) {
@@ -1000,6 +1191,7 @@ class Payload extends Model
1000
1191
 
1001
1192
  $this->current_waypoint_uuid = $destination->uuid;
1002
1193
  $this->saveQuietly();
1194
+ ApiModelCache::invalidateModelCache($this, $this->company_uuid);
1003
1195
  $this->updateWaypointActivity($activity, $location);
1004
1196
 
1005
1197
  return $this->load('currentWaypoint');
@@ -187,7 +187,8 @@ class Place extends Model
187
187
  return static::getAvatar($value);
188
188
  }
189
189
 
190
- return $value;
190
+ // legacy rows hold an absolute URL; re-sign it if it points into the private media bucket
191
+ return Utils::signStoredFileUrl($value);
191
192
  }
192
193
 
193
194
  /**
@@ -872,6 +873,11 @@ class Place extends Model
872
873
 
873
874
  $result = static::insert($values);
874
875
 
876
+ // Inserted without model events
877
+ if ($result) {
878
+ static::invalidateApiCacheManually($values['company_uuid']);
879
+ }
880
+
875
881
  return $result ? $uuid : false;
876
882
  }
877
883
 
@@ -91,6 +91,13 @@ class ServiceArea extends Model
91
91
  */
92
92
  protected $with = ['zones'];
93
93
 
94
+ /**
95
+ * Relation paths the cached service area lists embed through `$with` (fleetbase/core-api#301).
96
+ *
97
+ * @var string[]
98
+ */
99
+ protected $apiCacheDependencies = ['zones'];
100
+
94
101
  /**
95
102
  * Dynamic attributes that are appended to object.
96
103
  *
@@ -12,6 +12,8 @@ use Fleetbase\Traits\HasPublicId;
12
12
  use Fleetbase\Traits\HasUuid;
13
13
  use Fleetbase\Traits\SendsWebhooks;
14
14
  use Fleetbase\Traits\TracksApiCredential;
15
+ use Illuminate\Database\Eloquent\Relations\BelongsTo;
16
+ use Illuminate\Database\Eloquent\Relations\HasMany;
15
17
 
16
18
  class ServiceRate extends Model
17
19
  {
@@ -110,42 +112,30 @@ class ServiceRate extends Model
110
112
  */
111
113
  protected $filterParams = [];
112
114
 
113
- /**
114
- * @return \Illuminate\Database\Eloquent\Relations\HasMany
115
- */
116
- public function rateFees()
115
+ public function rateFees(): HasMany
117
116
  {
118
117
  return $this->hasMany(ServiceRateFee::class);
119
118
  }
120
119
 
121
- /**
122
- * @return \Illuminate\Database\Eloquent\Relations\HasMany
123
- */
124
- public function parcelFees()
120
+ public function parcelFees(): HasMany
125
121
  {
126
122
  return $this->hasMany(ServiceRateParcelFee::class);
127
123
  }
128
124
 
129
125
  /**
130
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
126
+ * @return BelongsTo
131
127
  */
132
128
  public function orderConfig()
133
129
  {
134
130
  return $this->belongsTo(OrderConfig::class)->withTrashed();
135
131
  }
136
132
 
137
- /**
138
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
139
- */
140
- public function serviceArea()
133
+ public function serviceArea(): BelongsTo
141
134
  {
142
135
  return $this->belongsTo(ServiceArea::class)->whereNull('deleted_at');
143
136
  }
144
137
 
145
- /**
146
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
147
- */
148
- public function zone()
138
+ public function zone(): BelongsTo
149
139
  {
150
140
  return $this->belongsTo(Zone::class);
151
141
  }
@@ -4,9 +4,11 @@ namespace Fleetbase\FleetOps\Models;
4
4
 
5
5
  use Fleetbase\Casts\PolymorphicType;
6
6
  use Fleetbase\FleetOps\Casts\Point;
7
+ use Fleetbase\FleetOps\Support\TrackingCode;
7
8
  use Fleetbase\FleetOps\Support\Utils;
8
9
  use Fleetbase\LaravelMysqlSpatial\Eloquent\SpatialTrait;
9
10
  use Fleetbase\Models\Model;
11
+ use Fleetbase\Support\ApiModelCache;
10
12
  use Fleetbase\Traits\HasApiModelBehavior;
11
13
  use Fleetbase\Traits\HasPublicId;
12
14
  use Fleetbase\Traits\HasUuid;
@@ -16,6 +18,7 @@ use Illuminate\Support\Carbon;
16
18
  use Illuminate\Support\Facades\DB;
17
19
  use Illuminate\Support\Facades\Schema;
18
20
  use Illuminate\Support\Str;
21
+ use Milon\Barcode\Facades\DNS1DFacade as DNS1D;
19
22
  use Milon\Barcode\Facades\DNS2DFacade as DNS2D;
20
23
 
21
24
  class TrackingNumber extends Model
@@ -236,6 +239,74 @@ class TrackingNumber extends Model
236
239
  throw (new \Illuminate\Database\Eloquent\ModelNotFoundException())->setModel(static::class, $id);
237
240
  }
238
241
 
242
+ /**
243
+ * The record a scanned code points at, within one company: the order, waypoint,
244
+ * entity or place that owns the tracking number.
245
+ *
246
+ * Accepts every format TrackingCode::parse() reads, including the bare owner uuid
247
+ * printed on labels before codes carried a url, and a bare tracking number or public
248
+ * id from a barcode or typed by hand. Without a company nothing resolves.
249
+ */
250
+ public static function findOwnerByCode(?string $code, ?string $companyUuid): ?\Illuminate\Database\Eloquent\Model
251
+ {
252
+ if (empty($companyUuid)) {
253
+ return null;
254
+ }
255
+
256
+ $parsed = TrackingCode::parse($code);
257
+
258
+ if ($parsed['uuid']) {
259
+ return static::ownerOf(static::firstForCode('owner_uuid', $parsed['uuid'], $companyUuid));
260
+ }
261
+
262
+ $number = $parsed['tracking_number'] ?? $parsed['reference'];
263
+ if ($number) {
264
+ $owner = static::ownerOf(static::firstForCode('tracking_number', $number, $companyUuid));
265
+
266
+ // A url names its tracking number deliberately, so an unknown one is a miss,
267
+ // not a cue to fall back to the owner it also names, which must agree.
268
+ if ($owner || $parsed['tracking_number']) {
269
+ return $owner && (!$parsed['public_id'] || data_get($owner, 'public_id') === $parsed['public_id']) ? $owner : null;
270
+ }
271
+ }
272
+
273
+ $publicId = $parsed['public_id'] ?? $parsed['reference'];
274
+
275
+ return $publicId ? static::ownerOf(static::firstForOwnerPublicId($publicId, $companyUuid)) : null;
276
+ }
277
+
278
+ protected static function firstForCode(string $column, string $value, string $companyUuid): ?self
279
+ {
280
+ return static::where('company_uuid', $companyUuid)->where($column, $value)->first();
281
+ }
282
+
283
+ protected static function firstForOwnerPublicId(string $publicId, string $companyUuid): ?self
284
+ {
285
+ // The public id's prefix names the owner's model, so only that table is searched.
286
+ $prefix = Str::contains($publicId, '_') ? Str::before($publicId, '_') : null;
287
+ $ownerClass = [
288
+ 'order' => Order::class,
289
+ 'waypoint' => Waypoint::class,
290
+ 'entity' => Entity::class,
291
+ 'place' => Place::class,
292
+ ][$prefix] ?? null;
293
+
294
+ if (!$ownerClass) {
295
+ return null;
296
+ }
297
+
298
+ return static::where('company_uuid', $companyUuid)
299
+ // Rows inserted raw (waypoints, entities) store the class with a leading slash.
300
+ ->whereIn('owner_type', [$ownerClass, '\\' . $ownerClass])
301
+ ->whereIn('owner_uuid', $ownerClass::withoutGlobalScopes()->select('uuid')->where('public_id', $publicId))
302
+ ->first();
303
+ }
304
+
305
+ protected static function ownerOf(?self $trackingNumber): ?\Illuminate\Database\Eloquent\Model
306
+ {
307
+ return $trackingNumber?->owner;
308
+ }
309
+
239
310
  public function updateOwnerStatus(?TrackingStatus $trackingStatus = null)
240
311
  {
241
312
  $trackingStatus = $trackingStatus ?? $this->load(['status'])->getRelationValue('status');
@@ -254,6 +325,10 @@ class TrackingNumber extends Model
254
325
 
255
326
  public static function insertGetUuid($values = [], ?Model $owner = null)
256
327
  {
328
+ // Read before the fillable filter below drops it. Callers that insert the owner
329
+ // with a raw query, and so have no model to pass, hand its public_id over here.
330
+ $ownerPublicId = $owner ? data_get($owner, 'public_id') : ($values['owner_public_id'] ?? null);
331
+
257
332
  $instance = new static();
258
333
  $fillable = $instance->getFillable();
259
334
  $insertKeys = array_keys($values);
@@ -276,8 +351,7 @@ class TrackingNumber extends Model
276
351
  }
277
352
 
278
353
  $values['tracking_number'] = static::newTrackingNumber($values['region'] ?? 'SG');
279
- $values['qr_code'] = static::newBarcode((string) ($values['owner_uuid'] ?? ''), 'QRCODE');
280
- $values['barcode'] = static::newBarcode((string) ($values['owner_uuid'] ?? ''), 'PDF417');
354
+ $values = array_merge($values, static::codeImages($values['tracking_number'], $ownerPublicId));
281
355
 
282
356
  if (isset($values['meta']) && (is_object($values['meta']) || is_array($values['meta']))) {
283
357
  $values['meta'] = json_encode($values['meta']);
@@ -291,13 +365,17 @@ class TrackingNumber extends Model
291
365
 
292
366
  $ownerTypeName = class_basename($values['owner_type']);
293
367
 
368
+ // owners may define their own initial status (e.g. orders with a configured lifecycle)
369
+ $initialStatus = $owner && method_exists($owner, 'getInitialTrackingStatus') ? $owner->getInitialTrackingStatus() : null;
370
+ $initialCode = is_array($initialStatus) && !empty($initialStatus['code']) ? $initialStatus['code'] : null;
371
+
294
372
  // create initial status
295
373
  $trackingStatusId = static::createInitialTrackingStatus([
296
374
  'tracking_number_uuid' => $uuid,
297
- 'status' => Str::title($ownerTypeName . ' created'),
298
- 'details' => 'New ' . Str::lower($ownerTypeName) . ' created.',
375
+ 'status' => $initialCode ? $initialStatus['status'] : Str::title($ownerTypeName . ' created'),
376
+ 'details' => $initialCode ? $initialStatus['details'] : 'New ' . Str::lower($ownerTypeName) . ' created.',
299
377
  'location' => $values['location'] ?? Utils::parsePointToWkt(new Point(0, 0)),
300
- 'code' => 'CREATED',
378
+ 'code' => $initialCode ? TrackingStatus::prepareCode($initialCode) : 'CREATED',
301
379
  ]);
302
380
 
303
381
  // update status of tracking number
@@ -309,9 +387,13 @@ class TrackingNumber extends Model
309
387
  // $model->update([ 'status' => 'created' ]);
310
388
 
311
389
  // silent update
312
- static::updateOwnerStatusColumn($owner, 'created');
390
+ static::updateOwnerStatusColumn($owner, $initialCode ?? 'created');
391
+ ApiModelCache::invalidateModelCache($owner, data_get($owner, 'company_uuid', $values['company_uuid']));
313
392
  }
314
393
 
394
+ // Inserted and updated without model events
395
+ ApiModelCache::invalidateModelCache($instance, $values['company_uuid']);
396
+
315
397
  return $uuid;
316
398
  }
317
399
 
@@ -335,8 +417,29 @@ class TrackingNumber extends Model
335
417
  return static::generateNumber($region);
336
418
  }
337
419
 
420
+ /**
421
+ * The base64 PNGs printed on a label: a QR code carrying the tracking url (see
422
+ * TrackingCode) and a Code 128 barcode carrying the bare tracking number.
423
+ *
424
+ * @return array{qr_code: string, barcode: string}
425
+ */
426
+ public static function codeImages(string $trackingNumber, ?string $ownerPublicId = null): array
427
+ {
428
+ return [
429
+ // Medium error correction: labels get scuffed, and the url fits comfortably.
430
+ 'qr_code' => static::newBarcode(TrackingCode::qrContent($trackingNumber, $ownerPublicId), 'QRCODE,M'),
431
+ 'barcode' => static::newBarcode(TrackingCode::barcodeContent($trackingNumber), 'C128'),
432
+ ];
433
+ }
434
+
338
435
  protected static function newBarcode(string $value, string $type): string
339
436
  {
437
+ if ($type === 'C128') {
438
+ // 2px modules, 60px tall: a typical tracking number renders ~330px wide, inside
439
+ // the label's 360px cap, so it prints at its own resolution.
440
+ return DNS1D::getBarcodePNG($value, $type, 2, 60);
441
+ }
442
+
340
443
  return DNS2D::getBarcodePNG($value, $type);
341
444
  }
342
445
 
@@ -0,0 +1,33 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Models;
4
+
5
+ use Fleetbase\Models\Model;
6
+ use Fleetbase\Traits\HasUuid;
7
+ use Illuminate\Database\Eloquent\Relations\HasMany;
8
+
9
+ /**
10
+ * A device that has verified on the public tracking page. The cookie's value is never
11
+ * stored, only its SHA-256 in `token_hash`.
12
+ */
13
+ class TrackingSession extends Model
14
+ {
15
+ use HasUuid;
16
+
17
+ protected $table = 'tracking_sessions';
18
+
19
+ protected $fillable = ['token_hash', 'ip', 'user_agent', 'last_seen_at', 'expires_at', 'revoked_at'];
20
+
21
+ protected $casts = [
22
+ 'last_seen_at' => 'datetime',
23
+ 'expires_at' => 'datetime',
24
+ 'revoked_at' => 'datetime',
25
+ ];
26
+
27
+ protected $hidden = ['token_hash'];
28
+
29
+ public function grants(): HasMany
30
+ {
31
+ return $this->hasMany(TrackingSessionGrant::class, 'tracking_session_uuid', 'uuid');
32
+ }
33
+ }