@fleetbase/fleetops-engine 0.6.48 → 0.6.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (266) hide show
  1. package/addon/components/customer/create-order-form.hbs +2 -2
  2. package/addon/components/customer/form.hbs +2 -2
  3. package/addon/components/customer/order-form.hbs +2 -2
  4. package/addon/components/customer/orders.hbs +2 -2
  5. package/addon/components/display-place.hbs +2 -2
  6. package/addon/components/driver/details.hbs +5 -1
  7. package/addon/components/driver/form.hbs +11 -1
  8. package/addon/components/fleet-listing-panel.hbs +2 -2
  9. package/addon/components/fleet-listing-panel.js +10 -0
  10. package/addon/components/map/google-live-map.js +18 -75
  11. package/addon/components/map/leaflet-live-map.hbs +187 -37
  12. package/addon/components/map/leaflet-live-map.js +184 -20
  13. package/addon/components/modals/order-change-destination.hbs +46 -0
  14. package/addon/components/modals/order-change-destination.js +106 -0
  15. package/addon/components/modals/stop-activity.hbs +28 -0
  16. package/addon/components/modals/stop-activity.js +52 -0
  17. package/addon/components/modals/update-order-activity.hbs +45 -3
  18. package/addon/components/modals/update-order-activity.js +143 -5
  19. package/addon/components/orchestrator/phase-builder.hbs +21 -0
  20. package/addon/components/orchestrator/phase-builder.js +13 -0
  21. package/addon/components/orchestrator/resource-panel.hbs +0 -10
  22. package/addon/components/orchestrator/resource-panel.js +2 -4
  23. package/addon/components/orchestrator-workbench.js +1 -0
  24. package/addon/components/order/activity-list.hbs +2 -2
  25. package/addon/components/order/activity-timeline.hbs +31 -29
  26. package/addon/components/order/details/activity.hbs +3 -3
  27. package/addon/components/order/details/activity.js +11 -0
  28. package/addon/components/order/details/documents.js +2 -0
  29. package/addon/components/order/details/purchase-rate.hbs +2 -2
  30. package/addon/components/order/details/route.hbs +11 -3
  31. package/addon/components/order/details/route.js +91 -14
  32. package/addon/components/order/details/tracking.hbs +101 -72
  33. package/addon/components/order/details/tracking.js +149 -2
  34. package/addon/components/order/form/details.hbs +3 -3
  35. package/addon/components/order/form/documents.js +11 -4
  36. package/addon/components/order/form/route.hbs +108 -104
  37. package/addon/components/order/form/route.js +37 -1
  38. package/addon/components/order/form/service-rate.hbs +2 -2
  39. package/addon/components/order/form.hbs +2 -2
  40. package/addon/components/order-config-manager/activity-flow.hbs +4 -1
  41. package/addon/components/order-config-manager/activity-flow.js +178 -12
  42. package/addon/components/place/address.js +1 -1
  43. package/addon/components/route-list.hbs +17 -17
  44. package/addon/components/route-list.js +42 -10
  45. package/addon/components/service-rate/details.hbs +41 -0
  46. package/addon/components/service-rate/form.hbs +118 -0
  47. package/addon/components/service-rate/form.js +55 -0
  48. package/addon/components/vehicle/form.js +1 -1
  49. package/addon/components/vehicle/pill.hbs +1 -1
  50. package/addon/components/widget/fleet-ops-key-metrics.hbs +19 -17
  51. package/addon/components/widget/fleet-ops-key-metrics.js +26 -0
  52. package/addon/components/widget/fuel-efficiency.hbs +26 -0
  53. package/addon/components/widget/fuel-efficiency.js +43 -0
  54. package/addon/components/widget/geofence-violations.hbs +44 -0
  55. package/addon/components/widget/geofence-violations.js +55 -0
  56. package/addon/components/widget/issues-insights.hbs +43 -0
  57. package/addon/components/widget/issues-insights.js +62 -0
  58. package/addon/components/widget/kpi-active-orders.hbs +10 -0
  59. package/addon/components/widget/kpi-active-orders.js +5 -0
  60. package/addon/components/widget/kpi-aov.hbs +10 -0
  61. package/addon/components/widget/kpi-aov.js +5 -0
  62. package/addon/components/widget/kpi-distance.hbs +10 -0
  63. package/addon/components/widget/kpi-distance.js +5 -0
  64. package/addon/components/widget/kpi-drivers-online.hbs +10 -0
  65. package/addon/components/widget/kpi-drivers-online.js +5 -0
  66. package/addon/components/widget/kpi-earnings.hbs +10 -0
  67. package/addon/components/widget/kpi-earnings.js +5 -0
  68. package/addon/components/widget/kpi-open-issues.hbs +11 -0
  69. package/addon/components/widget/kpi-open-issues.js +5 -0
  70. package/addon/components/widget/kpi-tile.hbs +59 -0
  71. package/addon/components/widget/kpi-tile.js +168 -0
  72. package/addon/components/widget/live-fleet.hbs +122 -0
  73. package/addon/components/widget/live-fleet.js +365 -0
  74. package/addon/components/widget/maintenance-overview.hbs +45 -0
  75. package/addon/components/widget/maintenance-overview.js +40 -0
  76. package/addon/components/widget/on-time-delivery.hbs +27 -0
  77. package/addon/components/widget/on-time-delivery.js +73 -0
  78. package/addon/components/widget/operations-pulse.hbs +51 -0
  79. package/addon/components/widget/operations-pulse.js +173 -0
  80. package/addon/components/widget/orders-by-status.hbs +26 -0
  81. package/addon/components/widget/orders-by-status.js +54 -0
  82. package/addon/components/widget/revenue-trend.hbs +34 -0
  83. package/addon/components/widget/revenue-trend.js +67 -0
  84. package/addon/components/widget/top-drivers.hbs +44 -0
  85. package/addon/components/widget/top-drivers.js +54 -0
  86. package/addon/controllers/management/vehicles/index/new.js +1 -1
  87. package/addon/controllers/operations/orders/index/details.js +4 -4
  88. package/addon/controllers/operations/orders/index.js +14 -0
  89. package/addon/controllers/settings/routing.js +26 -0
  90. package/addon/extension.js +172 -3
  91. package/addon/helpers/place-address-html.js +2 -2
  92. package/addon/routes/operations/service-rates/index.js +1 -1
  93. package/addon/services/geofence.js +163 -19
  94. package/addon/services/leaflet-contextmenu-manager.js +27 -0
  95. package/addon/services/map-adapter/google.js +13 -2
  96. package/addon/services/vehicle-actions.js +1 -1
  97. package/addon/styles/fleetops-engine.css +205 -88
  98. package/addon/templates/maintenance/equipment/index.hbs +1 -1
  99. package/addon/templates/maintenance/parts/index.hbs +1 -1
  100. package/addon/templates/maintenance/schedules/index.hbs +1 -1
  101. package/addon/templates/management/vehicles/index.hbs +1 -1
  102. package/addon/templates/settings/avatars.hbs +1 -1
  103. package/addon/templates/settings/custom-fields.hbs +1 -1
  104. package/addon/templates/settings/map.hbs +1 -1
  105. package/addon/templates/settings/navigator-app.hbs +1 -1
  106. package/addon/templates/settings/notifications.hbs +1 -1
  107. package/addon/templates/settings/orchestrator.hbs +1 -1
  108. package/addon/templates/settings/payments/index.hbs +1 -1
  109. package/addon/templates/settings/payments/onboard.hbs +1 -1
  110. package/addon/templates/settings/routing.hbs +22 -2
  111. package/addon/templates/settings/scheduling.hbs +1 -1
  112. package/addon/utils/fleet-ops-options.js +2 -1
  113. package/addon/utils/live-map-card-content.js +171 -0
  114. package/addon/utils/normalize-order-config-flow.js +41 -0
  115. package/addon/utils/place-address-html.js +3 -2
  116. package/addon/utils/route-visualization.js +54 -7
  117. package/app/components/modals/order-change-destination.js +1 -0
  118. package/app/components/modals/stop-activity.js +1 -0
  119. package/app/components/widget/fuel-efficiency.js +1 -0
  120. package/app/components/widget/geofence-violations.js +1 -0
  121. package/app/components/widget/issues-insights.js +1 -0
  122. package/app/components/widget/kpi-active-orders.js +1 -0
  123. package/app/components/widget/kpi-aov.js +1 -0
  124. package/app/components/widget/kpi-distance.js +1 -0
  125. package/app/components/widget/kpi-drivers-online.js +1 -0
  126. package/app/components/widget/kpi-earnings.js +1 -0
  127. package/app/components/widget/kpi-open-issues.js +1 -0
  128. package/app/components/widget/kpi-tile.js +1 -0
  129. package/app/components/widget/live-fleet.js +1 -0
  130. package/app/components/widget/maintenance-overview.js +1 -0
  131. package/app/components/widget/on-time-delivery.js +1 -0
  132. package/app/components/widget/operations-pulse.js +1 -0
  133. package/app/components/widget/orders-by-status.js +1 -0
  134. package/app/components/widget/revenue-trend.js +1 -0
  135. package/app/components/widget/top-drivers.js +1 -0
  136. package/app/utils/live-map-card-content.js +1 -0
  137. package/composer.json +1 -1
  138. package/extension.json +1 -1
  139. package/package.json +3 -3
  140. package/pnpm-workspace.yaml +1 -0
  141. package/server/config/telematics.php +111 -1
  142. package/server/migrations/2026_05_18_000001_add_license_expiry_to_drivers_table.php +25 -0
  143. package/server/migrations/2026_05_21_000001_add_geographic_rules_to_service_rate_fees_table.php +37 -0
  144. package/server/migrations/2026_05_25_000001_add_endpoint_tracking_refs_to_payloads_table.php +50 -0
  145. package/server/migrations/2026_05_27_000001_default_driver_vehicle_statuses_to_available.php +48 -0
  146. package/server/resources/views/labels/default.php +57 -49
  147. package/server/resources/views/labels/waypoint-label.php +27 -20
  148. package/server/seeders/Testing/Concerns/SeedsTestingData.php +183 -0
  149. package/server/seeders/Testing/ConnectivitySeeder.php +213 -0
  150. package/server/seeders/Testing/FleetSeeder.php +341 -0
  151. package/server/seeders/Testing/MaintenanceSeeder.php +238 -0
  152. package/server/seeders/Testing/NetworkSeeder.php +250 -0
  153. package/server/seeders/Testing/OrdersSeeder.php +198 -0
  154. package/server/seeders/Testing/TestingSeeder.php +40 -0
  155. package/server/seeders/{VroomOrchestrationSeeder.php → Testing/VroomOrchestrationSeeder.php} +29 -22
  156. package/server/src/Console/Commands/AuditCustomerUserConflicts.php +75 -0
  157. package/server/src/Console/Commands/ProcessOperationalAlerts.php +279 -0
  158. package/server/src/Exceptions/CustomerUserConflictException.php +13 -0
  159. package/server/src/Http/Controllers/Api/v1/ContactController.php +17 -2
  160. package/server/src/Http/Controllers/Api/v1/CustomerController.php +876 -0
  161. package/server/src/Http/Controllers/Api/v1/DriverController.php +1 -1
  162. package/server/src/Http/Controllers/Api/v1/OrderConfigController.php +54 -0
  163. package/server/src/Http/Controllers/Api/v1/OrderController.php +98 -70
  164. package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -2
  165. package/server/src/Http/Controllers/Api/v1/VehicleController.php +20 -3
  166. package/server/src/Http/Controllers/Internal/v1/AnalyticsController.php +119 -0
  167. package/server/src/Http/Controllers/Internal/v1/ContactController.php +19 -0
  168. package/server/src/Http/Controllers/Internal/v1/LiveController.php +101 -66
  169. package/server/src/Http/Controllers/Internal/v1/MetricsController.php +82 -3
  170. package/server/src/Http/Controllers/Internal/v1/OrderController.php +441 -32
  171. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
  172. package/server/src/Http/Controllers/Internal/v1/SettingController.php +46 -1
  173. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +54 -1
  174. package/server/src/Http/Middleware/AuthenticateCustomerToken.php +34 -0
  175. package/server/src/Http/Requests/CreateCustomerOrderRequest.php +63 -0
  176. package/server/src/Http/Requests/CreateCustomerRequest.php +48 -0
  177. package/server/src/Http/Requests/CreateDriverRequest.php +1 -1
  178. package/server/src/Http/Requests/CreateVehicleRequest.php +25 -1
  179. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +1 -1
  180. package/server/src/Http/Requests/VerifyCreateCustomerRequest.php +29 -0
  181. package/server/src/Http/Resources/Internal/v1/OrderConfig.php +41 -0
  182. package/server/src/Http/Resources/v1/Customer.php +79 -0
  183. package/server/src/Http/Resources/v1/Driver.php +2 -0
  184. package/server/src/Http/Resources/v1/Fleet.php +4 -0
  185. package/server/src/Http/Resources/v1/Index/Driver.php +48 -0
  186. package/server/src/Http/Resources/v1/Index/Vehicle.php +74 -1
  187. package/server/src/Http/Resources/v1/OrderConfig.php +74 -0
  188. package/server/src/Http/Resources/v1/Payload.php +23 -20
  189. package/server/src/Http/Resources/v1/ServiceArea.php +33 -19
  190. package/server/src/Http/Resources/v1/ServiceRateFee.php +7 -0
  191. package/server/src/Listeners/HandleOrderDispatched.php +2 -2
  192. package/server/src/Models/Contact.php +92 -15
  193. package/server/src/Models/Customer.php +64 -0
  194. package/server/src/Models/Driver.php +14 -3
  195. package/server/src/Models/Entity.php +2 -2
  196. package/server/src/Models/Order.php +29 -4
  197. package/server/src/Models/OrderConfig.php +14 -11
  198. package/server/src/Models/Payload.php +17 -1
  199. package/server/src/Models/ServiceRate.php +273 -1
  200. package/server/src/Models/ServiceRateFee.php +39 -9
  201. package/server/src/Models/TrackingNumber.php +8 -2
  202. package/server/src/Models/Vehicle.php +18 -2
  203. package/server/src/Models/Waypoint.php +2 -2
  204. package/server/src/Notifications/LateDeparture.php +48 -0
  205. package/server/src/Notifications/ProlongedStoppage.php +48 -0
  206. package/server/src/Notifications/RouteDeviation.php +48 -0
  207. package/server/src/Observers/ContactObserver.php +2 -0
  208. package/server/src/Observers/ServiceRateObserver.php +6 -6
  209. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +29 -2
  210. package/server/src/Providers/FleetOpsServiceProvider.php +6 -0
  211. package/server/src/Providers/NotificationServiceProvider.php +3 -0
  212. package/server/src/Support/Analytics/AbstractAnalytics.php +77 -0
  213. package/server/src/Support/Analytics/FuelEfficiency.php +83 -0
  214. package/server/src/Support/Analytics/GeofenceViolations.php +63 -0
  215. package/server/src/Support/Analytics/IssuesInsights.php +67 -0
  216. package/server/src/Support/Analytics/LiveFleet.php +127 -0
  217. package/server/src/Support/Analytics/MaintenanceOverview.php +70 -0
  218. package/server/src/Support/Analytics/OnTimeDelivery.php +75 -0
  219. package/server/src/Support/Analytics/OperationsPulse.php +86 -0
  220. package/server/src/Support/Analytics/OrdersByStatus.php +64 -0
  221. package/server/src/Support/Analytics/RevenueTrend.php +78 -0
  222. package/server/src/Support/Analytics/TopDrivers.php +92 -0
  223. package/server/src/Support/CustomerAuth.php +98 -0
  224. package/server/src/Support/LabelPdf.php +52 -0
  225. package/server/src/Support/Metrics/AbstractMetric.php +170 -0
  226. package/server/src/Support/Metrics/ActiveLiveOrdersMetric.php +28 -0
  227. package/server/src/Support/Metrics/AvgOrderValueMetric.php +53 -0
  228. package/server/src/Support/Metrics/DriversOnlineMetric.php +30 -0
  229. package/server/src/Support/Metrics/EarningsMetric.php +30 -0
  230. package/server/src/Support/Metrics/FuelCostsMetric.php +30 -0
  231. package/server/src/Support/Metrics/MoneyMetric.php +30 -0
  232. package/server/src/Support/Metrics/OpenIssuesMetric.php +35 -0
  233. package/server/src/Support/Metrics/OrdersCanceledMetric.php +35 -0
  234. package/server/src/Support/Metrics/OrdersCompletedMetric.php +35 -0
  235. package/server/src/Support/Metrics/OrdersInProgressMetric.php +51 -0
  236. package/server/src/Support/Metrics/OrdersScheduledMetric.php +37 -0
  237. package/server/src/Support/Metrics/Registry.php +46 -0
  238. package/server/src/Support/Metrics/ResolvedIssuesMetric.php +35 -0
  239. package/server/src/Support/Metrics/TotalCustomersMetric.php +29 -0
  240. package/server/src/Support/Metrics/TotalDistanceTraveledMetric.php +35 -0
  241. package/server/src/Support/Metrics/TotalDriversMetric.php +28 -0
  242. package/server/src/Support/Metrics/TotalTimeTraveledMetric.php +35 -0
  243. package/server/src/Support/Metrics.php +46 -249
  244. package/server/src/Support/ResolvesOrderServiceStops.php +512 -0
  245. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +290 -0
  246. package/server/src/Support/Telematics/Providers/SafeeProvider.php +333 -0
  247. package/server/src/Tracking/TrackingContextBuilder.php +41 -38
  248. package/server/src/Tracking/TrackingIntelligenceService.php +109 -9
  249. package/server/src/Tracking/TrackingStop.php +13 -11
  250. package/server/src/routes.php +50 -0
  251. package/server/tests/AnalyticsRoutesTest.php +41 -0
  252. package/server/tests/CapacityAllocationEngineTest.php +10 -10
  253. package/server/tests/CustomerContactIdentitySafetyTest.php +54 -0
  254. package/server/tests/CustomerEndpointTest.php +199 -0
  255. package/server/tests/DriverVehicleStatusDefaultTest.php +39 -0
  256. package/server/tests/LabelPdfRenderingTest.php +23 -0
  257. package/server/tests/LiveControllerViewportTest.php +60 -0
  258. package/server/tests/MetricsRegistryTest.php +84 -0
  259. package/server/tests/OrchestrationPayloadBuilderTest.php +2 -2
  260. package/server/tests/OrderConfigEndpointTest.php +86 -0
  261. package/server/tests/OrderFileAttachmentTest.php +20 -0
  262. package/server/tests/ServiceAreaZoneApiShapeTest.php +34 -0
  263. package/server/tests/VehicleDriverAssignmentTest.php +18 -0
  264. package/server/tests/VroomOrchestrationEngineTest.php +91 -0
  265. package/translations/en-us.yaml +38 -0
  266. package/server/migrations/2026_05_13_000001_repair_customer_contact_user_types.php +0 -86
@@ -0,0 +1,250 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Seeders\Testing;
4
+
5
+ use Fleetbase\FleetOps\Models\Contact;
6
+ use Fleetbase\FleetOps\Models\Place;
7
+ use Fleetbase\FleetOps\Models\ServiceArea;
8
+ use Fleetbase\FleetOps\Models\ServiceRate;
9
+ use Fleetbase\FleetOps\Models\ServiceRateFee;
10
+ use Fleetbase\FleetOps\Models\Vendor;
11
+ use Fleetbase\FleetOps\Models\Zone;
12
+ use Fleetbase\FleetOps\Seeders\Testing\Concerns\SeedsTestingData;
13
+ use Fleetbase\FleetOps\Support\FleetOps;
14
+ use Fleetbase\Models\Company;
15
+ use Illuminate\Database\Seeder;
16
+ use Illuminate\Support\Facades\DB;
17
+
18
+ class NetworkSeeder extends Seeder
19
+ {
20
+ use SeedsTestingData;
21
+
22
+ public function run(): void
23
+ {
24
+ $company = $this->prepareCompany();
25
+ if (!$company) {
26
+ return;
27
+ }
28
+
29
+ DB::transaction(function () use ($company) {
30
+ $this->purgeSeedData();
31
+ FleetOps::createTransportConfig($company);
32
+ $this->seedServiceAreas($company);
33
+ $this->seedZones($company);
34
+ $places = $this->seedPlaces($company);
35
+ $this->seedContacts($company, $places);
36
+ $vendors = $this->seedVendors($company, $places);
37
+ $this->seedServiceRates($company, $vendors);
38
+
39
+ $this->command?->info('Seeded FleetOps testing network fixtures for company: ' . $company->public_id);
40
+ });
41
+ }
42
+
43
+ public function purgeSeedData(): void
44
+ {
45
+ $this->purgeModel(ServiceRateFee::class);
46
+ $this->purgeModel(ServiceRate::class);
47
+ $this->purgeModel(Vendor::class);
48
+ $this->purgeModel(Contact::class);
49
+ $this->purgeModel(Place::class);
50
+ $this->purgeModel(Zone::class);
51
+ $this->purgeModel(ServiceArea::class);
52
+ }
53
+
54
+ protected function seedServiceAreas(Company $company): void
55
+ {
56
+ $areas = [
57
+ 'central_service_area' => [
58
+ 'name' => 'Singapore Central',
59
+ 'border' => [[1.2700, 103.8000], [1.2700, 103.9100], [1.3600, 103.9100], [1.3600, 103.8000]],
60
+ 'color' => '#2563eb',
61
+ ],
62
+ 'east_service_area' => [
63
+ 'name' => 'Singapore East',
64
+ 'border' => [[1.3050, 103.9000], [1.3050, 104.0200], [1.3950, 104.0200], [1.3950, 103.9000]],
65
+ 'color' => '#059669',
66
+ ],
67
+ ];
68
+
69
+ foreach ($areas as $seedId => $area) {
70
+ $this->createRecord(ServiceArea::class, [
71
+ '_key' => $this->fixtureKey($seedId),
72
+ 'company_uuid' => $company->uuid,
73
+ 'name' => $area['name'],
74
+ 'type' => 'city',
75
+ 'country' => 'SG',
76
+ 'border' => $this->multiPolygon($area['border']),
77
+ 'color' => $area['color'],
78
+ 'stroke_color' => $area['color'],
79
+ 'status' => 'active',
80
+ 'trigger_on_entry' => true,
81
+ 'trigger_on_exit' => true,
82
+ 'dwell_threshold_minutes' => 15,
83
+ 'speed_limit_kmh' => 50,
84
+ ]);
85
+ }
86
+ }
87
+
88
+ protected function seedZones(Company $company): void
89
+ {
90
+ $zones = [
91
+ 'cbd_zone' => [
92
+ 'service_area' => 'central_service_area',
93
+ 'name' => 'Marina Bay',
94
+ 'border' => [[1.2760, 103.8350], [1.2760, 103.8650], [1.3050, 103.8650], [1.3050, 103.8350]],
95
+ 'color' => '#7c3aed',
96
+ ],
97
+ 'airport_zone' => [
98
+ 'service_area' => 'east_service_area',
99
+ 'name' => 'Changi',
100
+ 'border' => [[1.3350, 103.9600], [1.3350, 104.0100], [1.3850, 104.0100], [1.3850, 103.9600]],
101
+ 'color' => '#dc2626',
102
+ ],
103
+ 'warehouse_zone' => [
104
+ 'service_area' => 'central_service_area',
105
+ 'name' => 'Jurong',
106
+ 'border' => [[1.3000, 103.7800], [1.3000, 103.8300], [1.3450, 103.8300], [1.3450, 103.7800]],
107
+ 'color' => '#ea580c',
108
+ ],
109
+ ];
110
+
111
+ foreach ($zones as $seedId => $zone) {
112
+ $serviceArea = $this->seededServiceArea($zone['service_area']);
113
+ $this->createRecord(Zone::class, [
114
+ '_key' => $this->fixtureKey($seedId),
115
+ 'company_uuid' => $company->uuid,
116
+ 'service_area_uuid' => $serviceArea?->uuid,
117
+ 'name' => $zone['name'],
118
+ 'description' => 'Fixture zone for FleetOps testing assertions.',
119
+ 'border' => $this->polygon($zone['border']),
120
+ 'color' => $zone['color'],
121
+ 'stroke_color' => $zone['color'],
122
+ 'status' => 'active',
123
+ 'trigger_on_entry' => true,
124
+ 'trigger_on_exit' => true,
125
+ 'dwell_threshold_minutes' => 10,
126
+ 'speed_limit_kmh' => 40,
127
+ ]);
128
+ }
129
+ }
130
+
131
+ protected function seedPlaces(Company $company): array
132
+ {
133
+ $places = [
134
+ 'central_depot' => ['Raffles Quay', '1 Raffles Quay', 'Singapore', 'SG', 1.2816, 103.8510],
135
+ 'west_depot' => ['Jurong East', '2 Jurong East Street 21', 'Singapore', 'SG', 1.3345, 103.7420],
136
+ 'airport_hub' => ['Changi Airfreight', '80 Airport Boulevard', 'Singapore', 'SG', 1.3644, 103.9915],
137
+ 'orchard_store' => ['ION Orchard', '2 Orchard Turn', 'Singapore', 'SG', 1.3048, 103.8318],
138
+ 'rochor_store' => ['Rochor Canal', '1 Rochor Canal Road', 'Singapore', 'SG', 1.3039, 103.8520],
139
+ 'tampines_store'=> ['Tampines Mall', '4 Tampines Central 5', 'Singapore', 'SG', 1.3525, 103.9447],
140
+ ];
141
+
142
+ $models = [];
143
+ foreach ($places as $seedId => [$name, $street1, $city, $country, $lat, $lng]) {
144
+ $models[$seedId] = $this->createRecord(Place::class, [
145
+ '_key' => $this->fixtureKey($seedId),
146
+ 'company_uuid' => $company->uuid,
147
+ 'name' => $name,
148
+ 'street1' => $street1,
149
+ 'city' => $city,
150
+ 'country' => $country,
151
+ 'location' => $this->point($lat, $lng),
152
+ 'meta' => $this->meta($seedId),
153
+ ]);
154
+ }
155
+
156
+ return $models;
157
+ }
158
+
159
+ protected function seedContacts(Company $company, array $places): void
160
+ {
161
+ $contacts = [
162
+ 'customer_alice' => ['Alice Tan', 'customer', 'alice.testing@example.test', '+6591000001', 'orchard_store'],
163
+ 'customer_ben' => ['Ben Lim', 'customer', 'ben.testing@example.test', '+6591000002', 'rochor_store'],
164
+ 'ops_manager' => ['Nadia Rahman', 'contact', 'nadia.testing@example.test', '+6591000003', 'central_depot'],
165
+ ];
166
+
167
+ foreach ($contacts as $seedId => [$name, $type, $email, $phone, $placeSeedId]) {
168
+ $this->createRecord(Contact::class, [
169
+ '_key' => $this->fixtureKey($seedId),
170
+ 'company_uuid' => $company->uuid,
171
+ 'place_uuid' => $places[$placeSeedId]?->uuid,
172
+ 'name' => $name,
173
+ 'title' => $type === 'customer' ? 'Receiving Contact' : 'Operations Manager',
174
+ 'email' => $email,
175
+ 'phone' => $phone,
176
+ 'type' => $type,
177
+ 'notes' => 'FleetOps testing fixture contact.',
178
+ 'meta' => $this->meta($seedId),
179
+ ]);
180
+ }
181
+ }
182
+
183
+ protected function seedVendors(Company $company, array $places): array
184
+ {
185
+ $vendors = [
186
+ 'facilitator_fastline' => ['Fastline Logistics', 'facilitator', 'ops@fastline.example.test', '+6592000001', 'central_depot'],
187
+ 'supplier_parts' => ['Apex Parts Supply', 'supplier', 'parts@example.test', '+6592000002', 'west_depot'],
188
+ ];
189
+
190
+ $models = [];
191
+ foreach ($vendors as $seedId => [$name, $type, $email, $phone, $placeSeedId]) {
192
+ $models[$seedId] = $this->createRecord(Vendor::class, [
193
+ '_key' => $this->fixtureKey($seedId),
194
+ 'company_uuid' => $company->uuid,
195
+ 'place_uuid' => $places[$placeSeedId]?->uuid,
196
+ 'name' => $name,
197
+ 'email' => $email,
198
+ 'phone' => $phone,
199
+ 'country' => 'SG',
200
+ 'status' => 'active',
201
+ 'type' => $type,
202
+ 'notes' => 'FleetOps testing fixture vendor.',
203
+ 'meta' => $this->meta($seedId),
204
+ ]);
205
+ }
206
+
207
+ return $models;
208
+ }
209
+
210
+ protected function seedServiceRates(Company $company, array $vendors): void
211
+ {
212
+ $orderConfig = FleetOps::createTransportConfig($company);
213
+ $rates = [
214
+ 'same_day_cbd' => ['Same Day CBD', 'same_day', 'central_service_area', 'cbd_zone', 1200, 120, 1],
215
+ 'airport_express' => ['Airport Express', 'express', 'east_service_area', 'airport_zone', 2800, 180, 0],
216
+ ];
217
+
218
+ foreach ($rates as $seedId => [$name, $type, $areaSeedId, $zoneSeedId, $baseFee, $perMeter, $estimatedDays]) {
219
+ $serviceRate = $this->createRecord(ServiceRate::class, [
220
+ '_key' => $this->fixtureKey($seedId),
221
+ 'company_uuid' => $company->uuid,
222
+ 'service_area_uuid' => $this->seededServiceArea($areaSeedId)?->uuid,
223
+ 'zone_uuid' => $this->seededZone($zoneSeedId)?->uuid,
224
+ 'order_config_uuid' => $orderConfig->uuid,
225
+ 'service_name' => $name,
226
+ 'service_type' => $type,
227
+ 'base_fee' => $baseFee,
228
+ 'per_meter_flat_rate_fee' => $perMeter,
229
+ 'per_meter_unit' => 'm',
230
+ 'algorithm' => 'fixed_meter',
231
+ 'rate_calculation_method' => 'fixed_meter',
232
+ 'currency' => 'SGD',
233
+ 'duration_terms' => 'days',
234
+ 'estimated_days' => $estimatedDays,
235
+ ]);
236
+
237
+ foreach ([['parcel', 1, 5, 400], ['parcel', 6, 20, 900]] as $index => [$unit, $min, $max, $fee]) {
238
+ $this->createRecord(ServiceRateFee::class, [
239
+ '_key' => $this->fixtureKey($seedId . '_fee_' . ($index + 1)),
240
+ 'service_rate_uuid' => $serviceRate->uuid,
241
+ 'min' => $min,
242
+ 'max' => $max,
243
+ 'unit' => $unit,
244
+ 'fee' => $fee,
245
+ 'currency' => 'SGD',
246
+ ]);
247
+ }
248
+ }
249
+ }
250
+ }
@@ -0,0 +1,198 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Seeders\Testing;
4
+
5
+ use Fleetbase\FleetOps\Models\Contact;
6
+ use Fleetbase\FleetOps\Models\Driver;
7
+ use Fleetbase\FleetOps\Models\Entity;
8
+ use Fleetbase\FleetOps\Models\Order;
9
+ use Fleetbase\FleetOps\Models\Payload;
10
+ use Fleetbase\FleetOps\Models\Place;
11
+ use Fleetbase\FleetOps\Models\TrackingNumber;
12
+ use Fleetbase\FleetOps\Models\TrackingStatus;
13
+ use Fleetbase\FleetOps\Models\Vehicle;
14
+ use Fleetbase\FleetOps\Models\Vendor;
15
+ use Fleetbase\FleetOps\Models\Waypoint;
16
+ use Fleetbase\FleetOps\Seeders\Testing\Concerns\SeedsTestingData;
17
+ use Fleetbase\FleetOps\Support\FleetOps;
18
+ use Fleetbase\Models\Company;
19
+ use Fleetbase\Models\User;
20
+ use Illuminate\Database\Seeder;
21
+ use Illuminate\Support\Facades\DB;
22
+
23
+ class OrdersSeeder extends Seeder
24
+ {
25
+ use SeedsTestingData;
26
+
27
+ public function run(): void
28
+ {
29
+ $company = $this->prepareCompany();
30
+ if (!$company) {
31
+ return;
32
+ }
33
+
34
+ DB::transaction(function () use ($company) {
35
+ $this->purgeSeedData();
36
+ $this->seedOrders($company);
37
+
38
+ $this->command?->info('Seeded FleetOps testing order fixtures for company: ' . $company->public_id);
39
+ });
40
+ }
41
+
42
+ public function purgeSeedData(): void
43
+ {
44
+ $this->purgeModel(TrackingStatus::class);
45
+ $this->purgeModel(Order::class);
46
+ $this->purgeModel(Entity::class);
47
+ $this->purgeModel(Waypoint::class);
48
+ $this->purgeModel(TrackingNumber::class);
49
+ $this->purgeModel(Payload::class);
50
+ }
51
+
52
+ protected function seedOrders(Company $company): void
53
+ {
54
+ $places = [
55
+ 'central_depot' => $this->seededModel(Place::class, 'central_depot'),
56
+ 'west_depot' => $this->seededModel(Place::class, 'west_depot'),
57
+ 'airport_hub' => $this->seededModel(Place::class, 'airport_hub'),
58
+ 'orchard_store' => $this->seededModel(Place::class, 'orchard_store'),
59
+ 'rochor_store' => $this->seededModel(Place::class, 'rochor_store'),
60
+ 'tampines_store' => $this->seededModel(Place::class, 'tampines_store'),
61
+ ];
62
+ $customers = [
63
+ 'customer_alice' => $this->seededModel(Contact::class, 'customer_alice'),
64
+ 'customer_ben' => $this->seededModel(Contact::class, 'customer_ben'),
65
+ ];
66
+ $drivers = [
67
+ 'driver_ava' => $this->seededModel(Driver::class, 'driver_ava'),
68
+ 'driver_ken' => $this->seededModel(Driver::class, 'driver_ken'),
69
+ ];
70
+ $vehicles = [
71
+ 'van_central' => $this->seededModel(Vehicle::class, 'van_central'),
72
+ 'van_east' => $this->seededModel(Vehicle::class, 'van_east'),
73
+ ];
74
+ $facilitator = $this->seededModel(Vendor::class, 'facilitator_fastline');
75
+ $dispatcher = $this->seededModel(User::class, 'dispatcher_user');
76
+ $orderConfig = FleetOps::createTransportConfig($company);
77
+
78
+ $orders = [
79
+ 'order_created_unassigned' => ['created', null, null, 'customer_alice', 'central_depot', 'orchard_store', [], 2, 1, null, false, false],
80
+ 'order_scheduled' => ['created', null, null, 'customer_ben', 'west_depot', 'rochor_store', [], 4, 1, $this->timestamp(30), false, false],
81
+ 'order_dispatched' => ['dispatched', 'driver_ava', 'van_central', 'customer_alice', 'central_depot', 'orchard_store', ['rochor_store'], 6, 2, null, true, false],
82
+ 'order_started' => ['started', 'driver_ken', 'van_east', 'customer_ben', 'airport_hub', 'tampines_store', [], 8, 1, null, true, true],
83
+ 'order_completed' => ['completed', 'driver_ava', 'van_central', 'customer_alice', 'central_depot', 'orchard_store', [], -4, 1, null, true, true],
84
+ 'order_canceled' => ['canceled', null, null, 'customer_ben', 'central_depot', 'rochor_store', [], -2, 1, null, false, false],
85
+ 'order_vehicle_assigned' => ['created', null, 'van_central', 'customer_alice', 'west_depot', 'airport_hub', ['orchard_store', 'rochor_store'], 10, 3, null, false, false],
86
+ ];
87
+
88
+ foreach ($orders as $seedId => [$status, $driverSeedId, $vehicleSeedId, $customerSeedId, $pickupSeedId, $dropoffSeedId, $waypointSeedIds, $hourOffset, $entityCount, $scheduledAt, $dispatched, $started]) {
89
+ $payload = $this->createPayload($company, $seedId, $places[$pickupSeedId], $places[$dropoffSeedId], array_map(fn ($placeSeedId) => $places[$placeSeedId], $waypointSeedIds));
90
+ $order = $this->createRecord(Order::class, [
91
+ '_key' => $this->fixtureKey($seedId),
92
+ 'internal_id' => 'TEST-' . strtoupper(str_replace('order_', '', $seedId)),
93
+ 'company_uuid' => $company->uuid,
94
+ 'payload_uuid' => $payload->uuid,
95
+ 'order_config_uuid' => $orderConfig->uuid,
96
+ 'customer_uuid' => $customers[$customerSeedId]?->uuid,
97
+ 'customer_type' => Contact::class,
98
+ 'facilitator_uuid' => $facilitator?->uuid,
99
+ 'facilitator_type' => Vendor::class,
100
+ 'driver_assigned_uuid' => $driverSeedId ? $drivers[$driverSeedId]?->uuid : null,
101
+ 'vehicle_assigned_uuid' => $vehicleSeedId ? $vehicles[$vehicleSeedId]?->uuid : null,
102
+ 'created_by_uuid' => $dispatcher?->uuid,
103
+ 'updated_by_uuid' => $dispatcher?->uuid,
104
+ 'scheduled_at' => $scheduledAt,
105
+ 'dispatched_at' => $dispatched ? $this->timestamp($hourOffset) : null,
106
+ 'dispatched' => $dispatched,
107
+ 'started' => $started,
108
+ 'started_at' => $started ? $this->timestamp($hourOffset + 1) : null,
109
+ 'type' => 'transport',
110
+ 'status' => $status,
111
+ 'notes' => 'FleetOps testing fixture order: ' . $seedId,
112
+ 'pod_method' => 'scan',
113
+ 'pod_required' => $status === 'completed',
114
+ 'orchestrator_priority' => 50,
115
+ 'required_skills' => $vehicleSeedId === 'van_east' ? ['cold_chain'] : [],
116
+ 'meta' => $this->meta($seedId),
117
+ 'created_at' => $this->timestamp($hourOffset),
118
+ 'updated_at' => $this->timestamp($hourOffset + 1),
119
+ ]);
120
+
121
+ $trackingNumber = $this->createTrackingNumber($company, $order, $seedId);
122
+ $order->forceFill(['tracking_number_uuid' => $trackingNumber->uuid])->save();
123
+ $this->seedEntities($company, $payload, $trackingNumber, $seedId, $entityCount, $places[$dropoffSeedId]);
124
+ }
125
+ }
126
+
127
+ protected function createPayload(Company $company, string $seedId, ?Place $pickup, ?Place $dropoff, array $waypoints): Payload
128
+ {
129
+ /** @var Payload $payload */
130
+ $payload = $this->createRecord(Payload::class, [
131
+ '_key' => $this->fixtureKey($seedId . '_payload'),
132
+ 'company_uuid' => $company->uuid,
133
+ 'pickup_uuid' => $pickup?->uuid,
134
+ 'dropoff_uuid' => $dropoff?->uuid,
135
+ 'type' => 'transport',
136
+ 'meta' => $this->meta($seedId . '_payload'),
137
+ ]);
138
+
139
+ foreach ($waypoints as $index => $place) {
140
+ $this->createRecord(Waypoint::class, [
141
+ '_key' => $this->fixtureKey($seedId . '_waypoint_' . ($index + 1)),
142
+ '_import_id' => $this->fixtureKey($seedId . ':waypoint:' . ($index + 1)),
143
+ 'company_uuid' => $company->uuid,
144
+ 'payload_uuid' => $payload->uuid,
145
+ 'place_uuid' => $place?->uuid,
146
+ 'type' => 'dropoff',
147
+ 'order' => $index + 1,
148
+ 'service_time' => 300,
149
+ 'notes' => 'FleetOps testing waypoint.',
150
+ ]);
151
+ }
152
+
153
+ return $payload;
154
+ }
155
+
156
+ protected function createTrackingNumber(Company $company, Order $order, string $seedId): TrackingNumber
157
+ {
158
+ /** @var TrackingNumber $trackingNumber */
159
+ $trackingNumber = $this->createRecord(TrackingNumber::class, [
160
+ '_key' => $this->fixtureKey($seedId . '_tracking'),
161
+ 'company_uuid' => $company->uuid,
162
+ 'owner_uuid' => $order->uuid,
163
+ 'owner_type' => Order::class,
164
+ 'tracking_number' => 'TEST-' . str_pad((string) crc32($seedId), 10, '0', STR_PAD_LEFT),
165
+ 'region' => 'SG',
166
+ ]);
167
+
168
+ return $trackingNumber;
169
+ }
170
+
171
+ protected function seedEntities(Company $company, Payload $payload, TrackingNumber $trackingNumber, string $seedId, int $entityCount, ?Place $destination): void
172
+ {
173
+ for ($i = 1; $i <= $entityCount; $i++) {
174
+ $this->createRecord(Entity::class, [
175
+ '_key' => $this->fixtureKey($seedId . '_entity_' . $i),
176
+ '_import_id' => $this->fixtureKey($seedId . ':entity:' . $i),
177
+ 'public_id' => Entity::generatePublicId(),
178
+ 'company_uuid' => $company->uuid,
179
+ 'payload_uuid' => $payload->uuid,
180
+ 'tracking_number_uuid' => $trackingNumber->uuid,
181
+ 'destination_uuid' => $destination?->uuid,
182
+ 'internal_id' => 'TEST-ENTITY-' . strtoupper($seedId) . '-' . $i,
183
+ 'name' => 'Item ' . $i,
184
+ 'type' => 'parcel',
185
+ 'description' => 'FleetOps testing fixture entity.',
186
+ 'weight' => 5 + $i,
187
+ 'weight_unit' => 'kg',
188
+ 'length' => 40,
189
+ 'width' => 30,
190
+ 'height' => 20,
191
+ 'dimensions_unit' => 'cm',
192
+ 'currency' => 'SGD',
193
+ 'declared_value' => 1000 + ($i * 100),
194
+ 'meta' => $this->meta($seedId . '_entity_' . $i),
195
+ ], true);
196
+ }
197
+ }
198
+ }
@@ -0,0 +1,40 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Seeders\Testing;
4
+
5
+ use Illuminate\Database\Seeder;
6
+ use Illuminate\Support\Facades\Schema;
7
+
8
+ class TestingSeeder extends Seeder
9
+ {
10
+ public function run(): void
11
+ {
12
+ Schema::disableForeignKeyConstraints();
13
+ try {
14
+ $this->purgeSeedData();
15
+ } finally {
16
+ Schema::enableForeignKeyConstraints();
17
+ }
18
+
19
+ $this->call([
20
+ NetworkSeeder::class,
21
+ FleetSeeder::class,
22
+ OrdersSeeder::class,
23
+ ConnectivitySeeder::class,
24
+ MaintenanceSeeder::class,
25
+ ]);
26
+ }
27
+
28
+ protected function purgeSeedData(): void
29
+ {
30
+ foreach ([
31
+ new MaintenanceSeeder(),
32
+ new ConnectivitySeeder(),
33
+ new OrdersSeeder(),
34
+ new FleetSeeder(),
35
+ new NetworkSeeder(),
36
+ ] as $seeder) {
37
+ $seeder->purgeSeedData();
38
+ }
39
+ }
40
+ }
@@ -1,6 +1,6 @@
1
1
  <?php
2
2
 
3
- namespace Fleetbase\FleetOps\Seeders;
3
+ namespace Fleetbase\FleetOps\Seeders\Testing;
4
4
 
5
5
  use Fleetbase\FleetOps\Models\Entity;
6
6
  use Fleetbase\FleetOps\Models\Order;
@@ -15,6 +15,13 @@ use Illuminate\Database\Seeder;
15
15
  use Illuminate\Support\Facades\DB;
16
16
  use Illuminate\Support\Str;
17
17
 
18
+ /**
19
+ * Explicit test-data seeder. Kept below server/seeders/Testing so fleetbase:seed
20
+ * does not auto-discover it during deploys.
21
+ *
22
+ * Run with:
23
+ * php artisan db:seed --class="Fleetbase\\FleetOps\\Seeders\\Testing\\VroomOrchestrationSeeder"
24
+ */
18
25
  class VroomOrchestrationSeeder extends Seeder
19
26
  {
20
27
  use ResolvesSeedCompany;
@@ -250,7 +257,7 @@ class VroomOrchestrationSeeder extends Seeder
250
257
  'model' => $vehicle['model'],
251
258
  'year' => '2024',
252
259
  'type' => 'van',
253
- 'status' => 'active',
260
+ 'status' => 'available',
254
261
  'online' => true,
255
262
  'location' => isset($vehicle['lat'], $vehicle['lng']) ? new Point($vehicle['lat'], $vehicle['lng']) : null,
256
263
  'skills' => $vehicle['skills'] ?? [],
@@ -280,22 +287,22 @@ class VroomOrchestrationSeeder extends Seeder
280
287
  $orders = [
281
288
  'endpoint_only' => [
282
289
  'payload_seed_id' => 'endpoint_only',
283
- 'name' => 'VROOM Test Order - Pickup Dropoff',
284
- 'pickup' => $places['depot'],
285
- 'dropoff' => $places['orchard'],
286
- 'waypoints' => [],
287
- 'entities' => [
290
+ 'name' => 'VROOM Test Order - Pickup Dropoff',
291
+ 'pickup' => $places['depot'],
292
+ 'dropoff' => $places['orchard'],
293
+ 'waypoints' => [],
294
+ 'entities' => [
288
295
  ['name' => 'Small carton', 'type' => 'parcel', 'weight' => 12, 'weight_unit' => 'kg', 'length' => 60, 'width' => 40, 'height' => 35, 'dimensions_unit' => 'cm'],
289
296
  ['name' => 'Fragile tote', 'type' => 'parcel', 'weight' => 8, 'weight_unit' => 'kg', 'length' => 50, 'width' => 35, 'height' => 30, 'dimensions_unit' => 'cm'],
290
297
  ],
291
298
  ],
292
299
  'waypoint_only' => [
293
300
  'payload_seed_id' => 'waypoint_only',
294
- 'name' => 'VROOM Test Order - Waypoints Only',
295
- 'pickup' => null,
296
- 'dropoff' => null,
297
- 'waypoints' => [$places['rochor'], $places['novena'], $places['serangoon']],
298
- 'entities' => [
301
+ 'name' => 'VROOM Test Order - Waypoints Only',
302
+ 'pickup' => null,
303
+ 'dropoff' => null,
304
+ 'waypoints' => [$places['rochor'], $places['novena'], $places['serangoon']],
305
+ 'entities' => [
299
306
  ['name' => 'Multi-stop parcel 1', 'type' => 'parcel', 'weight' => 25, 'weight_unit' => 'kg', 'length' => 80, 'width' => 50, 'height' => 40, 'dimensions_unit' => 'cm'],
300
307
  ['name' => 'Multi-stop parcel 2', 'type' => 'parcel', 'weight' => 18, 'weight_unit' => 'kg', 'length' => 70, 'width' => 45, 'height' => 35, 'dimensions_unit' => 'cm'],
301
308
  ['name' => 'Multi-stop parcel 3', 'type' => 'parcel', 'weight' => 15, 'weight_unit' => 'kg', 'length' => 65, 'width' => 40, 'height' => 35, 'dimensions_unit' => 'cm'],
@@ -303,22 +310,22 @@ class VroomOrchestrationSeeder extends Seeder
303
310
  ],
304
311
  'mixed' => [
305
312
  'payload_seed_id' => 'mixed',
306
- 'name' => 'VROOM Test Order - Mixed Stops',
307
- 'pickup' => $places['depot'],
308
- 'dropoff' => $places['changi'],
309
- 'waypoints' => [$places['paya_lebar'], $places['tampines']],
310
- 'entities' => [
313
+ 'name' => 'VROOM Test Order - Mixed Stops',
314
+ 'pickup' => $places['depot'],
315
+ 'dropoff' => $places['changi'],
316
+ 'waypoints' => [$places['paya_lebar'], $places['tampines']],
317
+ 'entities' => [
311
318
  ['name' => 'Heavy equipment crate', 'type' => 'freight', 'weight' => 180, 'weight_unit' => 'kg', 'length' => 120, 'width' => 80, 'height' => 90, 'dimensions_unit' => 'cm'],
312
319
  ['name' => 'Accessory box', 'type' => 'parcel', 'weight' => 22, 'weight_unit' => 'kg', 'length' => 75, 'width' => 55, 'height' => 45, 'dimensions_unit' => 'cm'],
313
320
  ],
314
321
  ],
315
322
  'single_waypoint' => [
316
323
  'payload_seed_id' => 'single_waypoint',
317
- 'name' => 'VROOM Test Order - Single Waypoint',
318
- 'pickup' => null,
319
- 'dropoff' => null,
320
- 'waypoints' => [$places['woodlands']],
321
- 'entities' => [
324
+ 'name' => 'VROOM Test Order - Single Waypoint',
325
+ 'pickup' => null,
326
+ 'dropoff' => null,
327
+ 'waypoints' => [$places['woodlands']],
328
+ 'entities' => [
322
329
  ['name' => 'Oversized sample case', 'type' => 'parcel', 'weight' => 60, 'weight_unit' => 'kg', 'length' => 100, 'width' => 60, 'height' => 60, 'dimensions_unit' => 'cm'],
323
330
  ],
324
331
  ],
@@ -0,0 +1,75 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Console\Commands;
4
+
5
+ use Fleetbase\FleetOps\Models\Contact;
6
+ use Illuminate\Console\Command;
7
+
8
+ class AuditCustomerUserConflicts extends Command
9
+ {
10
+ protected $signature = 'fleetops:audit-customer-user-conflicts {--company= : Limit audit to a company UUID} {--json : Output JSON instead of a table}';
11
+
12
+ protected $description = 'Reports customer contacts linked to users that may have been incorrectly promoted from staff/admin accounts.';
13
+
14
+ public function handle(): int
15
+ {
16
+ $query = Contact::where('type', 'customer')->whereNotNull('user_uuid')->with(['anyUser.companyUsers.roles', 'company']);
17
+
18
+ if ($company = $this->option('company')) {
19
+ $query->where('company_uuid', $company);
20
+ }
21
+
22
+ $rows = $query->get()->map(function (Contact $contact) {
23
+ $user = $contact->anyUser;
24
+ $companyUser = $user?->companyUsers?->firstWhere('company_uuid', $contact->company_uuid);
25
+ $roles = $companyUser?->roles?->pluck('name')->filter()->values()->all() ?? [];
26
+ $unexpected = array_values(array_filter($roles, fn ($role) => $role !== 'Fleet-Ops Customer'));
27
+ $reasons = [];
28
+
29
+ if (!$user) {
30
+ $reasons[] = 'missing linked user';
31
+ } elseif ($user->type !== 'customer') {
32
+ $reasons[] = 'linked user type is ' . $user->type;
33
+ }
34
+
35
+ if (!empty($unexpected)) {
36
+ $reasons[] = 'linked user has non-customer roles: ' . implode(', ', $unexpected);
37
+ }
38
+
39
+ if (empty($reasons)) {
40
+ return null;
41
+ }
42
+
43
+ return [
44
+ 'contact_id' => $contact->public_id,
45
+ 'contact_uuid' => $contact->uuid,
46
+ 'contact_name' => $contact->name,
47
+ 'contact_email' => $contact->email,
48
+ 'user_uuid' => $user?->uuid,
49
+ 'user_email' => $user?->email,
50
+ 'user_type' => $user?->type,
51
+ 'roles' => implode(', ', $roles),
52
+ 'company_uuid' => $contact->company_uuid,
53
+ 'company_name' => $contact->company?->name,
54
+ 'updated_at' => optional($contact->updated_at)->toDateTimeString(),
55
+ 'reason' => implode('; ', $reasons),
56
+ ];
57
+ })->filter()->values();
58
+
59
+ if ($this->option('json')) {
60
+ $this->line($rows->toJson(JSON_PRETTY_PRINT));
61
+
62
+ return self::SUCCESS;
63
+ }
64
+
65
+ if ($rows->isEmpty()) {
66
+ $this->info('No suspicious customer user conflicts found.');
67
+
68
+ return self::SUCCESS;
69
+ }
70
+
71
+ $this->table(array_keys($rows->first()), $rows->all());
72
+
73
+ return self::SUCCESS;
74
+ }
75
+ }