@fleetbase/fleetops-engine 0.6.47 → 0.6.49

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 (223) hide show
  1. package/addon/components/admin/routing-settings.hbs +71 -0
  2. package/addon/components/admin/routing-settings.js +124 -0
  3. package/addon/components/customer/orders.js +1 -1
  4. package/addon/components/display-place.hbs +1 -1
  5. package/addon/components/driver/details.hbs +5 -1
  6. package/addon/components/driver/form.hbs +11 -1
  7. package/addon/components/fleet-listing-panel.hbs +2 -2
  8. package/addon/components/fleet-listing-panel.js +10 -0
  9. package/addon/components/map/google-live-map.js +12 -73
  10. package/addon/components/map/leaflet-live-map.hbs +149 -33
  11. package/addon/components/map/order-list-overlay/order.hbs +18 -3
  12. package/addon/components/map/toolbar.hbs +2 -2
  13. package/addon/components/map/toolbar.js +10 -2
  14. package/addon/components/orchestrator/phase-builder.hbs +49 -5
  15. package/addon/components/orchestrator/phase-builder.js +34 -1
  16. package/addon/components/orchestrator/resource-panel.hbs +19 -1
  17. package/addon/components/orchestrator/resource-panel.js +4 -0
  18. package/addon/components/orchestrator-workbench.js +4 -1
  19. package/addon/components/order/details/purchase-rate.hbs +19 -22
  20. package/addon/components/order/details/route.hbs +26 -1
  21. package/addon/components/order/details/route.js +50 -0
  22. package/addon/components/order/details/tracking.hbs +171 -27
  23. package/addon/components/order/details/tracking.js +457 -0
  24. package/addon/components/order/form.hbs +2 -2
  25. package/addon/components/order/kanban-card.hbs +18 -3
  26. package/addon/components/order-list-overlay/order.hbs +18 -3
  27. package/addon/components/order-progress-card.hbs +21 -6
  28. package/addon/components/order-tracking-lookup.hbs +7 -7
  29. package/addon/components/order-tracking-lookup.js +1 -1
  30. package/addon/components/route-list.hbs +78 -8
  31. package/addon/components/route-list.js +48 -6
  32. package/addon/components/tracking-stop-progress.hbs +45 -0
  33. package/addon/components/tracking-stop-progress.js +79 -0
  34. package/addon/components/widget/fleet-ops-key-metrics.hbs +19 -17
  35. package/addon/components/widget/fleet-ops-key-metrics.js +26 -0
  36. package/addon/components/widget/fuel-efficiency.hbs +26 -0
  37. package/addon/components/widget/fuel-efficiency.js +43 -0
  38. package/addon/components/widget/geofence-violations.hbs +44 -0
  39. package/addon/components/widget/geofence-violations.js +55 -0
  40. package/addon/components/widget/issues-insights.hbs +43 -0
  41. package/addon/components/widget/issues-insights.js +62 -0
  42. package/addon/components/widget/kpi-active-orders.hbs +10 -0
  43. package/addon/components/widget/kpi-active-orders.js +5 -0
  44. package/addon/components/widget/kpi-aov.hbs +10 -0
  45. package/addon/components/widget/kpi-aov.js +5 -0
  46. package/addon/components/widget/kpi-distance.hbs +10 -0
  47. package/addon/components/widget/kpi-distance.js +5 -0
  48. package/addon/components/widget/kpi-drivers-online.hbs +10 -0
  49. package/addon/components/widget/kpi-drivers-online.js +5 -0
  50. package/addon/components/widget/kpi-earnings.hbs +10 -0
  51. package/addon/components/widget/kpi-earnings.js +5 -0
  52. package/addon/components/widget/kpi-open-issues.hbs +11 -0
  53. package/addon/components/widget/kpi-open-issues.js +5 -0
  54. package/addon/components/widget/kpi-tile.hbs +59 -0
  55. package/addon/components/widget/kpi-tile.js +168 -0
  56. package/addon/components/widget/live-fleet.hbs +122 -0
  57. package/addon/components/widget/live-fleet.js +365 -0
  58. package/addon/components/widget/maintenance-overview.hbs +45 -0
  59. package/addon/components/widget/maintenance-overview.js +40 -0
  60. package/addon/components/widget/on-time-delivery.hbs +27 -0
  61. package/addon/components/widget/on-time-delivery.js +73 -0
  62. package/addon/components/widget/operations-pulse.hbs +51 -0
  63. package/addon/components/widget/operations-pulse.js +173 -0
  64. package/addon/components/widget/orders-by-status.hbs +26 -0
  65. package/addon/components/widget/orders-by-status.js +54 -0
  66. package/addon/components/widget/revenue-trend.hbs +34 -0
  67. package/addon/components/widget/revenue-trend.js +67 -0
  68. package/addon/components/widget/top-drivers.hbs +44 -0
  69. package/addon/components/widget/top-drivers.js +54 -0
  70. package/addon/controllers/settings/routing.js +124 -1
  71. package/addon/extension.js +172 -3
  72. package/addon/helpers/format-duration.js +38 -0
  73. package/addon/routes/application.js +6 -0
  74. package/addon/services/map-adapter/google.js +13 -2
  75. package/addon/services/order-list-overlay.js +2 -2
  76. package/addon/styles/fleetops-engine.css +806 -46
  77. package/addon/templates/maintenance/equipment/index.hbs +1 -1
  78. package/addon/templates/maintenance/parts/index.hbs +1 -1
  79. package/addon/templates/maintenance/schedules/index.hbs +1 -1
  80. package/addon/templates/management/vehicles/index.hbs +1 -1
  81. package/addon/templates/settings/avatars.hbs +1 -1
  82. package/addon/templates/settings/custom-fields.hbs +1 -1
  83. package/addon/templates/settings/map.hbs +1 -1
  84. package/addon/templates/settings/navigator-app.hbs +1 -1
  85. package/addon/templates/settings/notifications.hbs +1 -1
  86. package/addon/templates/settings/orchestrator.hbs +1 -1
  87. package/addon/templates/settings/payments/index.hbs +1 -1
  88. package/addon/templates/settings/payments/onboard.hbs +1 -1
  89. package/addon/templates/settings/routing.hbs +82 -4
  90. package/addon/templates/settings/scheduling.hbs +1 -1
  91. package/addon/utils/live-map-card-content.js +171 -0
  92. package/app/components/tracking-stop-progress.js +1 -0
  93. package/app/components/widget/fuel-efficiency.js +1 -0
  94. package/app/components/widget/geofence-violations.js +1 -0
  95. package/app/components/widget/issues-insights.js +1 -0
  96. package/app/components/widget/kpi-active-orders.js +1 -0
  97. package/app/components/widget/kpi-aov.js +1 -0
  98. package/app/components/widget/kpi-distance.js +1 -0
  99. package/app/components/widget/kpi-drivers-online.js +1 -0
  100. package/app/components/widget/kpi-earnings.js +1 -0
  101. package/app/components/widget/kpi-open-issues.js +1 -0
  102. package/app/components/widget/kpi-tile.js +1 -0
  103. package/app/components/widget/live-fleet.js +1 -0
  104. package/app/components/widget/maintenance-overview.js +1 -0
  105. package/app/components/widget/on-time-delivery.js +1 -0
  106. package/app/components/widget/operations-pulse.js +1 -0
  107. package/app/components/widget/orders-by-status.js +1 -0
  108. package/app/components/widget/revenue-trend.js +1 -0
  109. package/app/components/widget/top-drivers.js +1 -0
  110. package/app/helpers/format-duration.js +1 -0
  111. package/app/utils/live-map-card-content.js +1 -0
  112. package/composer.json +1 -1
  113. package/extension.json +1 -1
  114. package/package.json +3 -3
  115. package/pnpm-workspace.yaml +1 -0
  116. package/server/config/fleetops.php +32 -11
  117. package/server/config/telematics.php +111 -1
  118. package/server/migrations/2026_05_13_000001_repair_customer_contact_user_types.php +86 -0
  119. package/server/migrations/2026_05_18_000001_add_license_expiry_to_drivers_table.php +25 -0
  120. package/server/seeders/Testing/Concerns/SeedsTestingData.php +183 -0
  121. package/server/seeders/Testing/ConnectivitySeeder.php +212 -0
  122. package/server/seeders/Testing/FleetSeeder.php +341 -0
  123. package/server/seeders/Testing/MaintenanceSeeder.php +238 -0
  124. package/server/seeders/Testing/NetworkSeeder.php +250 -0
  125. package/server/seeders/Testing/OrdersSeeder.php +214 -0
  126. package/server/seeders/Testing/TestingSeeder.php +40 -0
  127. package/server/seeders/Testing/VroomOrchestrationSeeder.php +409 -0
  128. package/server/src/Console/Commands/ProcessOperationalAlerts.php +279 -0
  129. package/server/src/Http/Controllers/Api/v1/OrchestrationController.php +64 -0
  130. package/server/src/Http/Controllers/Api/v1/OrderController.php +26 -26
  131. package/server/src/Http/Controllers/Api/v1/PayloadController.php +10 -10
  132. package/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php +1 -1
  133. package/server/src/Http/Controllers/Internal/v1/AnalyticsController.php +119 -0
  134. package/server/src/Http/Controllers/Internal/v1/ContactController.php +41 -0
  135. package/server/src/Http/Controllers/Internal/v1/LiveController.php +6 -42
  136. package/server/src/Http/Controllers/Internal/v1/MetricsController.php +82 -3
  137. package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +1 -1
  138. package/server/src/Http/Controllers/Internal/v1/OrderController.php +52 -21
  139. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
  140. package/server/src/Http/Controllers/Internal/v1/SettingController.php +164 -12
  141. package/server/src/Http/Resources/v1/Driver.php +2 -0
  142. package/server/src/Http/Resources/v1/Fleet.php +4 -0
  143. package/server/src/Http/Resources/v1/Index/Driver.php +48 -0
  144. package/server/src/Http/Resources/v1/Index/Payload.php +2 -2
  145. package/server/src/Http/Resources/v1/Index/Vehicle.php +74 -1
  146. package/server/src/Models/Contact.php +122 -13
  147. package/server/src/Models/Driver.php +2 -0
  148. package/server/src/Models/Order.php +1 -1
  149. package/server/src/Models/OrderConfig.php +14 -11
  150. package/server/src/Models/Payload.php +8 -8
  151. package/server/src/Models/Place.php +0 -3
  152. package/server/src/Models/ServiceRate.php +1 -1
  153. package/server/src/Notifications/LateDeparture.php +48 -0
  154. package/server/src/Notifications/ProlongedStoppage.php +48 -0
  155. package/server/src/Notifications/RouteDeviation.php +48 -0
  156. package/server/src/Observers/ContactObserver.php +8 -0
  157. package/server/src/Orchestration/Engines/CapacityAllocationEngine.php +220 -0
  158. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +357 -38
  159. package/server/src/Orchestration/Support/OrchestrationPayloadBuilder.php +259 -51
  160. package/server/src/Providers/FleetOpsServiceProvider.php +32 -0
  161. package/server/src/Providers/NotificationServiceProvider.php +3 -0
  162. package/server/src/Support/Analytics/AbstractAnalytics.php +77 -0
  163. package/server/src/Support/Analytics/FuelEfficiency.php +83 -0
  164. package/server/src/Support/Analytics/GeofenceViolations.php +63 -0
  165. package/server/src/Support/Analytics/IssuesInsights.php +67 -0
  166. package/server/src/Support/Analytics/LiveFleet.php +127 -0
  167. package/server/src/Support/Analytics/MaintenanceOverview.php +70 -0
  168. package/server/src/Support/Analytics/OnTimeDelivery.php +75 -0
  169. package/server/src/Support/Analytics/OperationsPulse.php +86 -0
  170. package/server/src/Support/Analytics/OrdersByStatus.php +64 -0
  171. package/server/src/Support/Analytics/RevenueTrend.php +79 -0
  172. package/server/src/Support/Analytics/TopDrivers.php +92 -0
  173. package/server/src/Support/LiveOrderQuery.php +75 -0
  174. package/server/src/Support/Metrics/AbstractMetric.php +170 -0
  175. package/server/src/Support/Metrics/ActiveLiveOrdersMetric.php +28 -0
  176. package/server/src/Support/Metrics/AvgOrderValueMetric.php +53 -0
  177. package/server/src/Support/Metrics/DriversOnlineMetric.php +30 -0
  178. package/server/src/Support/Metrics/EarningsMetric.php +30 -0
  179. package/server/src/Support/Metrics/FuelCostsMetric.php +30 -0
  180. package/server/src/Support/Metrics/MoneyMetric.php +30 -0
  181. package/server/src/Support/Metrics/OpenIssuesMetric.php +35 -0
  182. package/server/src/Support/Metrics/OrdersCanceledMetric.php +35 -0
  183. package/server/src/Support/Metrics/OrdersCompletedMetric.php +35 -0
  184. package/server/src/Support/Metrics/OrdersInProgressMetric.php +51 -0
  185. package/server/src/Support/Metrics/OrdersScheduledMetric.php +37 -0
  186. package/server/src/Support/Metrics/Registry.php +46 -0
  187. package/server/src/Support/Metrics/ResolvedIssuesMetric.php +35 -0
  188. package/server/src/Support/Metrics/TotalCustomersMetric.php +29 -0
  189. package/server/src/Support/Metrics/TotalDistanceTraveledMetric.php +35 -0
  190. package/server/src/Support/Metrics/TotalDriversMetric.php +28 -0
  191. package/server/src/Support/Metrics/TotalTimeTraveledMetric.php +35 -0
  192. package/server/src/Support/Metrics.php +46 -234
  193. package/server/src/Support/OSRM.php +11 -6
  194. package/server/src/Support/OrderTracker.php +7 -582
  195. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +290 -0
  196. package/server/src/Support/Telematics/Providers/SafeeProvider.php +333 -0
  197. package/server/src/Support/Utils.php +1 -1
  198. package/server/src/Tracking/Contracts/TrackingProviderInterface.php +19 -0
  199. package/server/src/Tracking/Providers/CalculatedTrackingProvider.php +65 -0
  200. package/server/src/Tracking/Providers/GoogleRoutesTrackingProvider.php +118 -0
  201. package/server/src/Tracking/Providers/OsrmTrackingProvider.php +71 -0
  202. package/server/src/Tracking/README.md +25 -0
  203. package/server/src/Tracking/Support/FakeTrackingProvider.php +42 -0
  204. package/server/src/Tracking/TrackingContext.php +63 -0
  205. package/server/src/Tracking/TrackingContextBuilder.php +204 -0
  206. package/server/src/Tracking/TrackingIntelligenceService.php +180 -0
  207. package/server/src/Tracking/TrackingOptions.php +49 -0
  208. package/server/src/Tracking/TrackingProviderCapabilities.php +30 -0
  209. package/server/src/Tracking/TrackingProviderManager.php +89 -0
  210. package/server/src/Tracking/TrackingProviderRegistry.php +35 -0
  211. package/server/src/Tracking/TrackingProviderResult.php +20 -0
  212. package/server/src/Tracking/TrackingStop.php +59 -0
  213. package/server/src/routes.php +27 -0
  214. package/server/tests/AnalyticsRoutesTest.php +41 -0
  215. package/server/tests/CapacityAllocationEngineTest.php +108 -0
  216. package/server/tests/LiveOrderQueryTest.php +34 -0
  217. package/server/tests/MetricsRegistryTest.php +88 -0
  218. package/server/tests/OrchestrationPayloadBuilderTest.php +132 -0
  219. package/server/tests/OrchestratorConsumableApiTest.php +74 -0
  220. package/server/tests/PingDriverEndpointTest.php +19 -0
  221. package/server/tests/TrackingIntelligenceTest.php +176 -0
  222. package/server/tests/VroomOrchestrationEngineTest.php +250 -0
  223. package/translations/en-us.yaml +41 -0
@@ -5,6 +5,7 @@ namespace Fleetbase\FleetOps\Models;
5
5
  use Fleetbase\Casts\Json;
6
6
  use Fleetbase\FleetOps\Exceptions\UserAlreadyExistsException;
7
7
  use Fleetbase\FleetOps\Support\Utils;
8
+ use Fleetbase\Models\CompanyUser;
8
9
  use Fleetbase\Models\Invite;
9
10
  use Fleetbase\Models\Model;
10
11
  use Fleetbase\Models\User;
@@ -261,6 +262,14 @@ class Contact extends Model
261
262
  return $this->type === 'customer';
262
263
  }
263
264
 
265
+ /**
266
+ * Determines if this contact is a Fleet-Ops customer.
267
+ */
268
+ public function isCustomer(): bool
269
+ {
270
+ return $this->type === 'customer';
271
+ }
272
+
264
273
  /**
265
274
  * Creates a new contact from an import row.
266
275
  */
@@ -308,19 +317,33 @@ class Contact extends Model
308
317
  public static function createUserFromContact(Contact $contact, bool $sendInvite = false, bool $update = false): User
309
318
  {
310
319
  // Check if user already exist with email or phone number
311
- $existingUser = User::where(function ($query) use ($contact) {
312
- $query->where('company_uuid', $contact->company_uuid);
313
- $query->where('email', $contact->email);
314
- if ($contact->phone) {
315
- $query->orWhere('phone', Utils::formatPhoneNumber($contact->phone));
316
- }
317
- })->whereNull('deleted_at')->first();
320
+ $existingUser = null;
321
+ if ($contact->email || $contact->phone) {
322
+ $existingUser = User::where('company_uuid', $contact->company_uuid)
323
+ ->where(function ($query) use ($contact) {
324
+ if ($contact->email) {
325
+ $query->where('email', $contact->email);
326
+ }
327
+
328
+ if ($contact->phone) {
329
+ $method = $contact->email ? 'orWhere' : 'where';
330
+ $query->{$method}('phone', Utils::formatPhoneNumber($contact->phone));
331
+ }
332
+ })
333
+ ->whereNull('deleted_at')
334
+ ->first();
335
+ }
336
+
318
337
  if ($existingUser) {
319
338
  // Check if existing user belongs to another contact
320
339
  $existingUserContact = Contact::where(['user_uuid' => $existingUser->uuid, 'company_uuid' => $contact->company_uuid])->whereHas('user')->first();
321
340
  if ($existingUserContact) {
322
341
  throw new UserAlreadyExistsException('User already exists, try to assigning the user to this contact.', $existingUser);
323
342
  } else {
343
+ if ($contact->isCustomer()) {
344
+ $contact->normalizeCustomerUser($existingUser);
345
+ }
346
+
324
347
  // Assign the user to this contact instead
325
348
  $contact->setAttribute('user_uuid', $existingUser->uuid);
326
349
  if ($update) {
@@ -351,10 +374,10 @@ class Contact extends Model
351
374
  $user->setType($contact->type);
352
375
 
353
376
  // Assing to company
354
- $user->assignCompany($contact->company, $user->type === 'customer' ? 'Fleet-Ops Customer' : 'Fleet-Ops Contact');
377
+ $user->assignCompany($contact->company, $contact->isCustomer() ? 'Fleet-Ops Customer' : 'Fleet-Ops Contact');
355
378
 
356
379
  // Assign customer role
357
- if ($user->type === 'customer') {
380
+ if ($contact->isCustomer()) {
358
381
  $user->assignSingleRole('Fleet-Ops Customer');
359
382
  }
360
383
 
@@ -386,6 +409,87 @@ class Contact extends Model
386
409
  return $user;
387
410
  }
388
411
 
412
+ /**
413
+ * Normalize the linked user for a customer contact.
414
+ */
415
+ public function normalizeCustomerUser(?User $user = null, bool $quiet = false): ?User
416
+ {
417
+ if (!$this->isCustomer()) {
418
+ return $user;
419
+ }
420
+
421
+ $user ??= $this->getUser();
422
+ if (!$user) {
423
+ return null;
424
+ }
425
+
426
+ if ($user->type !== 'customer') {
427
+ if ($quiet) {
428
+ $user->forceFill(['type' => 'customer'])->saveQuietly();
429
+ } else {
430
+ $user->setType('customer');
431
+ }
432
+ }
433
+
434
+ $this->loadMissing('company');
435
+ if ($this->company) {
436
+ $companyUser = CompanyUser::firstOrCreate(
437
+ [
438
+ 'company_uuid' => $this->company->uuid,
439
+ 'user_uuid' => $user->uuid,
440
+ ],
441
+ [
442
+ 'company_uuid' => $this->company->uuid,
443
+ 'user_uuid' => $user->uuid,
444
+ 'status' => $user->status ?? 'active',
445
+ ]
446
+ );
447
+ $user->forceFill(['company_uuid' => $this->company->uuid])->saveQuietly();
448
+ $user->setRelation('companyUser', $companyUser);
449
+
450
+ if ($companyUser) {
451
+ $companyUser->assignSingleRole('Fleet-Ops Customer');
452
+ }
453
+ }
454
+
455
+ $this->setRelation('user', $user);
456
+
457
+ return $user;
458
+ }
459
+
460
+ /**
461
+ * Repair a historical customer contact and its linked user.
462
+ */
463
+ public function repairCustomerTypeInvariant(bool $quiet = false): ?User
464
+ {
465
+ if (!$this->isCustomer()) {
466
+ $this->forceFill(['type' => 'customer'])->saveQuietly();
467
+ }
468
+
469
+ $user = $this->getUser();
470
+ if (!$user && ($this->email || $this->phone)) {
471
+ if ($quiet) {
472
+ $this->loadMissing('company');
473
+ $user = User::create([
474
+ 'company_uuid' => $this->company_uuid,
475
+ 'name' => $this->name,
476
+ 'email' => $this->email,
477
+ 'phone' => $this->phone ? Utils::formatPhoneNumber($this->phone) : null,
478
+ 'username' => Str::slug($this->name . '_' . Str::random(4), '_'),
479
+ 'password' => Str::random(),
480
+ 'timezone' => $this->company->timezone ?? date_default_timezone_get(),
481
+ 'status' => 'pending',
482
+ ]);
483
+ $user->forceFill(['type' => 'customer'])->saveQuietly();
484
+ $this->forceFill(['user_uuid' => $user->uuid])->saveQuietly();
485
+ } else {
486
+ $user = $this->createUser();
487
+ }
488
+ }
489
+
490
+ return $this->normalizeCustomerUser($user, $quiet);
491
+ }
492
+
389
493
  /**
390
494
  * Assigns a user to the company and optionally sends an invitation email.
391
495
  *
@@ -406,16 +510,21 @@ class Contact extends Model
406
510
  public function assignUser(User $user, bool $sendInvite = false): self
407
511
  {
408
512
  // Load company
409
- $this->loadMissing('copmany');
513
+ $this->loadMissing('company');
514
+
515
+ if ($this->isCustomer() && $user->type !== 'customer') {
516
+ $user->setType('customer');
517
+ }
410
518
 
411
519
  // Assing to company
412
- $user->assignCompany($this->company, $this->type === 'customer' ? 'Fleet-Ops Customer' : 'Fleet-Ops Contact');
520
+ $user->assignCompany($this->company, $this->isCustomer() ? 'Fleet-Ops Customer' : 'Fleet-Ops Contact');
413
521
 
414
522
  // Get the company user instance
415
523
  $companyUser = $user->getCompanyUser($this->company);
416
524
 
417
- // Assign customer role
418
- $companyUser->assignSingleRole('Fleet-Ops Customer');
525
+ if ($companyUser) {
526
+ $companyUser->assignSingleRole($this->isCustomer() ? 'Fleet-Ops Customer' : 'Fleet-Ops Contact');
527
+ }
419
528
 
420
529
  // Set user to contact
421
530
  $this->update(['user_uuid' => $user->uuid]);
@@ -97,6 +97,7 @@ class Driver extends Model
97
97
  'signup_token_used',
98
98
  'avatar_url',
99
99
  'drivers_license_number',
100
+ 'license_expiry',
100
101
  'location',
101
102
  'heading',
102
103
  'bearing',
@@ -141,6 +142,7 @@ class Driver extends Model
141
142
  'location' => Point::class,
142
143
  'online' => 'boolean',
143
144
  'meta' => Json::class,
145
+ 'license_expiry' => 'date',
144
146
  // Orchestrator
145
147
  'skills' => Json::class,
146
148
  'max_travel_time' => 'integer',
@@ -1739,7 +1739,7 @@ class Order extends Model
1739
1739
  }
1740
1740
  }
1741
1741
 
1742
- $company = $this->relationLoaded('company') ? $this->company : $this->company()->first();
1742
+ $company = $this->relationLoaded('company') ? $this->company : $this->company()->first();
1743
1743
  $orderConfig = OrderConfig::defaultOrCreate($company);
1744
1744
  if ($orderConfig instanceof OrderConfig) {
1745
1745
  $orderConfig->setOrderContext($this);
@@ -458,28 +458,31 @@ class OrderConfig extends Model
458
458
  /**
459
459
  * Resolves an OrderConfig from a given identifier or an array of identifiers within an optional company context.
460
460
  *
461
- * @param string|array $orderConfigIdentifier the identifier(s) for the OrderConfig (namespace, public_id, or key)
461
+ * @param string|array $orderConfigIdentifier the identifier(s) for the OrderConfig (uuid, namespace, public_id, or key)
462
462
  * @param Company|null $company the company context, if any, to narrow down the search
463
463
  *
464
464
  * @return OrderConfig|null the found OrderConfig or null if none found
465
465
  */
466
466
  public static function resolveFromIdentifier($orderConfigIdentifier, ?Company $company = null): ?OrderConfig
467
467
  {
468
- $query = static::query();
468
+ $companyUuid = $company instanceof Company ? $company->uuid : session('company');
469
+ $identifiers = is_array($orderConfigIdentifier) ? $orderConfigIdentifier : [$orderConfigIdentifier];
470
+ $identifiers = collect($identifiers)->filter(fn ($identifier) => filled($identifier))->values();
471
+
472
+ if ($identifiers->isEmpty()) {
473
+ return static::default($company);
474
+ }
475
+
476
+ foreach ($identifiers as $identifier) {
477
+ $query = static::query();
469
478
 
470
- if ($company instanceof Company) {
471
- $query->where('company_uuid', $company->uuid);
472
- } else {
473
- $companyUuid = session('company');
474
479
  if ($companyUuid) {
475
480
  $query->where('company_uuid', $companyUuid);
476
481
  }
477
- }
478
482
 
479
- $identifiers = is_array($orderConfigIdentifier) ? $orderConfigIdentifier : [$orderConfigIdentifier];
480
- foreach ($identifiers as $identifier) {
481
483
  $orderConfig = $query->where(function ($query) use ($identifier) {
482
- $query->where('namespace', $identifier)
484
+ $query->where('uuid', $identifier)
485
+ ->orWhere('namespace', $identifier)
483
486
  ->orWhere('public_id', $identifier)
484
487
  ->orWhere('key', $identifier);
485
488
  })->first();
@@ -489,7 +492,7 @@ class OrderConfig extends Model
489
492
  }
490
493
  }
491
494
 
492
- return static::default($company);
495
+ return null;
493
496
  }
494
497
 
495
498
  /**
@@ -555,15 +555,15 @@ class Payload extends Model
555
555
  }
556
556
 
557
557
  $this->loadMissing('waypointMarkers');
558
- $keptWaypointIds = [];
558
+ $keptWaypointIds = [];
559
559
  $availableWaypointMarkers = $this->waypointMarkers()->get();
560
560
 
561
561
  foreach ($waypoints as $index => $attributes) {
562
- $raw = $attributes;
563
- $type = data_get($raw, 'type', 'dropoff');
564
- $customerUuidIn = data_get($raw, 'customer_uuid');
562
+ $raw = $attributes;
563
+ $type = data_get($raw, 'type', 'dropoff');
564
+ $customerUuidIn = data_get($raw, 'customer_uuid');
565
565
  $customerPubIdIn = data_get($raw, 'customer_id');
566
- $customerType = data_get($raw, 'customer_type', 'fleetops:contact');
566
+ $customerType = data_get($raw, 'customer_type', 'fleetops:contact');
567
567
 
568
568
  if (Utils::isset($attributes, 'place') && is_array(Utils::get($attributes, 'place'))) {
569
569
  $attributes = Utils::get($attributes, 'place');
@@ -599,12 +599,12 @@ class Payload extends Model
599
599
  $placeUuid = $place->uuid;
600
600
  }
601
601
 
602
- $customerUuid = null;
602
+ $customerUuid = null;
603
603
  $customerTypeNamespace = null;
604
604
 
605
605
  if ($customerType) {
606
606
  $customerTypeNamespace = Utils::getMutationType($customerType);
607
- $customerModel = app($customerTypeNamespace);
607
+ $customerModel = app($customerTypeNamespace);
608
608
 
609
609
  if ($customerUuidIn && $customerModel->where('uuid', $customerUuidIn)->exists()) {
610
610
  $customerUuid = $customerUuidIn;
@@ -642,7 +642,7 @@ class Payload extends Model
642
642
  continue;
643
643
  }
644
644
 
645
- $waypointRecord = Waypoint::create($values);
645
+ $waypointRecord = Waypoint::create($values);
646
646
  $keptWaypointIds[] = $waypointRecord->uuid;
647
647
  }
648
648
 
@@ -21,7 +21,6 @@ use Fleetbase\Traits\Searchable;
21
21
  use Fleetbase\Traits\SendsWebhooks;
22
22
  use Fleetbase\Traits\TracksApiCredential;
23
23
  use Illuminate\Support\Carbon;
24
- use Illuminate\Support\Facades\DB;
25
24
  use Illuminate\Support\Str;
26
25
 
27
26
  class Place extends Model
@@ -383,8 +382,6 @@ class Place extends Model
383
382
 
384
383
  /**
385
384
  * Create a new Place instance from a geocoding lookup.
386
- *
387
- * @return array
388
385
  */
389
386
  public static function getValuesFromGeocodingLookup(string $address): array
390
387
  {
@@ -740,7 +740,7 @@ class ServiceRate extends Model
740
740
 
741
741
  if ($this->isAlgorithm()) {
742
742
  $resolvedEndpointCount = $endpointCount ?? $this->inferEndpointCountFromStops($waypoints);
743
- $rateFee = $this->normalizeCalculatedMoney(Algo::exec(
743
+ $rateFee = $this->normalizeCalculatedMoney(Algo::exec(
744
744
  $this->algorithm,
745
745
  $this->buildAlgorithmVariables(
746
746
  $entities,
@@ -0,0 +1,48 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Notifications;
4
+
5
+ use Fleetbase\FleetOps\Models\Order;
6
+ use Illuminate\Bus\Queueable;
7
+ use Illuminate\Contracts\Queue\ShouldQueue;
8
+ use Illuminate\Notifications\Messages\MailMessage;
9
+ use Illuminate\Notifications\Notification;
10
+
11
+ class LateDeparture extends Notification implements ShouldQueue
12
+ {
13
+ use Queueable;
14
+
15
+ public static string $name = 'Late Departure';
16
+ public static string $description = 'Notify when an order has not departed after the configured grace period.';
17
+ public static string $package = 'fleet-ops';
18
+
19
+ public function __construct(public Order $order, public array $context = [])
20
+ {
21
+ }
22
+
23
+ public function via($notifiable): array
24
+ {
25
+ return ['mail', 'database'];
26
+ }
27
+
28
+ public function toMail($notifiable): MailMessage
29
+ {
30
+ $orderId = $this->order->tracking ?? $this->order->public_id;
31
+
32
+ return (new MailMessage())
33
+ ->subject("Late departure detected for {$orderId}")
34
+ ->line("Order {$orderId} has not departed within the configured grace period.")
35
+ ->line('Please review the order and dispatch status.');
36
+ }
37
+
38
+ public function toArray($notifiable): array
39
+ {
40
+ return [
41
+ 'event' => 'order.late_departure',
42
+ 'order_id' => $this->order->public_id,
43
+ 'order_uuid' => $this->order->uuid,
44
+ 'context' => $this->context,
45
+ 'message' => sprintf('Order %s has not departed within the configured grace period.', $this->order->tracking ?? $this->order->public_id),
46
+ ];
47
+ }
48
+ }
@@ -0,0 +1,48 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Notifications;
4
+
5
+ use Fleetbase\FleetOps\Models\Order;
6
+ use Illuminate\Bus\Queueable;
7
+ use Illuminate\Contracts\Queue\ShouldQueue;
8
+ use Illuminate\Notifications\Messages\MailMessage;
9
+ use Illuminate\Notifications\Notification;
10
+
11
+ class ProlongedStoppage extends Notification implements ShouldQueue
12
+ {
13
+ use Queueable;
14
+
15
+ public static string $name = 'Prolonged Stoppage';
16
+ public static string $description = 'Notify when a vehicle remains stopped during an active trip beyond the configured threshold.';
17
+ public static string $package = 'fleet-ops';
18
+
19
+ public function __construct(public Order $order, public array $context = [])
20
+ {
21
+ }
22
+
23
+ public function via($notifiable): array
24
+ {
25
+ return ['mail', 'database'];
26
+ }
27
+
28
+ public function toMail($notifiable): MailMessage
29
+ {
30
+ $orderId = $this->order->tracking ?? $this->order->public_id;
31
+
32
+ return (new MailMessage())
33
+ ->subject("Prolonged stoppage detected for {$orderId}")
34
+ ->line("The vehicle assigned to order {$orderId} has remained stopped beyond the configured threshold.")
35
+ ->line('Please review the active trip and vehicle location.');
36
+ }
37
+
38
+ public function toArray($notifiable): array
39
+ {
40
+ return [
41
+ 'event' => 'order.prolonged_stoppage',
42
+ 'order_id' => $this->order->public_id,
43
+ 'order_uuid' => $this->order->uuid,
44
+ 'context' => $this->context,
45
+ 'message' => sprintf('The vehicle assigned to order %s has remained stopped beyond the configured threshold.', $this->order->tracking ?? $this->order->public_id),
46
+ ];
47
+ }
48
+ }
@@ -0,0 +1,48 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Notifications;
4
+
5
+ use Fleetbase\FleetOps\Models\Order;
6
+ use Illuminate\Bus\Queueable;
7
+ use Illuminate\Contracts\Queue\ShouldQueue;
8
+ use Illuminate\Notifications\Messages\MailMessage;
9
+ use Illuminate\Notifications\Notification;
10
+
11
+ class RouteDeviation extends Notification implements ShouldQueue
12
+ {
13
+ use Queueable;
14
+
15
+ public static string $name = 'Route Deviation';
16
+ public static string $description = 'Notify when tracked vehicle location deviates from the active route beyond the configured threshold.';
17
+ public static string $package = 'fleet-ops';
18
+
19
+ public function __construct(public Order $order, public array $context = [])
20
+ {
21
+ }
22
+
23
+ public function via($notifiable): array
24
+ {
25
+ return ['mail', 'database'];
26
+ }
27
+
28
+ public function toMail($notifiable): MailMessage
29
+ {
30
+ $orderId = $this->order->tracking ?? $this->order->public_id;
31
+
32
+ return (new MailMessage())
33
+ ->subject("Route deviation detected for {$orderId}")
34
+ ->line("The vehicle assigned to order {$orderId} has deviated from the active route beyond the configured threshold.")
35
+ ->line('Please review the route and current vehicle position.');
36
+ }
37
+
38
+ public function toArray($notifiable): array
39
+ {
40
+ return [
41
+ 'event' => 'order.route_deviation',
42
+ 'order_id' => $this->order->public_id,
43
+ 'order_uuid' => $this->order->uuid,
44
+ 'context' => $this->context,
45
+ 'message' => sprintf('The vehicle assigned to order %s has deviated from the active route beyond the configured threshold.', $this->order->tracking ?? $this->order->public_id),
46
+ ];
47
+ }
48
+ }
@@ -27,11 +27,19 @@ class ContactObserver
27
27
  */
28
28
  public function saving(Contact $contact)
29
29
  {
30
+ if ($contact->exists && $contact->getOriginal('type') === 'customer' && $contact->isDirty('type') && $contact->type !== 'customer') {
31
+ throw new \Exception('Customer contact type cannot be changed.');
32
+ }
33
+
30
34
  // Get the contacts assosciated user
31
35
  if ($contact->doesntHaveUser()) {
32
36
  $contact->createUser();
33
37
  }
34
38
 
39
+ if ($contact->isCustomer()) {
40
+ $contact->normalizeCustomerUser();
41
+ }
42
+
35
43
  // Validate email is available to user
36
44
  if (!empty($contact->email) && $contact->wasChanged('email') && $this->isEmailUnavailable($contact)) {
37
45
  throw new \Exception('Email attempting to update for ' . $contact->type . ' is not available.');