@fleetbase/fleetops-engine 0.2.6 → 0.2.8

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 (306) hide show
  1. package/.gitattributes +14 -0
  2. package/.php-cs-fixer.php +29 -0
  3. package/LICENSE.md +16 -4
  4. package/README.md +103 -13
  5. package/composer.json +85 -0
  6. package/extension.json +10 -0
  7. package/package.json +3 -3
  8. package/phpstan.neon.dist +8 -0
  9. package/phpunit.xml.dist +16 -0
  10. package/server/config/api.php +765 -0
  11. package/server/config/cache.stores.php +18 -0
  12. package/server/config/dompdf.php +242 -0
  13. package/server/config/fleetops.php +19 -0
  14. package/server/config/geocoder.php +108 -0
  15. package/server/data/globe.json +261 -0
  16. package/server/migrations/2023_04_27_053456_create_contacts_table.php +49 -0
  17. package/server/migrations/2023_04_27_053456_create_drivers_table.php +62 -0
  18. package/server/migrations/2023_04_27_053456_create_entities_table.php +67 -0
  19. package/server/migrations/2023_04_27_053456_create_fleet_drivers_table.php +39 -0
  20. package/server/migrations/2023_04_27_053456_create_fleets_table.php +47 -0
  21. package/server/migrations/2023_04_27_053456_create_fuel_reports_table.php +50 -0
  22. package/server/migrations/2023_04_27_053456_create_integrated_vendors_table.php +44 -0
  23. package/server/migrations/2023_04_27_053456_create_issues_table.php +53 -0
  24. package/server/migrations/2023_04_27_053456_create_orders_table.php +70 -0
  25. package/server/migrations/2023_04_27_053456_create_payloads_table.php +51 -0
  26. package/server/migrations/2023_04_27_053456_create_places_table.php +62 -0
  27. package/server/migrations/2023_04_27_053456_create_positions_table.php +46 -0
  28. package/server/migrations/2023_04_27_053456_create_proofs_table.php +44 -0
  29. package/server/migrations/2023_04_27_053456_create_purchase_rates_table.php +46 -0
  30. package/server/migrations/2023_04_27_053456_create_routes_table.php +40 -0
  31. package/server/migrations/2023_04_27_053456_create_service_areas_table.php +47 -0
  32. package/server/migrations/2023_04_27_053456_create_service_quote_items_table.php +42 -0
  33. package/server/migrations/2023_04_27_053456_create_service_quotes_table.php +47 -0
  34. package/server/migrations/2023_04_27_053456_create_service_rate_fees_table.php +45 -0
  35. package/server/migrations/2023_04_27_053456_create_service_rate_parcel_fees_table.php +47 -0
  36. package/server/migrations/2023_04_27_053456_create_service_rates_table.php +61 -0
  37. package/server/migrations/2023_04_27_053456_create_tracking_numbers_table.php +46 -0
  38. package/server/migrations/2023_04_27_053456_create_tracking_statuses_table.php +54 -0
  39. package/server/migrations/2023_04_27_053456_create_vehicles_table.php +84 -0
  40. package/server/migrations/2023_04_27_053456_create_vendors_table.php +56 -0
  41. package/server/migrations/2023_04_27_053456_create_waypoints_table.php +45 -0
  42. package/server/migrations/2023_04_27_053456_create_zones_table.php +46 -0
  43. package/server/migrations/2023_04_27_053459_add_foreign_keys_to_companies_table.php +32 -0
  44. package/server/migrations/2023_04_27_053459_add_foreign_keys_to_contacts_table.php +34 -0
  45. package/server/migrations/2023_04_27_053459_add_foreign_keys_to_drivers_table.php +40 -0
  46. package/server/migrations/2023_04_27_053459_add_foreign_keys_to_entities_table.php +40 -0
  47. package/server/migrations/2023_04_27_053459_add_foreign_keys_to_fleet_drivers_table.php +34 -0
  48. package/server/migrations/2023_04_27_053459_add_foreign_keys_to_fleets_table.php +36 -0
  49. package/server/migrations/2023_04_27_053459_add_foreign_keys_to_fuel_reports_table.php +36 -0
  50. package/server/migrations/2023_04_27_053459_add_foreign_keys_to_integrated_vendors_table.php +34 -0
  51. package/server/migrations/2023_04_27_053459_add_foreign_keys_to_issues_table.php +38 -0
  52. package/server/migrations/2023_04_27_053459_add_foreign_keys_to_orders_table.php +48 -0
  53. package/server/migrations/2023_04_27_053459_add_foreign_keys_to_payloads_table.php +40 -0
  54. package/server/migrations/2023_04_27_053459_add_foreign_keys_to_places_table.php +32 -0
  55. package/server/migrations/2023_04_27_053459_add_foreign_keys_to_positions_table.php +36 -0
  56. package/server/migrations/2023_04_27_053459_add_foreign_keys_to_proofs_table.php +34 -0
  57. package/server/migrations/2023_04_27_053459_add_foreign_keys_to_purchase_rates_table.php +38 -0
  58. package/server/migrations/2023_04_27_053459_add_foreign_keys_to_routes_table.php +34 -0
  59. package/server/migrations/2023_04_27_053459_add_foreign_keys_to_service_areas_table.php +34 -0
  60. package/server/migrations/2023_04_27_053459_add_foreign_keys_to_service_quote_items_table.php +32 -0
  61. package/server/migrations/2023_04_27_053459_add_foreign_keys_to_service_quotes_table.php +38 -0
  62. package/server/migrations/2023_04_27_053459_add_foreign_keys_to_service_rate_fees_table.php +32 -0
  63. package/server/migrations/2023_04_27_053459_add_foreign_keys_to_service_rate_parcel_fees_table.php +32 -0
  64. package/server/migrations/2023_04_27_053459_add_foreign_keys_to_service_rates_table.php +36 -0
  65. package/server/migrations/2023_04_27_053459_add_foreign_keys_to_tracking_numbers_table.php +34 -0
  66. package/server/migrations/2023_04_27_053459_add_foreign_keys_to_tracking_statuses_table.php +34 -0
  67. package/server/migrations/2023_04_27_053459_add_foreign_keys_to_vehicles_table.php +36 -0
  68. package/server/migrations/2023_04_27_053459_add_foreign_keys_to_vendors_table.php +40 -0
  69. package/server/migrations/2023_04_27_053459_add_foreign_keys_to_waypoints_table.php +38 -0
  70. package/server/migrations/2023_04_27_053459_add_foreign_keys_to_zones_table.php +34 -0
  71. package/server/migrations/2023_09_14_074512_update_vehicles_table.php +100 -0
  72. package/server/migrations/2023_09_14_074619_create_vehicle_devices_table.php +44 -0
  73. package/server/migrations/2023_09_26_071205_add_online_column_to_vehicles_table.php +32 -0
  74. package/server/migrations/2023_09_26_083957_create_fleet_vehicles_table.php +45 -0
  75. package/server/migrations/2023_09_27_023757_add_column_to_fleets_table.php +46 -0
  76. package/server/migrations/2023_10_05_074811_update_vehicle.php +42 -0
  77. package/server/resources/views/labels/default.php +165 -0
  78. package/server/src/Auth/Schemas/FleetOps.php +75 -0
  79. package/server/src/Casts/MultiPolygon.php +50 -0
  80. package/server/src/Casts/Point.php +109 -0
  81. package/server/src/Casts/Polygon.php +46 -0
  82. package/server/src/Console/Commands/DispatchAdhocOrders.php +137 -0
  83. package/server/src/Console/Commands/DispatchOrders.php +71 -0
  84. package/server/src/Console/Commands/TrackOrderDistanceAndTime.php +62 -0
  85. package/server/src/Events/DriverLocationChanged.php +174 -0
  86. package/server/src/Events/DriverSimulatedLocationChanged.php +183 -0
  87. package/server/src/Events/EntityDriverAssigned.php +36 -0
  88. package/server/src/Events/OrderCanceled.php +15 -0
  89. package/server/src/Events/OrderCompleted.php +15 -0
  90. package/server/src/Events/OrderDispatchFailed.php +42 -0
  91. package/server/src/Events/OrderDispatched.php +15 -0
  92. package/server/src/Events/OrderDriverAssigned.php +15 -0
  93. package/server/src/Events/OrderReady.php +15 -0
  94. package/server/src/Events/OrderStarted.php +15 -0
  95. package/server/src/Exceptions/IntegratedVendorException.php +18 -0
  96. package/server/src/Expansions/UserExpansion.php +63 -0
  97. package/server/src/Exports/ContactExport.php +56 -0
  98. package/server/src/Exports/DriverExport.php +61 -0
  99. package/server/src/Exports/FleetExport.php +53 -0
  100. package/server/src/Exports/PlaceExport.php +55 -0
  101. package/server/src/Exports/ServiceAreaExport.php +58 -0
  102. package/server/src/Exports/VehicleExport.php +57 -0
  103. package/server/src/Exports/VendorExport.php +57 -0
  104. package/server/src/Http/Controllers/Api/v1/ContactController.php +133 -0
  105. package/server/src/Http/Controllers/Api/v1/DriverController.php +691 -0
  106. package/server/src/Http/Controllers/Api/v1/EntityController.php +230 -0
  107. package/server/src/Http/Controllers/Api/v1/FleetController.php +146 -0
  108. package/server/src/Http/Controllers/Api/v1/LabelController.php +63 -0
  109. package/server/src/Http/Controllers/Api/v1/OrderController.php +1114 -0
  110. package/server/src/Http/Controllers/Api/v1/PayloadController.php +195 -0
  111. package/server/src/Http/Controllers/Api/v1/PlaceController.php +333 -0
  112. package/server/src/Http/Controllers/Api/v1/PurchaseRateController.php +199 -0
  113. package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +133 -0
  114. package/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php +381 -0
  115. package/server/src/Http/Controllers/Api/v1/ServiceRateController.php +223 -0
  116. package/server/src/Http/Controllers/Api/v1/TrackingNumberController.php +146 -0
  117. package/server/src/Http/Controllers/Api/v1/TrackingStatusController.php +138 -0
  118. package/server/src/Http/Controllers/Api/v1/VehicleController.php +191 -0
  119. package/server/src/Http/Controllers/Api/v1/VendorController.php +159 -0
  120. package/server/src/Http/Controllers/Api/v1/ZoneController.php +146 -0
  121. package/server/src/Http/Controllers/FleetOpsController.php +13 -0
  122. package/server/src/Http/Controllers/Internal/v1/ContactController.php +69 -0
  123. package/server/src/Http/Controllers/Internal/v1/DriverController.php +261 -0
  124. package/server/src/Http/Controllers/Internal/v1/EntityController.php +15 -0
  125. package/server/src/Http/Controllers/Internal/v1/FleetController.php +152 -0
  126. package/server/src/Http/Controllers/Internal/v1/FleetOpsLookupController.php +80 -0
  127. package/server/src/Http/Controllers/Internal/v1/FuelReportController.php +15 -0
  128. package/server/src/Http/Controllers/Internal/v1/GeocoderController.php +96 -0
  129. package/server/src/Http/Controllers/Internal/v1/IntegratedVendorController.php +63 -0
  130. package/server/src/Http/Controllers/Internal/v1/IssueController.php +15 -0
  131. package/server/src/Http/Controllers/Internal/v1/LiveController.php +127 -0
  132. package/server/src/Http/Controllers/Internal/v1/MetricsController.php +75 -0
  133. package/server/src/Http/Controllers/Internal/v1/MorphController.php +77 -0
  134. package/server/src/Http/Controllers/Internal/v1/OrderConfigController.php +269 -0
  135. package/server/src/Http/Controllers/Internal/v1/OrderController.php +564 -0
  136. package/server/src/Http/Controllers/Internal/v1/PayloadController.php +15 -0
  137. package/server/src/Http/Controllers/Internal/v1/PlaceController.php +163 -0
  138. package/server/src/Http/Controllers/Internal/v1/ProofController.php +139 -0
  139. package/server/src/Http/Controllers/Internal/v1/PurchaseRateController.php +15 -0
  140. package/server/src/Http/Controllers/Internal/v1/RouteController.php +15 -0
  141. package/server/src/Http/Controllers/Internal/v1/ServiceAreaController.php +32 -0
  142. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +345 -0
  143. package/server/src/Http/Controllers/Internal/v1/ServiceRateController.php +47 -0
  144. package/server/src/Http/Controllers/Internal/v1/SettingController.php +30 -0
  145. package/server/src/Http/Controllers/Internal/v1/TrackingNumberController.php +15 -0
  146. package/server/src/Http/Controllers/Internal/v1/TrackingStatusController.php +15 -0
  147. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +65 -0
  148. package/server/src/Http/Controllers/Internal/v1/VehicleDeviceController.php +15 -0
  149. package/server/src/Http/Controllers/Internal/v1/VendorController.php +120 -0
  150. package/server/src/Http/Controllers/Internal/v1/ZoneController.php +15 -0
  151. package/server/src/Http/Filter/ContactFilter.php +86 -0
  152. package/server/src/Http/Filter/DriverFilter.php +148 -0
  153. package/server/src/Http/Filter/FleetFilter.php +108 -0
  154. package/server/src/Http/Filter/IntegratedVendorFilter.php +13 -0
  155. package/server/src/Http/Filter/OrderFilter.php +242 -0
  156. package/server/src/Http/Filter/PlaceFilter.php +111 -0
  157. package/server/src/Http/Filter/ServiceAreaFilter.php +13 -0
  158. package/server/src/Http/Filter/ServiceQuoteFilter.php +13 -0
  159. package/server/src/Http/Filter/ServiceRateFilter.php +33 -0
  160. package/server/src/Http/Filter/TrackingStatusFilter.php +43 -0
  161. package/server/src/Http/Filter/VehicleFilter.php +91 -0
  162. package/server/src/Http/Filter/VendorFilter.php +83 -0
  163. package/server/src/Http/Filter/ZoneFilter.php +13 -0
  164. package/server/src/Http/Requests/CancelOrderRequest.php +31 -0
  165. package/server/src/Http/Requests/CreateContactRequest.php +33 -0
  166. package/server/src/Http/Requests/CreateDriverRequest.php +55 -0
  167. package/server/src/Http/Requests/CreateEntityRequest.php +43 -0
  168. package/server/src/Http/Requests/CreateFleetRequest.php +32 -0
  169. package/server/src/Http/Requests/CreateOrderRequest.php +66 -0
  170. package/server/src/Http/Requests/CreatePayloadRequest.php +37 -0
  171. package/server/src/Http/Requests/CreatePlaceRequest.php +38 -0
  172. package/server/src/Http/Requests/CreatePurchaseRateRequest.php +33 -0
  173. package/server/src/Http/Requests/CreateServiceAreaRequest.php +35 -0
  174. package/server/src/Http/Requests/CreateServiceQuoteRequest.php +29 -0
  175. package/server/src/Http/Requests/CreateServiceRateRequest.php +56 -0
  176. package/server/src/Http/Requests/CreateTrackingNumberRequest.php +34 -0
  177. package/server/src/Http/Requests/CreateTrackingStatusRequest.php +42 -0
  178. package/server/src/Http/Requests/CreateVehicleRequest.php +32 -0
  179. package/server/src/Http/Requests/CreateVendorRequest.php +35 -0
  180. package/server/src/Http/Requests/CreateZoneRequest.php +35 -0
  181. package/server/src/Http/Requests/DecodeTrackingNumberQR.php +30 -0
  182. package/server/src/Http/Requests/DriverSimulationRequest.php +46 -0
  183. package/server/src/Http/Requests/Internal/AssignOrderRequest.php +31 -0
  184. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +42 -0
  185. package/server/src/Http/Requests/Internal/FleetActionRequest.php +32 -0
  186. package/server/src/Http/Requests/Internal/UpdateDriverRequest.php +7 -0
  187. package/server/src/Http/Requests/QueryServiceQuotesRequest.php +41 -0
  188. package/server/src/Http/Requests/UpdateContactRequest.php +20 -0
  189. package/server/src/Http/Requests/UpdateDriverRequest.php +7 -0
  190. package/server/src/Http/Requests/UpdateEntityRequest.php +7 -0
  191. package/server/src/Http/Requests/UpdateFleetRequest.php +7 -0
  192. package/server/src/Http/Requests/UpdateOrderRequest.php +7 -0
  193. package/server/src/Http/Requests/UpdatePayloadRequest.php +7 -0
  194. package/server/src/Http/Requests/UpdatePlaceRequest.php +7 -0
  195. package/server/src/Http/Requests/UpdatePurchaseRateRequest.php +7 -0
  196. package/server/src/Http/Requests/UpdateServiceAreaRequest.php +29 -0
  197. package/server/src/Http/Requests/UpdateServiceQuoteRequest.php +29 -0
  198. package/server/src/Http/Requests/UpdateServiceRateRequest.php +29 -0
  199. package/server/src/Http/Requests/UpdateTrackingNumberRequest.php +7 -0
  200. package/server/src/Http/Requests/UpdateTrackingStatusRequest.php +32 -0
  201. package/server/src/Http/Requests/UpdateVehicleRequest.php +7 -0
  202. package/server/src/Http/Requests/UpdateVendorRequest.php +7 -0
  203. package/server/src/Http/Requests/UpdateZoneRequest.php +7 -0
  204. package/server/src/Http/Resources/Internal/v1/IntegratedVendorFacilitator.php +37 -0
  205. package/server/src/Http/Resources/Internal/v1/Vehicle.php +33 -0
  206. package/server/src/Http/Resources/Internal/v1/Waypoint.php +34 -0
  207. package/server/src/Http/Resources/v1/Contact.php +62 -0
  208. package/server/src/Http/Resources/v1/CurrentJob.php +36 -0
  209. package/server/src/Http/Resources/v1/DeletedResource.php +61 -0
  210. package/server/src/Http/Resources/v1/Driver.php +89 -0
  211. package/server/src/Http/Resources/v1/Entity.php +107 -0
  212. package/server/src/Http/Resources/v1/Fleet.php +60 -0
  213. package/server/src/Http/Resources/v1/FuelReport.php +67 -0
  214. package/server/src/Http/Resources/v1/Order.php +131 -0
  215. package/server/src/Http/Resources/v1/Payload.php +94 -0
  216. package/server/src/Http/Resources/v1/Place.php +84 -0
  217. package/server/src/Http/Resources/v1/Proof.php +36 -0
  218. package/server/src/Http/Resources/v1/PurchaseRate.php +77 -0
  219. package/server/src/Http/Resources/v1/ServiceArea.php +55 -0
  220. package/server/src/Http/Resources/v1/ServiceQuote.php +57 -0
  221. package/server/src/Http/Resources/v1/ServiceRate.php +94 -0
  222. package/server/src/Http/Resources/v1/ServiceRateFee.php +58 -0
  223. package/server/src/Http/Resources/v1/ServiceRateParcelFee.php +48 -0
  224. package/server/src/Http/Resources/v1/TrackingNumber.php +62 -0
  225. package/server/src/Http/Resources/v1/TrackingStatus.php +84 -0
  226. package/server/src/Http/Resources/v1/Vehicle.php +75 -0
  227. package/server/src/Http/Resources/v1/VehicleDevice.php +44 -0
  228. package/server/src/Http/Resources/v1/VehicleWithoutDriver.php +72 -0
  229. package/server/src/Http/Resources/v1/Vendor.php +72 -0
  230. package/server/src/Http/Resources/v1/Zone.php +60 -0
  231. package/server/src/Imports/OrdersImport.php +18 -0
  232. package/server/src/Integrations/Lalamove/Lalamove.php +678 -0
  233. package/server/src/Integrations/Lalamove/LalamoveDeliveryStop.php +97 -0
  234. package/server/src/Integrations/Lalamove/LalamoveMarket.php +123 -0
  235. package/server/src/Integrations/Lalamove/LalamoveServiceType.php +161 -0
  236. package/server/src/Jobs/SimulateDrivingRoute.php +81 -0
  237. package/server/src/Jobs/SimulateWaypointReached.php +79 -0
  238. package/server/src/Listeners/HandleOrderCanceled.php +46 -0
  239. package/server/src/Listeners/HandleOrderCreated.php +23 -0
  240. package/server/src/Listeners/HandleOrderDispatchFailed.php +35 -0
  241. package/server/src/Listeners/HandleOrderDispatched.php +108 -0
  242. package/server/src/Listeners/HandleOrderDriverAssigned.php +42 -0
  243. package/server/src/Listeners/HandleOrderReady.php +27 -0
  244. package/server/src/Listeners/HandleUserRemovedFromCompany.php +31 -0
  245. package/server/src/Models/Contact.php +239 -0
  246. package/server/src/Models/Driver.php +543 -0
  247. package/server/src/Models/Entity.php +416 -0
  248. package/server/src/Models/Fleet.php +203 -0
  249. package/server/src/Models/FleetDriver.php +73 -0
  250. package/server/src/Models/FleetVehicle.php +73 -0
  251. package/server/src/Models/FuelReport.php +125 -0
  252. package/server/src/Models/IntegratedVendor.php +219 -0
  253. package/server/src/Models/Issue.php +111 -0
  254. package/server/src/Models/Order.php +1132 -0
  255. package/server/src/Models/Payload.php +699 -0
  256. package/server/src/Models/Place.php +656 -0
  257. package/server/src/Models/Position.php +119 -0
  258. package/server/src/Models/Proof.php +94 -0
  259. package/server/src/Models/PurchaseRate.php +208 -0
  260. package/server/src/Models/Route.php +128 -0
  261. package/server/src/Models/ServiceArea.php +161 -0
  262. package/server/src/Models/ServiceQuote.php +207 -0
  263. package/server/src/Models/ServiceQuoteItem.php +46 -0
  264. package/server/src/Models/ServiceRate.php +953 -0
  265. package/server/src/Models/ServiceRateFee.php +96 -0
  266. package/server/src/Models/ServiceRateParcelFee.php +116 -0
  267. package/server/src/Models/TrackingNumber.php +310 -0
  268. package/server/src/Models/TrackingStatus.php +177 -0
  269. package/server/src/Models/Vehicle.php +452 -0
  270. package/server/src/Models/VehicleDevice.php +79 -0
  271. package/server/src/Models/Vendor.php +244 -0
  272. package/server/src/Models/Waypoint.php +226 -0
  273. package/server/src/Models/Zone.php +95 -0
  274. package/server/src/Notifications/OrderAssigned.php +140 -0
  275. package/server/src/Notifications/OrderCanceled.php +129 -0
  276. package/server/src/Notifications/OrderDispatchFailed.php +81 -0
  277. package/server/src/Notifications/OrderDispatched.php +173 -0
  278. package/server/src/Notifications/OrderPing.php +162 -0
  279. package/server/src/Notifications/OrderSplit.php +56 -0
  280. package/server/src/Observers/DriverObserver.php +34 -0
  281. package/server/src/Observers/OrderObserver.php +34 -0
  282. package/server/src/Observers/PayloadObserver.php +30 -0
  283. package/server/src/Observers/PlaceObserver.php +26 -0
  284. package/server/src/Observers/PurchaseRateObserver.php +61 -0
  285. package/server/src/Observers/ServiceAreaObserver.php +35 -0
  286. package/server/src/Observers/ServiceRateObserver.php +78 -0
  287. package/server/src/Observers/TrackingNumberObserver.php +44 -0
  288. package/server/src/Observers/UserObserver.php +20 -0
  289. package/server/src/Observers/VehicleObserver.php +66 -0
  290. package/server/src/Providers/EventServiceProvider.php +30 -0
  291. package/server/src/Providers/FleetOpsServiceProvider.php +94 -0
  292. package/server/src/Rules/ResolvablePoint.php +37 -0
  293. package/server/src/Scopes/DriverScope.php +21 -0
  294. package/server/src/Support/Algo.php +55 -0
  295. package/server/src/Support/DistanceMatrix.php +27 -0
  296. package/server/src/Support/Encoding/Polyline.php +168 -0
  297. package/server/src/Support/Flow.php +622 -0
  298. package/server/src/Support/Geocoding.php +216 -0
  299. package/server/src/Support/IntegratedVendors.php +287 -0
  300. package/server/src/Support/Metrics.php +305 -0
  301. package/server/src/Support/OSRM.php +191 -0
  302. package/server/src/Support/ParsePhone.php +21 -0
  303. package/server/src/Support/Utils.php +1145 -0
  304. package/server/src/Traits/HasTrackingNumber.php +185 -0
  305. package/server/src/routes.php +381 -0
  306. package/server/tests/Feature.php +5 -0
@@ -0,0 +1,953 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Models;
4
+
5
+ use Brick\Geo\IO\GeoJSONReader;
6
+ use Fleetbase\Casts\Money;
7
+ use Fleetbase\FleetOps\Support\Algo;
8
+ use Fleetbase\FleetOps\Support\Utils;
9
+ use Fleetbase\Models\Model;
10
+ use Fleetbase\Traits\HasApiModelBehavior;
11
+ use Fleetbase\Traits\HasPublicId;
12
+ use Fleetbase\Traits\HasUuid;
13
+ use Fleetbase\Traits\SendsWebhooks;
14
+ use Fleetbase\Traits\TracksApiCredential;
15
+ use Illuminate\Support\Facades\DB;
16
+
17
+ class ServiceRate extends Model
18
+ {
19
+ use HasUuid;
20
+ use HasPublicId;
21
+ use TracksApiCredential;
22
+ use SendsWebhooks;
23
+ use HasApiModelBehavior;
24
+
25
+ /**
26
+ * The database table used by the model.
27
+ *
28
+ * @var string
29
+ */
30
+ protected $table = 'service_rates';
31
+
32
+ /**
33
+ * The type of public Id to generate.
34
+ *
35
+ * @var string
36
+ */
37
+ protected $publicIdType = 'service';
38
+
39
+ /**
40
+ * These attributes that can be queried.
41
+ *
42
+ * @var array
43
+ */
44
+ protected $searchableColumns = [];
45
+
46
+ /**
47
+ * The attributes that are mass assignable.
48
+ *
49
+ * @var array
50
+ */
51
+ protected $fillable = [
52
+ '_key',
53
+ 'company_uuid',
54
+ 'service_area_uuid',
55
+ 'zone_uuid',
56
+ 'service_name',
57
+ 'service_type',
58
+ 'per_meter_flat_rate_fee',
59
+ 'per_meter_unit',
60
+ 'base_fee',
61
+ 'algorithm',
62
+ 'rate_calculation_method',
63
+ 'has_cod_fee',
64
+ 'cod_calculation_method',
65
+ 'cod_flat_fee',
66
+ 'cod_percent',
67
+ 'has_peak_hours_fee',
68
+ 'peak_hours_calculation_method',
69
+ 'peak_hours_flat_fee',
70
+ 'peak_hours_percent',
71
+ 'peak_hours_start',
72
+ 'peak_hours_end',
73
+ 'currency',
74
+ 'duration_terms',
75
+ 'estimated_days',
76
+ ];
77
+
78
+ /**
79
+ * The attributes that should be cast to native types.
80
+ *
81
+ * @var array
82
+ */
83
+ protected $casts = [
84
+ 'base_fee' => Money::class . ':currency',
85
+ 'per_meter_flat_rate_fee' => Money::class . ':currency',
86
+ 'cod_flat_fee' => Money::class . ':currency',
87
+ 'peak_hours_flat_fee' => Money::class . ':currency',
88
+ ];
89
+
90
+ /**
91
+ * Dynamic attributes that are appended to object.
92
+ *
93
+ * @var array
94
+ */
95
+ protected $appends = ['service_area_name', 'zone_name'];
96
+
97
+ /**
98
+ * The attributes excluded from the model's JSON form.
99
+ *
100
+ * @var array
101
+ */
102
+ protected $hidden = ['serviceArea', 'zone'];
103
+
104
+ /**
105
+ * Attributes that is filterable on this model.
106
+ *
107
+ * @var array
108
+ */
109
+ protected $filterParams = [];
110
+
111
+ /**
112
+ * @return \Illuminate\Database\Eloquent\Relations\HasMany
113
+ */
114
+ public function rateFees()
115
+ {
116
+ return $this->hasMany(ServiceRateFee::class);
117
+ }
118
+
119
+ /**
120
+ * @return \Illuminate\Database\Eloquent\Relations\HasMany
121
+ */
122
+ public function parcelFees()
123
+ {
124
+ return $this->hasMany(ServiceRateParcelFee::class);
125
+ }
126
+
127
+ /**
128
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
129
+ */
130
+ public function serviceArea()
131
+ {
132
+ return $this->belongsTo(ServiceArea::class)->whereNull('deleted_at');
133
+ }
134
+
135
+ /**
136
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
137
+ */
138
+ public function zone()
139
+ {
140
+ return $this->belongsTo(Zone::class);
141
+ }
142
+
143
+ /**
144
+ * Get the service area name attribute.
145
+ */
146
+ public function getServiceAreaNameAttribute(): ?string
147
+ {
148
+ return data_get($this, 'serviceArea.name');
149
+ }
150
+
151
+ /**
152
+ * Get the zone name attribute.
153
+ */
154
+ public function getZoneNameAttribute(): ?string
155
+ {
156
+ return data_get($this, 'zone.name');
157
+ }
158
+
159
+ /**
160
+ * Set the number of estimated days for the service to complete.
161
+ *
162
+ * @param int $estimatedDays
163
+ *
164
+ * @return void
165
+ */
166
+ public function setEstimatedDaysAttribute($estimatedDays = 0)
167
+ {
168
+ $this->attributes['estimated_days'] = $estimatedDays ?? 0;
169
+ }
170
+
171
+ /**
172
+ * Check if the rate calculation method matches the given method.
173
+ *
174
+ * @param string $method
175
+ */
176
+ public function isRateCalculationMethod($method): bool
177
+ {
178
+ return $this->rate_calculation_method === $method;
179
+ }
180
+
181
+ /**
182
+ * Check if the rate calculation method is "fixed_meter".
183
+ */
184
+ public function isFixedMeter(): bool
185
+ {
186
+ return $this->rate_calculation_method === 'fixed_meter';
187
+ }
188
+
189
+ /**
190
+ * Check if the rate calculation method is "per_meter".
191
+ */
192
+ public function isPerMeter(): bool
193
+ {
194
+ return $this->rate_calculation_method === 'per_meter';
195
+ }
196
+
197
+ /**
198
+ * Check if the rate calculation method is "per_drop".
199
+ */
200
+ public function isPerDrop(): bool
201
+ {
202
+ return $this->rate_calculation_method === 'per_drop';
203
+ }
204
+
205
+ /**
206
+ * Check if the rate calculation method is "algo".
207
+ */
208
+ public function isAlgorithm(): bool
209
+ {
210
+ return $this->rate_calculation_method === 'algo';
211
+ }
212
+
213
+ /**
214
+ * Check if the service type is "parcel".
215
+ */
216
+ public function isParcelService(): bool
217
+ {
218
+ return $this->service_type === 'parcel';
219
+ }
220
+
221
+ /**
222
+ * Check if the object has a peak hours fee.
223
+ */
224
+ public function hasPeakHoursFee(): bool
225
+ {
226
+ return (bool) $this->has_peak_hours_fee;
227
+ }
228
+
229
+ /**
230
+ * Check if the current time is within peak hours.
231
+ */
232
+ public function isWithinPeakHours(): bool
233
+ {
234
+ $currentTime = strtotime(date('H:i'));
235
+ $startTime = strtotime($this->peak_hours_start);
236
+ $endTime = strtotime($this->peak_hours_end);
237
+
238
+ return $currentTime >= $startTime && $currentTime <= $endTime;
239
+ }
240
+
241
+ /**
242
+ * Check if the peak hours calculation method is "flat".
243
+ */
244
+ public function hasPeakHoursFlatFee(): bool
245
+ {
246
+ return $this->peak_hours_calculation_method === 'flat';
247
+ }
248
+
249
+ /**
250
+ * Check if the peak hours calculation method is "percentage".
251
+ */
252
+ public function hasPeakHoursPercentageFee(): bool
253
+ {
254
+ return $this->peak_hours_calculation_method === 'percentage';
255
+ }
256
+
257
+ /**
258
+ * Check if the object has a COD fee.
259
+ */
260
+ public function hasCodFee(): bool
261
+ {
262
+ return (bool) $this->has_cod_fee;
263
+ }
264
+
265
+ /**
266
+ * Check if the COD calculation method is "flat".
267
+ */
268
+ public function hasCodFlatFee(): bool
269
+ {
270
+ return $this->cod_calculation_method === 'flat';
271
+ }
272
+
273
+ /**
274
+ * Check if the COD calculation method is "percentage".
275
+ */
276
+ public function hasCodPercentageFee(): bool
277
+ {
278
+ return $this->cod_calculation_method === 'percentage';
279
+ }
280
+
281
+ /**
282
+ * Check if the object has a related zone.
283
+ */
284
+ public function hasZone(): bool
285
+ {
286
+ return $this->loadMissing('zone')->zone instanceof Zone;
287
+ }
288
+
289
+ /**
290
+ * Check if the object has a related service area.
291
+ */
292
+ public function hasServiceArea(): bool
293
+ {
294
+ return $this->loadMissing('serviceArea')->serviceArea instanceof ServiceArea;
295
+ }
296
+
297
+ /**
298
+ * Set the service rate fees for the current object.
299
+ *
300
+ * @param array|null $serviceRateFees an optional array of service rate fees
301
+ *
302
+ * @return $this
303
+ */
304
+ public function setServiceRateFees(?array $serviceRateFees = [])
305
+ {
306
+ if (!$serviceRateFees) {
307
+ return $this;
308
+ }
309
+
310
+ $iterate = count($serviceRateFees);
311
+
312
+ for ($i = 0; $i < $iterate; $i++) {
313
+ // if already has uuid then we just update the record and remove from insert array
314
+ if (!empty($serviceRateFees[$i]['uuid'])) {
315
+ $id = $serviceRateFees[$i]['uuid'];
316
+ $updateableAttributes = collect($serviceRateFees[$i])->except(['uuid', 'created_at', 'updated_at'])->toArray();
317
+
318
+ if ($updateableAttributes) {
319
+ ServiceRateFee::where('uuid', $id)->update($updateableAttributes);
320
+ }
321
+
322
+ unset($serviceRateFees[$i]);
323
+ continue;
324
+ }
325
+
326
+ $serviceRateFees[$i]['service_rate_uuid'] = $this->uuid;
327
+ }
328
+
329
+ $serviceRateFees = collect($serviceRateFees)->filter()->values()->toArray();
330
+ ServiceRateFee::bulkInsert($serviceRateFees);
331
+
332
+ return $this;
333
+ }
334
+
335
+ /**
336
+ * Set the service rate parcel fees for the current object.
337
+ *
338
+ * @param array|null $serviceRateParcelFees an optional array of service rate parcel fees
339
+ *
340
+ * @return $this
341
+ */
342
+ public function setServiceRateParcelFees(?array $serviceRateParcelFees = [])
343
+ {
344
+ if (!$serviceRateParcelFees) {
345
+ return $this;
346
+ }
347
+
348
+ $iterate = count($serviceRateParcelFees);
349
+
350
+ for ($i = 0; $i < $iterate; $i++) {
351
+ // if already has uuid then we just update the record and remove from insert array
352
+ if (isset($serviceRateParcelFees[$i]['uuid'])) {
353
+ $id = $serviceRateParcelFees[$i]['uuid'];
354
+ $updateableAttributes = collect($serviceRateParcelFees[$i])->except(['uuid', 'created_at', 'updated_at'])->toArray();
355
+
356
+ if ($updateableAttributes) {
357
+ ServiceRateParcelFee::where('uuid', $id)->update($updateableAttributes);
358
+ }
359
+
360
+ unset($serviceRateParcelFees[$i]);
361
+ continue;
362
+ }
363
+
364
+ $serviceRateParcelFees[$i]['service_rate_uuid'] = $this->uuid;
365
+ }
366
+
367
+ $serviceRateParcelFees = collect($serviceRateParcelFees)->filter()->values()->toArray();
368
+ ServiceRateParcelFee::bulkInsert($serviceRateParcelFees);
369
+
370
+ return $this;
371
+ }
372
+
373
+ /**
374
+ * Get the service rates applicable for the given waypoints.
375
+ *
376
+ * @param array $waypoints an array of waypoints to check against service areas and zones
377
+ * @param \Closure|null $queryCallback an optional closure to modify the service rates query
378
+ *
379
+ * @return array an array of applicable service rates
380
+ */
381
+ public static function getServicableForWaypoints($waypoints = [], \Closure $queryCallback = null): array
382
+ {
383
+ $reader = new GeoJSONReader();
384
+ $applicableServiceRates = [];
385
+ $serviceRatesQuery = static::with(['zone', 'serviceArea']);
386
+
387
+ if (is_callable($queryCallback)) {
388
+ $queryCallback($serviceRatesQuery);
389
+ }
390
+
391
+ // get service rates
392
+ $serviceRates = $serviceRatesQuery->get();
393
+
394
+ foreach ($serviceRates as $serviceRate) {
395
+ if ($serviceRate->hasServiceArea()) {
396
+ if (Utils::exists($serviceRate, 'serviceArea.border')) {
397
+ // make sure all waypoints fall within the service area
398
+ foreach ($serviceRate->serviceArea->border as $polygon) {
399
+ $polygon = $reader->read($polygon->toJson());
400
+
401
+ foreach ($waypoints as $waypoint) {
402
+ if (!$polygon->contains($waypoint)) {
403
+ // waypoint outside of service area, not applicable to route
404
+ continue;
405
+ }
406
+ }
407
+ }
408
+ }
409
+ }
410
+
411
+ if ($serviceRate->hasZone()) {
412
+ // make sure all waypoints fall within the service area
413
+ if (Utils::exists($serviceRate, 'zone.border')) {
414
+ foreach ($serviceRate->zone->border as $polygon) {
415
+ $polygon = $reader->read($polygon->toJson());
416
+
417
+ foreach ($waypoints as $waypoint) {
418
+ if (!$polygon->contains($waypoint)) {
419
+ // waypoint outside of zone, not applicable to route
420
+ continue;
421
+ }
422
+ }
423
+ }
424
+ }
425
+ }
426
+
427
+ $applicableServiceRates[] = $serviceRate;
428
+ }
429
+
430
+ return $applicableServiceRates;
431
+ }
432
+
433
+ /**
434
+ * Get the service rates applicable for the given places based on service type and currency.
435
+ *
436
+ * @param array $places an array of places to check against service areas and zones
437
+ * @param string|null $service an optional service type to filter service rates
438
+ * @param string|null $currency an optional currency to filter service rates
439
+ * @param \Closure|null $queryCallback an optional closure to modify the service rates query
440
+ *
441
+ * @return array an array of applicable service rates
442
+ */
443
+ public static function getServicableForPlaces($places = [], $service = null, $currency = null, \Closure $queryCallback = null): array
444
+ {
445
+ $reader = new GeoJSONReader();
446
+ $applicableServiceRates = [];
447
+ $serviceRatesQuery = static::with(['zone', 'serviceArea', 'rateFees', 'parcelFees']);
448
+
449
+ if ($currency) {
450
+ $serviceRatesQuery->where(DB::raw('lower(currency)'), strtolower($currency));
451
+ }
452
+
453
+ if ($service) {
454
+ $serviceRatesQuery->where('service_type', $service);
455
+ }
456
+
457
+ if (is_callable($queryCallback)) {
458
+ $queryCallback($serviceRatesQuery);
459
+ }
460
+
461
+ $serviceRates = $serviceRatesQuery->get();
462
+
463
+ $waypoints = collect($places)->map(function ($place) {
464
+ return $place->getLocationAsPoint();
465
+ });
466
+
467
+ foreach ($serviceRates as $serviceRate) {
468
+ if ($serviceRate->hasServiceArea()) {
469
+ // make sure all waypoints fall within the service area
470
+ foreach ($serviceRate->serviceArea->border as $polygon) {
471
+ $polygon = $reader->read($polygon->toJson());
472
+
473
+ foreach ($waypoints as $waypoint) {
474
+ if (!$polygon->contains($waypoint)) {
475
+ // waypoint outside of service area, not applicable to route
476
+ continue;
477
+ }
478
+ }
479
+ }
480
+ }
481
+
482
+ if ($serviceRate->hasZone()) {
483
+ // make sure all waypoints fall within the service area
484
+ foreach ($serviceRate->zone->border as $polygon) {
485
+ $polygon = $reader->read($polygon->toJson());
486
+
487
+ foreach ($waypoints as $waypoint) {
488
+ if (!$polygon->contains($waypoint)) {
489
+ // waypoint outside of zone, not applicable to route
490
+ continue;
491
+ }
492
+ }
493
+ }
494
+ }
495
+
496
+ $applicableServiceRates[] = $serviceRate;
497
+ }
498
+
499
+ return $applicableServiceRates;
500
+ }
501
+
502
+ /**
503
+ * Generate a quote for a given pickup and dropoff point and entities.
504
+ *
505
+ * @param string $pickupPoint the coordinates of the pickup point
506
+ * @param string $dropoffPoint the coordinates of the dropoff point
507
+ * @param array $entities an array of entities to be considered for the quote
508
+ *
509
+ * @return mixed the calculated quote based on the preliminary data
510
+ */
511
+ public function pointQuote($pickupPoint, $dropoffPoint, $entities = [])
512
+ {
513
+ $payload = new Payload();
514
+ $payload->entities = $entities;
515
+ $payload->pickup = $pickup = new Place([
516
+ 'location' => Utils::getPointFromCoordinates($pickupPoint),
517
+ ]);
518
+ $payload->dropoff = $dropoff = new Place([
519
+ 'location' => Utils::getPointFromCoordinates($dropoffPoint),
520
+ ]);
521
+
522
+ // calculate distance and time
523
+ $matrix = Utils::getDrivingDistanceAndTime($payload->pickup, $payload->dropoff);
524
+
525
+ return $this->quoteFromPreliminaryData($entities, [$pickup, $dropoff], $matrix->distance, $matrix->time);
526
+ }
527
+
528
+ /**
529
+ * Generate a quote based on the preliminary data provided.
530
+ *
531
+ * @param array $entities an array of entities to be considered for the quote
532
+ * @param array $waypoints an array of waypoints to be considered for the quote
533
+ * @param int|null $totalDistance the total distance for the service in meters
534
+ * @param int|null $totalTime the total time for the service in seconds
535
+ * @param bool|null $isCashOnDelivery flag indicating if the payment method is Cash on Delivery
536
+ *
537
+ * @return array an array containing the calculated quote and line items
538
+ */
539
+ public function quoteFromPreliminaryData($entities = [], $waypoints = [], ?int $totalDistance = 0, ?int $totalTime = 0, ?bool $isCashOnDelivery = false)
540
+ {
541
+ $lines = collect();
542
+ $subTotal = data_get($this, 'base_fee', 0);
543
+
544
+ $lines->push([
545
+ 'details' => 'Base Fee',
546
+ 'raw_amount' => $subTotal,
547
+ 'amount' => Utils::numbersOnly($subTotal),
548
+ 'formatted_amount' => Utils::moneyFormat($subTotal, $this->currency),
549
+ 'currency' => $this->currency,
550
+ 'code' => 'BASE_FEE',
551
+ ]);
552
+
553
+ if ($this->isFixedMeter()) {
554
+ $distanceFee = $this->findServiceRateFeeByDistance($totalDistance);
555
+
556
+ if ($distanceFee) {
557
+ $subTotal += Utils::numbersOnly($distanceFee->fee);
558
+
559
+ $lines->push([
560
+ 'details' => 'Service Fee',
561
+ 'raw_amount' => $distanceFee->fee,
562
+ 'amount' => Utils::numbersOnly($distanceFee->fee),
563
+ 'formatted_amount' => Utils::moneyFormat($distanceFee->fee, $this->currency),
564
+ 'currency' => $this->currency,
565
+ 'code' => 'BASE_FEE',
566
+ ]);
567
+ }
568
+ }
569
+
570
+ if ($this->isPerDrop()) {
571
+ $rateFee = $this->findServiceRateFeeByMinMax(count($waypoints));
572
+
573
+ if ($rateFee) {
574
+ $subTotal += Utils::numbersOnly($rateFee->fee);
575
+
576
+ $lines->push([
577
+ 'details' => 'Service Fee',
578
+ 'amount' => Utils::numbersOnly($rateFee->fee),
579
+ 'formatted_amount' => Utils::moneyFormat($rateFee->fee, $this->currency),
580
+ 'currency' => $this->currency,
581
+ 'code' => 'BASE_FEE',
582
+ ]);
583
+ }
584
+ }
585
+
586
+ if ($this->isPerMeter()) {
587
+ $perMeterDistance = $this->per_meter_unit === 'km' ? round($totalDistance / 1000) : $totalDistance;
588
+ $rateFee = $perMeterDistance * $this->per_meter_flat_rate_fee;
589
+ $subTotal += $rateFee;
590
+
591
+ $lines->push([
592
+ 'details' => 'Service Fee',
593
+ 'amount' => Utils::numbersOnly($rateFee),
594
+ 'formatted_amount' => Utils::moneyFormat($rateFee, $this->currency),
595
+ 'currency' => $this->currency,
596
+ 'code' => 'BASE_FEE',
597
+ ]);
598
+ }
599
+
600
+ if ($this->isAlgorithm()) {
601
+ $rateFee = Algo::exec(
602
+ $this->algorithm,
603
+ [
604
+ 'distance' => $totalDistance,
605
+ 'time' => $totalTime,
606
+ ],
607
+ true
608
+ );
609
+
610
+ $subTotal += Utils::numbersOnly($rateFee);
611
+
612
+ $lines->push([
613
+ 'details' => 'Service Fee',
614
+ 'amount' => Utils::numbersOnly($rateFee),
615
+ 'formatted_amount' => Utils::moneyFormat($rateFee, $this->currency),
616
+ 'currency' => $this->currency,
617
+ 'code' => 'BASE_FEE',
618
+ ]);
619
+ }
620
+
621
+ // if parcel fee's add into the base rate
622
+ if ($this->isParcelService()) {
623
+ $parcels = collect($entities)->where('type', 'parcel')->all();
624
+
625
+ foreach ($parcels as $parcel) {
626
+ // convert all length units to cm and weight units to grams
627
+ $length = $parcel->length_unit->toUnit('cm');
628
+ $width = $parcel->width_unit->toUnit('cm');
629
+ $height = $parcel->height_unit->toUnit('cm');
630
+ $weight = $parcel->mass_unit->toUnit('g');
631
+ $serviceParcelFee = null;
632
+
633
+ // iterate through parcel fees to find where it fits
634
+ foreach ($this->parcelFees as $parcelFee) {
635
+ $feeLength = $parcelFee->length_unit->toUnit('cm');
636
+ $feeWidth = $parcelFee->width_unit->toUnit('cm');
637
+ $feeHeight = $parcelFee->height_unit->toUnit('cm');
638
+ $feeWeight = $parcelFee->mass_unit->toUnit('g');
639
+
640
+ $previousParcelFee = $parcelFee;
641
+
642
+ if ($length > $feeLength && $width > $feeWidth && $height > $feeHeight && $weight > $feeWeight) {
643
+ continue;
644
+ } elseif ($length < $feeLength && $width < $feeWidth && $height < $feeHeight && $weight < $feeWeight) {
645
+ $serviceParcelFee = $previousParcelFee;
646
+ } else {
647
+ $serviceParcelFee = $parcelFee;
648
+ }
649
+ }
650
+
651
+ // if no distance fee use the last
652
+ if ($serviceParcelFee === null) {
653
+ $serviceParcelFee = $this->parcelFees->sortByDesc()->first();
654
+ }
655
+
656
+ $subTotal += $serviceParcelFee->fee;
657
+
658
+ $lines->push([
659
+ 'details' => $serviceParcelFee->name . ' parcel fee',
660
+ 'amount' => Utils::numbersOnly($serviceParcelFee->fee),
661
+ 'formatted_amount' => Utils::moneyFormat($serviceParcelFee->fee, $this->currency),
662
+ 'currency' => $this->currency,
663
+ 'code' => 'PARCEL_FEE',
664
+ ]);
665
+ }
666
+ }
667
+
668
+ // set the base rate
669
+ $baseRate = $subTotal;
670
+
671
+ // if the rate has cod add this into the quote price
672
+ if ($this->hasCodFee() && $isCashOnDelivery) {
673
+ if ($this->hasCodFlatFee()) {
674
+ $subTotal += $codFee = $this->cod_flat_fee;
675
+ } elseif ($this->hasCodPercentageFee()) {
676
+ $subTotal += $codFee = Utils::calculatePercentage($this->cod_percent, $baseRate);
677
+ }
678
+
679
+ $lines->push([
680
+ 'details' => 'Cash on delivery fee',
681
+ 'amount' => Utils::numbersOnly($codFee),
682
+ 'formatted_amount' => Utils::moneyFormat($codFee, $this->currency),
683
+ 'currency' => $this->currency,
684
+ 'code' => 'COD_FEE',
685
+ ]);
686
+ }
687
+
688
+ // if this has peak hour fee add in
689
+ if ($this->hasPeakHoursFee() && $this->isWithinPeakHours()) {
690
+ if ($this->hasPeakHoursFlatFee()) {
691
+ $subTotal += $peakHoursFee = $this->peak_hours_flat_fee;
692
+ } elseif ($this->hasPeakHoursPercentageFee()) {
693
+ $subTotal += $peakHoursFee = Utils::calculatePercentage($this->peak_hours_percent, $baseRate);
694
+ }
695
+
696
+ $lines->push([
697
+ 'details' => 'Peak hours fee',
698
+ 'amount' => Utils::numbersOnly($peakHoursFee),
699
+ 'formatted_amount' => Utils::moneyFormat($peakHoursFee, $this->currency),
700
+ 'currency' => $this->currency,
701
+ 'code' => 'PEAK_HOUR_FEE',
702
+ ]);
703
+ }
704
+
705
+ return [$subTotal, $lines];
706
+ }
707
+
708
+ /**
709
+ * Generate a quote based on the payload provided.
710
+ *
711
+ * @param Payload $payload an instance of the Payload class containing all necessary data for the quote calculation
712
+ *
713
+ * @return array an array containing the calculated quote and line items
714
+ */
715
+ public function quote(Payload $payload)
716
+ {
717
+ $lines = collect();
718
+ $subTotal = $this->base_fee ?? 0;
719
+
720
+ $lines->push([
721
+ 'details' => 'Base Fee',
722
+ 'amount' => Utils::numbersOnly($subTotal),
723
+ 'formatted_amount' => Utils::moneyFormat($subTotal, $this->currency),
724
+ 'currency' => $this->currency,
725
+ 'code' => 'BASE_FEE',
726
+ ]);
727
+
728
+ // Prepare all waypoints and origin and destination
729
+ $waypoints = $payload->getAllStops()->mapInto(Place::class);
730
+ $origin = $waypoints->first();
731
+ $destinations = $waypoints->skip(1)->toArray();
732
+
733
+ // Lookup distance matrix for total distance and time
734
+ $distanceMatrix = Utils::distanceMatrix([$origin], $destinations);
735
+ $totalDistance = $distanceMatrix->distance;
736
+ $totalTime = $distanceMatrix->time;
737
+
738
+ if ($this->isFixedMeter()) {
739
+ $distanceFee = $this->findServiceRateFeeByDistance($totalDistance);
740
+
741
+ if ($distanceFee) {
742
+ $subTotal += Utils::numbersOnly($distanceFee->fee);
743
+
744
+ $lines->push([
745
+ 'details' => 'Service Fee',
746
+ 'amount' => Utils::numbersOnly($distanceFee->fee),
747
+ 'formatted_amount' => Utils::moneyFormat($distanceFee->fee, $this->currency),
748
+ 'currency' => $this->currency,
749
+ 'code' => 'BASE_FEE',
750
+ ]);
751
+ }
752
+ }
753
+
754
+ if ($this->isPerDrop()) {
755
+ $rateFee = $this->findServiceRateFeeByMinMax(count($waypoints));
756
+
757
+ if ($rateFee) {
758
+ $subTotal += Utils::numbersOnly($rateFee->fee);
759
+
760
+ $lines->push([
761
+ 'details' => 'Service Fee',
762
+ 'amount' => Utils::numbersOnly($rateFee->fee),
763
+ 'formatted_amount' => Utils::moneyFormat($rateFee->fee, $this->currency),
764
+ 'currency' => $this->currency,
765
+ 'code' => 'BASE_FEE',
766
+ ]);
767
+ }
768
+ }
769
+
770
+ if ($this->isPerMeter()) {
771
+ $perMeterDistance = $this->per_meter_unit === 'km' ? round($totalDistance / 1000) : $totalDistance;
772
+ $rateFee = $perMeterDistance * $this->per_meter_flat_rate_fee;
773
+ $subTotal += $rateFee;
774
+
775
+ $lines->push([
776
+ 'details' => 'Service Fee',
777
+ 'amount' => Utils::numbersOnly($rateFee),
778
+ 'formatted_amount' => Utils::moneyFormat($rateFee, $this->currency),
779
+ 'currency' => $this->currency,
780
+ 'code' => 'BASE_FEE',
781
+ ]);
782
+ }
783
+
784
+ if ($this->isAlgorithm()) {
785
+ $rateFee = Algo::exec(
786
+ $this->algorithm,
787
+ [
788
+ 'distance' => $totalDistance,
789
+ 'time' => $totalTime,
790
+ ],
791
+ true
792
+ );
793
+
794
+ $subTotal += Utils::numbersOnly($rateFee);
795
+
796
+ $lines->push([
797
+ 'details' => 'Service Fee',
798
+ 'amount' => Utils::numbersOnly($rateFee),
799
+ 'formatted_amount' => Utils::moneyFormat($rateFee, $this->currency),
800
+ 'currency' => $this->currency,
801
+ 'code' => 'BASE_FEE',
802
+ ]);
803
+ }
804
+
805
+ // if parcel fee's add into the base rate
806
+ if ($this->isParcelService()) {
807
+ $parcels = $payload->entities->where('type', 'parcel')->all();
808
+
809
+ foreach ($parcels as $parcel) {
810
+ // convert all length units to cm and weight units to grams
811
+ $length = $parcel->length_unit->toUnit('cm');
812
+ $width = $parcel->width_unit->toUnit('cm');
813
+ $height = $parcel->height_unit->toUnit('cm');
814
+ $weight = $parcel->mass_unit->toUnit('g');
815
+ $serviceParcelFee = null;
816
+
817
+ // iterate through parcel fees to find where it fits
818
+ foreach ($this->parcelFees as $parcelFee) {
819
+ $feeLength = $parcelFee->length_unit->toUnit('cm');
820
+ $feeWidth = $parcelFee->width_unit->toUnit('cm');
821
+ $feeHeight = $parcelFee->height_unit->toUnit('cm');
822
+ $feeWeight = $parcelFee->mass_unit->toUnit('g');
823
+
824
+ $previousParcelFee = $parcelFee;
825
+
826
+ if ($length > $feeLength && $width > $feeWidth && $height > $feeHeight && $weight > $feeWeight) {
827
+ continue;
828
+ } elseif ($length < $feeLength && $width < $feeWidth && $height < $feeHeight && $weight < $feeWeight) {
829
+ $serviceParcelFee = $previousParcelFee;
830
+ } else {
831
+ $serviceParcelFee = $parcelFee;
832
+ }
833
+ }
834
+
835
+ // if no distance fee use the last
836
+ if ($serviceParcelFee === null) {
837
+ $serviceParcelFee = $this->parcelFees->sortByDesc()->first();
838
+ }
839
+
840
+ $subTotal += $serviceParcelFee->fee;
841
+
842
+ $lines->push([
843
+ 'details' => $serviceParcelFee->name . ' parcel fee',
844
+ 'amount' => Utils::numbersOnly($serviceParcelFee->fee),
845
+ 'formatted_amount' => Utils::moneyFormat($serviceParcelFee->fee, $this->currency),
846
+ 'currency' => $this->currency,
847
+ 'code' => 'PARCEL_FEE',
848
+ ]);
849
+ }
850
+ }
851
+
852
+ // set the base rate
853
+ $baseRate = $subTotal;
854
+
855
+ // if the rate has cod add this into the quote price
856
+ if ($this->hasCodFee() && $payload->cod_amount !== null) {
857
+ if ($this->hasCodFlatFee()) {
858
+ $subTotal += $codFee = $this->cod_flat_fee;
859
+ } elseif ($this->hasCodPercentageFee()) {
860
+ $subTotal += $codFee = Utils::calculatePercentage($this->cod_percent, $baseRate);
861
+ }
862
+
863
+ $lines->push([
864
+ 'details' => 'Cash on delivery fee',
865
+ 'amount' => Utils::numbersOnly($codFee),
866
+ 'formatted_amount' => Utils::moneyFormat($codFee, $this->currency),
867
+ 'currency' => $this->currency,
868
+ 'code' => 'COD_FEE',
869
+ ]);
870
+ }
871
+
872
+ // if this has peak hour fee add in
873
+ if ($this->hasPeakHoursFee() && $this->isWithinPeakHours()) {
874
+ if ($this->hasPeakHoursFlatFee()) {
875
+ $subTotal += $peakHoursFee = $this->peak_hours_flat_fee;
876
+ } elseif ($this->hasPeakHoursPercentageFee()) {
877
+ $subTotal += $peakHoursFee = Utils::calculatePercentage($this->peak_hours_percent, $baseRate);
878
+ }
879
+
880
+ $lines->push([
881
+ 'details' => 'Peak hours fee',
882
+ 'amount' => Utils::numbersOnly($peakHoursFee),
883
+ 'formatted_amount' => Utils::moneyFormat($peakHoursFee, $this->currency),
884
+ 'currency' => $this->currency,
885
+ 'code' => 'PEAK_HOUR_FEE',
886
+ ]);
887
+ }
888
+
889
+ return [$subTotal, $lines];
890
+ }
891
+
892
+ /**
893
+ * Find the ServiceRateFee based on the total distance.
894
+ *
895
+ * @param int $totalDistance the total distance in meters
896
+ *
897
+ * @return ServiceRateFee|null the ServiceRateFee instance if found, otherwise null
898
+ */
899
+ public function findServiceRateFeeByDistance(int $totalDistance): ?ServiceRateFee
900
+ {
901
+ $this->load('rateFees');
902
+
903
+ $distanceInKms = round($totalDistance / 1000);
904
+ $distanceFee = null;
905
+
906
+ foreach ($this->rateFees as $rateFee) {
907
+ $previousRateFee = $rateFee;
908
+
909
+ if ($distanceInKms > $rateFee->distance) {
910
+ continue;
911
+ } elseif ($rateFee->distance > $distanceInKms) {
912
+ $distanceFee = $previousRateFee;
913
+ } else {
914
+ $distanceFee = $rateFee;
915
+ }
916
+ }
917
+
918
+ // if no distance fee use the last
919
+ if ($distanceFee === null) {
920
+ $distanceFee = $this->rateFees->sortByDesc('distance')->first();
921
+ }
922
+
923
+ return $distanceFee;
924
+ }
925
+
926
+ /**
927
+ * Find the ServiceRateFee based on the given number within the min and max range.
928
+ *
929
+ * @param int $number the number to check within the ServiceRateFee's min and max range
930
+ *
931
+ * @return ServiceRateFee|null the ServiceRateFee instance if found, otherwise null
932
+ */
933
+ public function findServiceRateFeeByMinMax(int $number): ?ServiceRateFee
934
+ {
935
+ $this->load('rateFees');
936
+
937
+ $serviceRateFee = null;
938
+
939
+ foreach ($this->rateFees as $rateFee) {
940
+ if ($rateFee->isWithinMinMax($number)) {
941
+ $serviceRateFee = $rateFee;
942
+ break;
943
+ }
944
+ }
945
+
946
+ // if no distance fee use the last
947
+ if ($serviceRateFee === null) {
948
+ $serviceRateFee = $this->rateFees->sortByDesc('max')->first();
949
+ }
950
+
951
+ return $serviceRateFee;
952
+ }
953
+ }