@fleetbase/fleetops-engine 0.6.49 → 0.6.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (271) hide show
  1. package/README.md +185 -67
  2. package/addon/components/contact/panel-header.hbs +11 -48
  3. package/addon/components/customer/create-order-form.hbs +2 -2
  4. package/addon/components/customer/details.hbs +1 -2
  5. package/addon/components/customer/form.hbs +62 -50
  6. package/addon/components/customer/form.js +27 -1
  7. package/addon/components/customer/order-form.hbs +2 -2
  8. package/addon/components/customer/orders.hbs +50 -2
  9. package/addon/components/customer/orders.js +67 -5
  10. package/addon/components/display-place.hbs +2 -2
  11. package/addon/components/driver/form.hbs +1 -1
  12. package/addon/components/home/getting-started-guidance.hbs +113 -0
  13. package/addon/components/home/getting-started-guidance.js +107 -0
  14. package/addon/components/issue/details/comments.hbs +3 -0
  15. package/addon/components/issue/details/comments.js +3 -0
  16. package/addon/components/issue/details/correspondence.hbs +9 -0
  17. package/addon/components/issue/details/correspondence.js +83 -0
  18. package/addon/components/issue/details/documents.hbs +69 -0
  19. package/addon/components/issue/details/documents.js +85 -0
  20. package/addon/components/issue/details.hbs +220 -59
  21. package/addon/components/issue/details.js +245 -0
  22. package/addon/components/issue/form.hbs +19 -1
  23. package/addon/components/issue/panel-header.hbs +43 -0
  24. package/addon/components/issue/panel-header.js +22 -0
  25. package/addon/components/issue/timeline.hbs +40 -0
  26. package/addon/components/issue/timeline.js +45 -0
  27. package/addon/components/leaflet-tracking-marker.js +3 -0
  28. package/addon/components/map/drawer/position-listing.js +3 -0
  29. package/addon/components/map/google-live-map.js +6 -2
  30. package/addon/components/map/leaflet-live-map.hbs +48 -14
  31. package/addon/components/map/leaflet-live-map.js +201 -20
  32. package/addon/components/map/order-list-overlay.hbs +4 -4
  33. package/addon/components/modals/convert-customer-to-vendor.hbs +30 -0
  34. package/addon/components/modals/convert-customer-to-vendor.js +55 -0
  35. package/addon/components/modals/issue-assign.hbs +20 -0
  36. package/addon/components/modals/issue-assign.js +49 -0
  37. package/addon/components/modals/issue-close.hbs +30 -0
  38. package/addon/components/modals/issue-close.js +73 -0
  39. package/addon/components/modals/issue-status.hbs +20 -0
  40. package/addon/components/modals/issue-status.js +43 -0
  41. package/addon/components/modals/order-change-destination.hbs +46 -0
  42. package/addon/components/modals/order-change-destination.js +106 -0
  43. package/addon/components/modals/reset-customer-credentials.js +12 -6
  44. package/addon/components/modals/stop-activity.hbs +28 -0
  45. package/addon/components/modals/stop-activity.js +52 -0
  46. package/addon/components/modals/update-order-activity.hbs +78 -2
  47. package/addon/components/modals/update-order-activity.js +197 -5
  48. package/addon/components/orchestrator/resource-panel.hbs +0 -10
  49. package/addon/components/orchestrator/resource-panel.js +2 -4
  50. package/addon/components/order/activity-list.hbs +2 -2
  51. package/addon/components/order/activity-timeline.hbs +31 -29
  52. package/addon/components/order/details/activity.hbs +3 -3
  53. package/addon/components/order/details/activity.js +13 -2
  54. package/addon/components/order/details/documents.js +2 -0
  55. package/addon/components/order/details/proof.hbs +40 -38
  56. package/addon/components/order/details/proof.js +31 -4
  57. package/addon/components/order/details/purchase-rate.hbs +2 -2
  58. package/addon/components/order/details/route.hbs +11 -3
  59. package/addon/components/order/details/route.js +91 -14
  60. package/addon/components/order/details/tracking.hbs +101 -72
  61. package/addon/components/order/details/tracking.js +149 -2
  62. package/addon/components/order/details.hbs +5 -5
  63. package/addon/components/order/details.js +19 -1
  64. package/addon/components/order/form/details.hbs +3 -3
  65. package/addon/components/order/form/documents.js +11 -4
  66. package/addon/components/order/form/route.hbs +108 -104
  67. package/addon/components/order/form/route.js +37 -1
  68. package/addon/components/order/form/service-rate.hbs +2 -2
  69. package/addon/components/order/header.hbs +1 -1
  70. package/addon/components/order-config-manager/activity-flow.hbs +4 -1
  71. package/addon/components/order-config-manager/activity-flow.js +178 -12
  72. package/addon/components/place/address.js +1 -1
  73. package/addon/components/route-list.hbs +17 -29
  74. package/addon/components/route-list.js +42 -10
  75. package/addon/components/service-rate/details.hbs +42 -1
  76. package/addon/components/service-rate/form.hbs +119 -1
  77. package/addon/components/service-rate/form.js +59 -0
  78. package/addon/components/vehicle/form.js +1 -1
  79. package/addon/components/vehicle/pill.hbs +1 -1
  80. package/addon/components/vendor/personnel-manager.hbs +166 -0
  81. package/addon/components/vendor/personnel-manager.js +143 -0
  82. package/addon/components/widget/live-fleet.hbs +1 -0
  83. package/addon/components/widget/live-fleet.js +2 -7
  84. package/addon/components/widget/revenue-trend.hbs +1 -1
  85. package/addon/components/widget/revenue-trend.js +75 -2
  86. package/addon/controllers/management/contacts/customers/details.js +31 -13
  87. package/addon/controllers/management/contacts/customers.js +16 -5
  88. package/addon/controllers/management/contacts/index/details.js +31 -13
  89. package/addon/controllers/management/contacts/index.js +7 -0
  90. package/addon/controllers/management/issues/index/details.js +52 -6
  91. package/addon/controllers/management/vehicles/index/new.js +1 -1
  92. package/addon/controllers/management/vendors/index/details.js +4 -0
  93. package/addon/controllers/operations/orders/index/details.js +15 -7
  94. package/addon/controllers/operations/orders/index.js +15 -1
  95. package/addon/extension.js +19 -12
  96. package/addon/helpers/format-duration.js +1 -38
  97. package/addon/helpers/place-address-html.js +2 -2
  98. package/addon/initializers/load-leaflet-assets.js +5 -6
  99. package/addon/initializers/patch-ember-leaflet-tooltip-layer.js +51 -0
  100. package/addon/routes/management/issues/index/details.js +1 -1
  101. package/addon/routes/operations/service-rates/index.js +1 -1
  102. package/addon/routes.js +1 -0
  103. package/addon/services/contact-actions.js +231 -1
  104. package/addon/services/geofence.js +163 -19
  105. package/addon/services/getting-started.js +44 -0
  106. package/addon/services/issue-actions.js +71 -0
  107. package/addon/services/leaflet-contextmenu-manager.js +27 -0
  108. package/addon/services/leaflet-map-manager.js +2 -0
  109. package/addon/services/map-adapter/leaflet.js +19 -0
  110. package/addon/services/service-areas.js +3 -0
  111. package/addon/services/vehicle-actions.js +1 -1
  112. package/addon/styles/fleetops-engine.css +1127 -1
  113. package/addon/templates/management/contacts/customers/details.hbs +1 -1
  114. package/addon/templates/management/contacts/index/details.hbs +1 -1
  115. package/addon/templates/management/contacts.hbs +2 -2
  116. package/addon/templates/management/issues/index/details/index.hbs +3 -1
  117. package/addon/templates/management/issues/index/details.hbs +3 -2
  118. package/addon/templates/management/vendors/index/details/personnel.hbs +1 -2
  119. package/addon/templates/operations/orders/index/details/index.hbs +2 -2
  120. package/addon/templates/operations/orders/index.hbs +1 -1
  121. package/addon/templates/operations/service-rates/index/edit.hbs +1 -0
  122. package/addon/templates/operations/service-rates/index/new.hbs +1 -0
  123. package/addon/templates/settings/payments/onboard.hbs +8 -8
  124. package/addon/utils/fleet-ops-options.js +2 -1
  125. package/addon/utils/leaflet-draw-namespace-guard.js +22 -0
  126. package/addon/utils/leaflet-plugin-loader.js +219 -0
  127. package/addon/utils/normalize-order-config-flow.js +41 -0
  128. package/addon/utils/place-address-html.js +3 -2
  129. package/addon/utils/route-visualization.js +54 -7
  130. package/addon/utils/setup-customer-portal.js +4 -64
  131. package/app/components/home/getting-started-guidance.js +1 -0
  132. package/app/components/issue/details/comments.js +1 -0
  133. package/app/components/issue/details/correspondence.js +1 -0
  134. package/app/components/issue/details/documents.js +1 -0
  135. package/app/components/issue/panel-header.js +1 -0
  136. package/app/components/modals/convert-customer-to-vendor.js +1 -0
  137. package/app/components/modals/issue-assign.js +1 -0
  138. package/app/components/modals/issue-close.js +1 -0
  139. package/app/components/modals/issue-status.js +1 -0
  140. package/app/components/modals/order-change-destination.js +1 -0
  141. package/app/components/modals/stop-activity.js +1 -0
  142. package/app/components/vendor/personnel-manager.js +1 -0
  143. package/app/helpers/format-duration.js +1 -1
  144. package/app/services/getting-started.js +1 -0
  145. package/app/utils/leaflet-draw-namespace-guard.js +1 -0
  146. package/app/utils/leaflet-plugin-loader.js +1 -0
  147. package/composer.json +1 -1
  148. package/extension.json +1 -1
  149. package/package.json +5 -5
  150. package/server/migrations/2026_05_21_000001_add_geographic_rules_to_service_rate_fees_table.php +37 -0
  151. package/server/migrations/2026_05_25_000001_add_endpoint_tracking_refs_to_payloads_table.php +50 -0
  152. package/server/migrations/2026_05_27_000001_default_driver_vehicle_statuses_to_available.php +48 -0
  153. package/server/migrations/2026_05_31_000001_add_portal_fields_to_vendor_personnels_table.php +30 -0
  154. package/server/migrations/2026_06_03_000000_ensure_tags_column_on_issues_table.php +31 -0
  155. package/server/migrations/2026_06_03_000001_add_order_uuid_to_issues_table.php +25 -0
  156. package/server/migrations/2026_06_04_000001_backfill_order_transaction_subjects.php +39 -0
  157. package/server/migrations/2026_06_04_000002_backfill_order_transaction_directions.php +24 -0
  158. package/server/resources/views/labels/default.php +57 -49
  159. package/server/resources/views/labels/waypoint-label.php +27 -20
  160. package/server/resources/views/mail/customer-credentials.blade.php +16 -13
  161. package/server/seeders/Testing/ConnectivitySeeder.php +1 -0
  162. package/server/seeders/Testing/FleetSeeder.php +22 -22
  163. package/server/seeders/Testing/OrdersSeeder.php +6 -22
  164. package/server/seeders/Testing/VroomOrchestrationSeeder.php +21 -21
  165. package/server/src/Console/Commands/AuditCustomerUserConflicts.php +75 -0
  166. package/server/src/Exceptions/CustomerUserConflictException.php +13 -0
  167. package/server/src/Expansions/UserFilterExpansion.php +28 -0
  168. package/server/src/Http/Controllers/Api/v1/ContactController.php +17 -2
  169. package/server/src/Http/Controllers/Api/v1/CustomerController.php +876 -0
  170. package/server/src/Http/Controllers/Api/v1/DriverController.php +1 -1
  171. package/server/src/Http/Controllers/Api/v1/IssueController.php +2 -0
  172. package/server/src/Http/Controllers/Api/v1/OrderConfigController.php +54 -0
  173. package/server/src/Http/Controllers/Api/v1/OrderController.php +109 -80
  174. package/server/src/Http/Controllers/Api/v1/PlaceController.php +10 -58
  175. package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -2
  176. package/server/src/Http/Controllers/Api/v1/VehicleController.php +20 -3
  177. package/server/src/Http/Controllers/Internal/v1/ContactController.php +154 -0
  178. package/server/src/Http/Controllers/Internal/v1/CustomerController.php +116 -6
  179. package/server/src/Http/Controllers/Internal/v1/GettingStartedController.php +17 -0
  180. package/server/src/Http/Controllers/Internal/v1/IssueController.php +251 -0
  181. package/server/src/Http/Controllers/Internal/v1/LiveController.php +101 -66
  182. package/server/src/Http/Controllers/Internal/v1/OrderController.php +530 -52
  183. package/server/src/Http/Controllers/Internal/v1/PlaceController.php +10 -68
  184. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
  185. package/server/src/Http/Controllers/Internal/v1/SettingController.php +2 -2
  186. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +54 -1
  187. package/server/src/Http/Controllers/Internal/v1/VendorController.php +89 -0
  188. package/server/src/Http/Middleware/AuthenticateCustomerToken.php +34 -0
  189. package/server/src/Http/Requests/CreateCustomerOrderRequest.php +63 -0
  190. package/server/src/Http/Requests/CreateCustomerRequest.php +48 -0
  191. package/server/src/Http/Requests/CreateDriverRequest.php +1 -1
  192. package/server/src/Http/Requests/CreateIssueRequest.php +4 -0
  193. package/server/src/Http/Requests/CreateVehicleRequest.php +25 -1
  194. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +1 -1
  195. package/server/src/Http/Requests/UpdateIssueRequest.php +4 -0
  196. package/server/src/Http/Requests/VerifyCreateCustomerRequest.php +29 -0
  197. package/server/src/Http/Resources/Internal/v1/OrderConfig.php +41 -0
  198. package/server/src/Http/Resources/v1/Customer.php +79 -0
  199. package/server/src/Http/Resources/v1/Index/Order.php +12 -9
  200. package/server/src/Http/Resources/v1/Issue.php +25 -0
  201. package/server/src/Http/Resources/v1/Order.php +20 -18
  202. package/server/src/Http/Resources/v1/OrderConfig.php +74 -0
  203. package/server/src/Http/Resources/v1/Payload.php +23 -20
  204. package/server/src/Http/Resources/v1/ServiceArea.php +33 -19
  205. package/server/src/Http/Resources/v1/ServiceRateFee.php +7 -0
  206. package/server/src/Listeners/HandleOrderDispatched.php +2 -2
  207. package/server/src/Mail/CustomerCredentialsMail.php +5 -6
  208. package/server/src/Models/Contact.php +122 -66
  209. package/server/src/Models/Customer.php +64 -0
  210. package/server/src/Models/Driver.php +12 -3
  211. package/server/src/Models/Entity.php +2 -2
  212. package/server/src/Models/Issue.php +47 -6
  213. package/server/src/Models/Order.php +62 -10
  214. package/server/src/Models/Payload.php +17 -1
  215. package/server/src/Models/ServiceArea.php +1 -1
  216. package/server/src/Models/ServiceRate.php +273 -1
  217. package/server/src/Models/ServiceRateFee.php +39 -9
  218. package/server/src/Models/TrackingNumber.php +8 -2
  219. package/server/src/Models/Vehicle.php +18 -2
  220. package/server/src/Models/VendorPersonnel.php +10 -1
  221. package/server/src/Models/Waypoint.php +2 -2
  222. package/server/src/Models/Zone.php +1 -1
  223. package/server/src/Notifications/LateDeparture.php +2 -2
  224. package/server/src/Notifications/ProlongedStoppage.php +2 -2
  225. package/server/src/Notifications/RouteDeviation.php +2 -2
  226. package/server/src/Observers/ContactObserver.php +2 -0
  227. package/server/src/Observers/PurchaseRateObserver.php +20 -0
  228. package/server/src/Observers/ServiceRateObserver.php +6 -6
  229. package/server/src/Observers/ZoneObserver.php +56 -0
  230. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +1 -1
  231. package/server/src/Providers/FleetOpsServiceProvider.php +2 -0
  232. package/server/src/Support/Analytics/AbstractAnalytics.php +6 -6
  233. package/server/src/Support/Analytics/FuelEfficiency.php +2 -2
  234. package/server/src/Support/Analytics/OrdersByStatus.php +1 -1
  235. package/server/src/Support/Analytics/RevenueTrend.php +0 -1
  236. package/server/src/Support/Analytics/TopDrivers.php +10 -2
  237. package/server/src/Support/CustomerAuth.php +98 -0
  238. package/server/src/Support/GettingStarted.php +144 -0
  239. package/server/src/Support/LabelPdf.php +52 -0
  240. package/server/src/Support/Metrics/AbstractMetric.php +4 -4
  241. package/server/src/Support/PlaceSearch.php +282 -0
  242. package/server/src/Support/ResolvesOrderServiceStops.php +522 -0
  243. package/server/src/Tracking/TrackingContextBuilder.php +41 -38
  244. package/server/src/Tracking/TrackingIntelligenceService.php +109 -9
  245. package/server/src/Tracking/TrackingStop.php +13 -11
  246. package/server/src/routes.php +76 -10
  247. package/server/tests/AnalyticsRoutesTest.php +8 -0
  248. package/server/tests/CapacityAllocationEngineTest.php +10 -10
  249. package/server/tests/CustomerContactIdentitySafetyTest.php +95 -0
  250. package/server/tests/CustomerEndpointTest.php +199 -0
  251. package/server/tests/DriverVehicleStatusDefaultTest.php +39 -0
  252. package/server/tests/GettingStartedStatusTest.php +21 -0
  253. package/server/tests/LabelPdfRenderingTest.php +23 -0
  254. package/server/tests/LiveControllerViewportTest.php +60 -0
  255. package/server/tests/MetricsRegistryTest.php +4 -8
  256. package/server/tests/OrchestrationPayloadBuilderTest.php +2 -2
  257. package/server/tests/OrderActivityFlowRegressionTest.php +116 -0
  258. package/server/tests/OrderConfigEndpointTest.php +86 -0
  259. package/server/tests/OrderFileAttachmentTest.php +20 -0
  260. package/server/tests/ServiceAreaZoneApiShapeTest.php +78 -0
  261. package/server/tests/VehicleDriverAssignmentTest.php +18 -0
  262. package/translations/ar-ae.yml +9 -0
  263. package/translations/bg-bg.yaml +9 -0
  264. package/translations/en-us.yaml +12 -0
  265. package/translations/es-pa.yaml +9 -0
  266. package/translations/fr-fr.yaml +9 -0
  267. package/translations/mn-mn.yaml +9 -0
  268. package/translations/pt-br.yaml +9 -0
  269. package/translations/ru-ru.yaml +9 -0
  270. package/translations/vi-vn.yaml +9 -0
  271. package/server/migrations/2026_05_13_000001_repair_customer_contact_user_types.php +0 -86
@@ -3,13 +3,12 @@
3
3
  namespace Fleetbase\FleetOps\Models;
4
4
 
5
5
  use Fleetbase\Casts\Json;
6
+ use Fleetbase\FleetOps\Exceptions\CustomerUserConflictException;
6
7
  use Fleetbase\FleetOps\Exceptions\UserAlreadyExistsException;
7
8
  use Fleetbase\FleetOps\Support\Utils;
8
9
  use Fleetbase\Models\CompanyUser;
9
- use Fleetbase\Models\Invite;
10
10
  use Fleetbase\Models\Model;
11
11
  use Fleetbase\Models\User;
12
- use Fleetbase\Notifications\UserInvited;
13
12
  use Fleetbase\Traits\HasApiModelBehavior;
14
13
  use Fleetbase\Traits\HasApiModelCache;
15
14
  use Fleetbase\Traits\HasCustomFields;
@@ -300,15 +299,15 @@ class Contact extends Model
300
299
  }
301
300
 
302
301
  /**
303
- * Creates a new user from the given contact and optionally sends an invitation.
302
+ * Creates a new user from the given contact without sending an organization invitation.
304
303
  *
305
304
  * This method first checks if a user with the same email or phone number as the contact already exists.
306
305
  * If such a user exists, it throws a UserAlreadyExistsException. Otherwise, it proceeds to create a new
307
306
  * user record using the contact's details, assigns the user to the contact's company, and links the
308
- * user to the contact. Optionally, it sends an invitation to the newly created user via email.
307
+ * user to the contact.
309
308
  *
310
309
  * @param Contact $contact the contact instance from which the user should be created
311
- * @param bool $sendInvite (optional) Whether to send an invitation to the newly created user. Default is false.
310
+ * @param bool $sendInvite Deprecated and ignored. Contact and customer users do not receive organization invitations.
312
311
  *
313
312
  * @return User the newly created user instance
314
313
  *
@@ -319,7 +318,12 @@ class Contact extends Model
319
318
  // Check if user already exist with email or phone number
320
319
  $existingUser = null;
321
320
  if ($contact->email || $contact->phone) {
322
- $existingUser = User::where('company_uuid', $contact->company_uuid)
321
+ $existingUser = User::where(function ($query) use ($contact) {
322
+ $query->where('company_uuid', $contact->company_uuid)
323
+ ->orWhereHas('companyUsers', function ($query) use ($contact) {
324
+ $query->where('company_uuid', $contact->company_uuid);
325
+ });
326
+ })
323
327
  ->where(function ($query) use ($contact) {
324
328
  if ($contact->email) {
325
329
  $query->where('email', $contact->email);
@@ -335,15 +339,15 @@ class Contact extends Model
335
339
  }
336
340
 
337
341
  if ($existingUser) {
342
+ if ($contact->isCustomer()) {
343
+ $contact->assertCustomerUserCanBeAssigned($existingUser);
344
+ }
345
+
338
346
  // Check if existing user belongs to another contact
339
347
  $existingUserContact = Contact::where(['user_uuid' => $existingUser->uuid, 'company_uuid' => $contact->company_uuid])->whereHas('user')->first();
340
348
  if ($existingUserContact) {
341
349
  throw new UserAlreadyExistsException('User already exists, try to assigning the user to this contact.', $existingUser);
342
350
  } else {
343
- if ($contact->isCustomer()) {
344
- $contact->normalizeCustomerUser($existingUser);
345
- }
346
-
347
351
  // Assign the user to this contact instead
348
352
  $contact->setAttribute('user_uuid', $existingUser->uuid);
349
353
  if ($update) {
@@ -373,8 +377,8 @@ class Contact extends Model
373
377
  // Set user type
374
378
  $user->setType($contact->type);
375
379
 
376
- // Assing to company
377
- $user->assignCompany($contact->company, $contact->isCustomer() ? 'Fleet-Ops Customer' : 'Fleet-Ops Contact');
380
+ // Assign to company without triggering core organization invitations.
381
+ static::assignUserToContactCompany($contact, $user);
378
382
 
379
383
  // Assign customer role
380
384
  if ($contact->isCustomer()) {
@@ -387,23 +391,6 @@ class Contact extends Model
387
391
  $contact->update(['user_uuid' => $user->uuid]);
388
392
  }
389
393
 
390
- // Optionally, send invite
391
- if ($sendInvite) {
392
- // send invitation to user
393
- $invitation = Invite::create([
394
- 'company_uuid' => $user->company_uuid,
395
- 'created_by_uuid' => session('user'),
396
- 'subject_uuid' => $user->company_uuid,
397
- 'subject_type' => Utils::getMutationType('company'),
398
- 'protocol' => 'email',
399
- 'recipients' => [$user->email],
400
- 'reason' => 'join_company',
401
- ]);
402
-
403
- // notify user
404
- $user->notify(new UserInvited($invitation));
405
- }
406
-
407
394
  $contact->setRelation('user', $user);
408
395
 
409
396
  return $user;
@@ -423,13 +410,7 @@ class Contact extends Model
423
410
  return null;
424
411
  }
425
412
 
426
- if ($user->type !== 'customer') {
427
- if ($quiet) {
428
- $user->forceFill(['type' => 'customer'])->saveQuietly();
429
- } else {
430
- $user->setType('customer');
431
- }
432
- }
413
+ $this->assertCustomerUserCanBeAssigned($user);
433
414
 
434
415
  $this->loadMissing('company');
435
416
  if ($this->company) {
@@ -463,7 +444,7 @@ class Contact extends Model
463
444
  public function repairCustomerTypeInvariant(bool $quiet = false): ?User
464
445
  {
465
446
  if (!$this->isCustomer()) {
466
- $this->forceFill(['type' => 'customer'])->saveQuietly();
447
+ return $this->getUser();
467
448
  }
468
449
 
469
450
  $user = $this->getUser();
@@ -491,33 +472,41 @@ class Contact extends Model
491
472
  }
492
473
 
493
474
  /**
494
- * Assigns a user to the company and optionally sends an invitation email.
475
+ * Assigns a user to the contact's company without sending an organization invitation.
495
476
  *
496
477
  * This method performs the following actions:
497
478
  * 1. Associates the provided user with the current company, assigning them a role based on the
498
479
  * company's type (e.g., 'Fleet-Ops Customer' or 'Fleet-Ops Contact').
499
480
  * 2. If the user is of type 'customer', assigns the 'Fleet-Ops Customer' role specifically.
500
481
  * 3. Updates the current entity's `user_uuid` to reference the assigned user.
501
- * 4. Optionally sends an invitation email to the user, inviting them to join the company.
502
482
  *
503
483
  * @param User $user the user to be assigned to the company
504
- * @param bool $sendInvite Determines whether to send an invitation email to the user. Defaults to false.
484
+ * @param bool $sendInvite Deprecated and ignored. Contact and customer users do not receive organization invitations.
505
485
  *
506
486
  * @return self returns the current instance to allow method chaining
507
487
  *
508
- * @throws \Exception if an error occurs during user assignment or while sending the invitation
488
+ * @throws \Exception if an error occurs during user assignment
509
489
  */
510
490
  public function assignUser(User $user, bool $sendInvite = false): self
511
491
  {
512
492
  // Load company
513
493
  $this->loadMissing('company');
514
494
 
515
- if ($this->isCustomer() && $user->type !== 'customer') {
516
- $user->setType('customer');
495
+ if ($this->isCustomer()) {
496
+ $this->assertCustomerUserCanBeAssigned($user);
497
+
498
+ $existingUserContact = static::where(['user_uuid' => $user->uuid, 'company_uuid' => $this->company_uuid])
499
+ ->whereNot('uuid', $this->uuid)
500
+ ->whereHas('user')
501
+ ->first();
502
+
503
+ if ($existingUserContact) {
504
+ throw new UserAlreadyExistsException('User already exists, try to assigning the user to this contact.', $user);
505
+ }
517
506
  }
518
507
 
519
- // Assing to company
520
- $user->assignCompany($this->company, $this->isCustomer() ? 'Fleet-Ops Customer' : 'Fleet-Ops Contact');
508
+ // Assign to company without triggering core organization invitations.
509
+ static::assignUserToContactCompany($this, $user);
521
510
 
522
511
  // Get the company user instance
523
512
  $companyUser = $user->getCompanyUser($this->company);
@@ -529,28 +518,79 @@ class Contact extends Model
529
518
  // Set user to contact
530
519
  $this->update(['user_uuid' => $user->uuid]);
531
520
 
532
- // Optionally, send invite
533
- if ($sendInvite) {
534
- // send invitation to user
535
- $invitation = Invite::create([
536
- 'company_uuid' => $user->company_uuid,
537
- 'created_by_uuid' => session('user'),
538
- 'subject_uuid' => $user->company_uuid,
539
- 'subject_type' => Utils::getMutationType('company'),
540
- 'protocol' => 'email',
541
- 'recipients' => [$user->email],
542
- 'reason' => 'join_company',
543
- ]);
544
-
545
- // notify user
546
- $user->notify(new UserInvited($invitation));
547
- }
548
-
549
521
  $this->setRelation('user', $user);
550
522
 
551
523
  return $this;
552
524
  }
553
525
 
526
+ public function assertCustomerIdentityIsAvailable(): void
527
+ {
528
+ if (!$this->isCustomer()) {
529
+ return;
530
+ }
531
+
532
+ $assignedUser = $this->getAssignedUserWithoutTypeScope();
533
+ if ($assignedUser) {
534
+ $this->assertCustomerUserCanBeAssigned($assignedUser);
535
+ }
536
+
537
+ $matchedUser = $this->findExistingUserByIdentity();
538
+ if ($matchedUser && $matchedUser->uuid !== $this->user_uuid) {
539
+ $this->assertCustomerUserCanBeAssigned($matchedUser);
540
+ }
541
+ }
542
+
543
+ public function assertCustomerUserCanBeAssigned(User $user): void
544
+ {
545
+ if ($user->type === 'customer') {
546
+ return;
547
+ }
548
+
549
+ throw new CustomerUserConflictException(static::customerUserConflictMessage($user), $user);
550
+ }
551
+
552
+ public static function customerUserConflictMessage(?User $user = null): string
553
+ {
554
+ $field = $user?->email ? 'email' : ($user?->phone ? 'phone number' : 'user account');
555
+
556
+ return 'This ' . $field . ' belongs to an existing staff user and cannot be used for a customer account.';
557
+ }
558
+
559
+ private function findExistingUserByIdentity(): ?User
560
+ {
561
+ if (!$this->email && !$this->phone) {
562
+ return null;
563
+ }
564
+
565
+ return User::where(function ($query) {
566
+ $query->where('company_uuid', $this->company_uuid)
567
+ ->orWhereHas('companyUsers', function ($query) {
568
+ $query->where('company_uuid', $this->company_uuid);
569
+ });
570
+ })
571
+ ->where(function ($query) {
572
+ if ($this->email) {
573
+ $query->where('email', $this->email);
574
+ }
575
+
576
+ if ($this->phone) {
577
+ $method = $this->email ? 'orWhere' : 'where';
578
+ $query->{$method}('phone', Utils::formatPhoneNumber($this->phone));
579
+ }
580
+ })
581
+ ->whereNull('deleted_at')
582
+ ->first();
583
+ }
584
+
585
+ private function getAssignedUserWithoutTypeScope(): ?User
586
+ {
587
+ if (!Str::isUuid($this->user_uuid)) {
588
+ return null;
589
+ }
590
+
591
+ return User::where('uuid', $this->user_uuid)->first();
592
+ }
593
+
554
594
  public function syncWithUser(): bool
555
595
  {
556
596
  $updates = [];
@@ -580,12 +620,11 @@ class Contact extends Model
580
620
  }
581
621
 
582
622
  /**
583
- * Creates a new user from the current contact instance and optionally sends an invitation.
623
+ * Creates a new user from the current contact instance without sending an organization invitation.
584
624
  *
585
- * This method is a wrapper around the createUserFromContact method. It allows creating a user directly
586
- * from a contact instance and, optionally, sending an invitation to the newly created user.
625
+ * This method is a wrapper around the createUserFromContact method.
587
626
  *
588
- * @param bool $sendInvite (optional) Whether to send an invitation to the newly created user. Default is false.
627
+ * @param bool $sendInvite Deprecated and ignored. Contact and customer users do not receive organization invitations.
589
628
  *
590
629
  * @return User the newly created user instance
591
630
  */
@@ -632,4 +671,21 @@ class Contact extends Model
632
671
  {
633
672
  return !$this->hasUser();
634
673
  }
674
+
675
+ private static function assignUserToContactCompany(Contact $contact, User $user): ?CompanyUser
676
+ {
677
+ $contact->loadMissing('company');
678
+
679
+ if (!$contact->company) {
680
+ return null;
681
+ }
682
+
683
+ $role = $contact->isCustomer() ? 'Fleet-Ops Customer' : 'Fleet-Ops Contact';
684
+ $companyUser = $contact->company->addUser($user, $role);
685
+
686
+ $user->forceFill(['company_uuid' => $contact->company->uuid])->saveQuietly();
687
+ $user->setRelation('companyUser', $companyUser);
688
+
689
+ return $companyUser;
690
+ }
635
691
  }
@@ -0,0 +1,64 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Models;
4
+
5
+ use Illuminate\Database\Eloquent\Builder;
6
+ use Illuminate\Database\Eloquent\Relations\HasMany;
7
+ use Illuminate\Support\Str;
8
+
9
+ /**
10
+ * Fleet-Ops customer.
11
+ *
12
+ * A thin specialization of {@see Contact} that is scoped to rows with
13
+ * `type='customer'` and forces that type on create. This is what the public
14
+ * customer API surface (`/v1/customers/...`) operates on.
15
+ *
16
+ * Mirrors {@see \Fleetbase\Storefront\Models\Customer} but lives directly in
17
+ * FleetOps so that customer auth + customer-scoped orders work without the
18
+ * Storefront publishable-key + Store/Network gating.
19
+ */
20
+ class Customer extends Contact
21
+ {
22
+ /**
23
+ * The key to use in the payload responses.
24
+ */
25
+ protected string $payloadKey = 'customer';
26
+
27
+ /**
28
+ * Boot the model.
29
+ */
30
+ protected static function boot()
31
+ {
32
+ parent::boot();
33
+
34
+ static::creating(function ($model) {
35
+ $model->type = 'customer';
36
+ });
37
+
38
+ static::addGlobalScope('type', function (Builder $builder) {
39
+ $builder->where('type', 'customer');
40
+ });
41
+ }
42
+
43
+ /**
44
+ * Orders owned by this customer.
45
+ */
46
+ public function orders(): HasMany
47
+ {
48
+ return $this->hasMany(Order::class, 'customer_uuid')
49
+ ->whereNull('deleted_at')
50
+ ->withoutGlobalScopes();
51
+ }
52
+
53
+ /**
54
+ * Find a customer by either a `customer_xxx` or `contact_xxx` public id.
55
+ */
56
+ public static function findFromCustomerId(string $publicId): ?self
57
+ {
58
+ if (Str::startsWith($publicId, 'customer')) {
59
+ $publicId = Str::replaceFirst('customer', 'contact', $publicId);
60
+ }
61
+
62
+ return static::where('public_id', $publicId)->first();
63
+ }
64
+ }
@@ -119,6 +119,15 @@ class Driver extends Model
119
119
  'time_window_end',
120
120
  ];
121
121
 
122
+ /**
123
+ * Set attributes and defaults.
124
+ *
125
+ * @var array
126
+ */
127
+ protected $attributes = [
128
+ 'status' => 'available',
129
+ ];
130
+
122
131
  /**
123
132
  * The attributes that are guarded and not mass assignable.
124
133
  *
@@ -595,9 +604,9 @@ class Driver extends Model
595
604
  /**
596
605
  * Set the driver status attribute.
597
606
  */
598
- public function setStatusAttribute(?string $status = 'active'): void
607
+ public function setStatusAttribute(?string $status = 'available'): void
599
608
  {
600
- $this->attributes['status'] = $status;
609
+ $this->attributes['status'] = $status === null || $status === 'active' ? 'available' : $status;
601
610
  }
602
611
 
603
612
  /**
@@ -854,7 +863,7 @@ class Driver extends Model
854
863
  'user_uuid' => $user->uuid,
855
864
  'drivers_license_number' => $driversLicenseNumber,
856
865
  'country' => $country,
857
- 'status' => 'active',
866
+ 'status' => 'available',
858
867
  'location' => Utils::parsePointToWkt(new Point(0, 0)),
859
868
  ]);
860
869
 
@@ -2,9 +2,9 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Models;
4
4
 
5
- use Barryvdh\DomPDF\Facade\Pdf;
6
5
  use Fleetbase\Casts\Json;
7
6
  use Fleetbase\Casts\PolymorphicType;
7
+ use Fleetbase\FleetOps\Support\LabelPdf;
8
8
  use Fleetbase\FleetOps\Support\Utils;
9
9
  use Fleetbase\FleetOps\Traits\HasTrackingNumber;
10
10
  use Fleetbase\FleetOps\Traits\PayloadAccessors;
@@ -142,7 +142,7 @@ class Entity extends Model
142
142
  */
143
143
  public function pdfLabel()
144
144
  {
145
- return Pdf::loadHTML($this->label());
145
+ return LabelPdf::fromHtml($this->label());
146
146
  }
147
147
 
148
148
  /**
@@ -14,7 +14,11 @@ use Fleetbase\Traits\HasCustomFields;
14
14
  use Fleetbase\Traits\HasPublicId;
15
15
  use Fleetbase\Traits\HasUuid;
16
16
  use Fleetbase\Traits\TracksApiCredential;
17
+ use Illuminate\Database\Eloquent\Relations\BelongsTo;
18
+ use Illuminate\Database\Eloquent\Relations\HasMany;
17
19
  use Illuminate\Support\Str;
20
+ use Spatie\Activitylog\LogOptions;
21
+ use Spatie\Activitylog\Traits\LogsActivity;
18
22
 
19
23
  class Issue extends Model
20
24
  {
@@ -24,6 +28,7 @@ class Issue extends Model
24
28
  use SpatialTrait;
25
29
  use HasApiModelBehavior;
26
30
  use HasApiModelCache;
31
+ use LogsActivity;
27
32
  use HasCustomFields;
28
33
 
29
34
  /**
@@ -60,6 +65,7 @@ class Issue extends Model
60
65
  'assigned_to_uuid',
61
66
  'vehicle_uuid',
62
67
  'driver_uuid',
68
+ 'order_uuid',
63
69
  'issue_id',
64
70
  'location',
65
71
  'category',
@@ -106,6 +112,31 @@ class Issue extends Model
106
112
  */
107
113
  protected $appends = ['driver_name', 'vehicle_name', 'assignee_name', 'reporter_name'];
108
114
 
115
+ /**
116
+ * Get the activity log options for the model.
117
+ */
118
+ public function getActivitylogOptions(): LogOptions
119
+ {
120
+ return LogOptions::defaults()
121
+ ->logOnly([
122
+ 'title',
123
+ 'report',
124
+ 'status',
125
+ 'priority',
126
+ 'assigned_to_uuid',
127
+ 'vehicle_uuid',
128
+ 'driver_uuid',
129
+ 'order_uuid',
130
+ 'type',
131
+ 'category',
132
+ 'tags',
133
+ 'resolved_at',
134
+ 'meta',
135
+ ])
136
+ ->logOnlyDirty()
137
+ ->dontSubmitEmptyLogs();
138
+ }
139
+
109
140
  /**
110
141
  * The attributes excluded from the model's JSON form.
111
142
  *
@@ -114,7 +145,7 @@ class Issue extends Model
114
145
  protected $hidden = [];
115
146
 
116
147
  /**
117
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
148
+ * @return BelongsTo
118
149
  */
119
150
  public function reportedBy()
120
151
  {
@@ -122,7 +153,7 @@ class Issue extends Model
122
153
  }
123
154
 
124
155
  /**
125
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
156
+ * @return BelongsTo
126
157
  */
127
158
  public function reporter()
128
159
  {
@@ -130,7 +161,7 @@ class Issue extends Model
130
161
  }
131
162
 
132
163
  /**
133
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
164
+ * @return BelongsTo
134
165
  */
135
166
  public function assignedTo()
136
167
  {
@@ -138,7 +169,7 @@ class Issue extends Model
138
169
  }
139
170
 
140
171
  /**
141
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
172
+ * @return BelongsTo
142
173
  */
143
174
  public function assignee()
144
175
  {
@@ -146,7 +177,7 @@ class Issue extends Model
146
177
  }
147
178
 
148
179
  /**
149
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
180
+ * @return BelongsTo
150
181
  */
151
182
  public function vehicle()
152
183
  {
@@ -154,13 +185,23 @@ class Issue extends Model
154
185
  }
155
186
 
156
187
  /**
157
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
188
+ * @return BelongsTo
158
189
  */
159
190
  public function driver()
160
191
  {
161
192
  return $this->belongsTo(Driver::class);
162
193
  }
163
194
 
195
+ public function order(): BelongsTo
196
+ {
197
+ return $this->belongsTo(Order::class, 'order_uuid', 'uuid')->without(['comments', 'files']);
198
+ }
199
+
200
+ public function files(): HasMany
201
+ {
202
+ return $this->hasMany(\Fleetbase\Models\File::class, 'subject_uuid', 'uuid')->latest();
203
+ }
204
+
164
205
  /**
165
206
  * Set the title attribute for the Issue model.
166
207
  *