@fleetbase/fleetops-engine 0.6.60 → 0.6.62

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 (109) hide show
  1. package/RELEASE.md +21 -17
  2. package/addon/components/ai/route-preview-map.js +1 -5
  3. package/addon/components/customer/orders.js +2 -1
  4. package/addon/components/driver/card.hbs +49 -0
  5. package/addon/components/driver/card.js +30 -0
  6. package/addon/components/map/leaflet-live-map.js +4 -19
  7. package/addon/components/modals/place-details.hbs +1 -1
  8. package/addon/components/modals/point-map.hbs +1 -1
  9. package/addon/components/orchestrator-workbench.js +2 -1
  10. package/addon/components/order/form/route.js +3 -0
  11. package/addon/components/order-tracking-lookup.js +2 -1
  12. package/addon/components/place/details.hbs +1 -1
  13. package/addon/components/positions-replay.js +4 -1
  14. package/addon/components/service-area/details.hbs +1 -1
  15. package/addon/components/widget/live-fleet.js +7 -7
  16. package/addon/components/zone/details.hbs +1 -1
  17. package/addon/controllers/management/drivers/index.js +22 -1
  18. package/addon/controllers/settings/map.js +16 -0
  19. package/addon/helpers/leaflet-tile-url.js +19 -0
  20. package/addon/services/map-settings.js +26 -1
  21. package/addon/templates/management/drivers/index.hbs +67 -33
  22. package/addon/templates/settings/map.hbs +10 -0
  23. package/addon/utils/leaflet-tile-url.js +31 -0
  24. package/addon/utils/prepare-place-for-save.js +13 -0
  25. package/app/components/driver/card.js +1 -0
  26. package/app/helpers/leaflet-tile-url.js +1 -0
  27. package/app/utils/leaflet-tile-url.js +1 -0
  28. package/composer.json +1 -1
  29. package/extension.json +1 -1
  30. package/index.js +11 -0
  31. package/package.json +2 -1
  32. package/server/migrations/2026_09_05_000001_add_unique_indexes_to_fleet_membership_pivots.php +129 -0
  33. package/server/src/Exceptions/PublicRelationNotFoundException.php +47 -0
  34. package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesFleetOpsApiResources.php +42 -7
  35. package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesPublicExpansions.php +187 -0
  36. package/server/src/Http/Controllers/Api/v1/DriverController.php +312 -59
  37. package/server/src/Http/Controllers/Api/v1/EntityController.php +1 -1
  38. package/server/src/Http/Controllers/Api/v1/FleetController.php +370 -33
  39. package/server/src/Http/Controllers/Api/v1/FuelReportController.php +1 -1
  40. package/server/src/Http/Controllers/Api/v1/IssueController.php +1 -1
  41. package/server/src/Http/Controllers/Api/v1/ManifestController.php +250 -0
  42. package/server/src/Http/Controllers/Api/v1/OrderController.php +1 -1
  43. package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +1 -1
  44. package/server/src/Http/Controllers/Api/v1/TrackingStatusController.php +1 -1
  45. package/server/src/Http/Controllers/Api/v1/VehicleController.php +139 -34
  46. package/server/src/Http/Controllers/Api/v1/ZoneController.php +1 -1
  47. package/server/src/Http/Controllers/Internal/v1/SettingController.php +22 -0
  48. package/server/src/Http/Filter/Concerns/ResolvesPublicRelationUuids.php +54 -0
  49. package/server/src/Http/Filter/DriverFilter.php +71 -15
  50. package/server/src/Http/Filter/FleetFilter.php +32 -33
  51. package/server/src/Http/Filter/FuelReportFilter.php +29 -0
  52. package/server/src/Http/Filter/IssueFilter.php +29 -0
  53. package/server/src/Http/Filter/VehicleFilter.php +57 -19
  54. package/server/src/Http/Requests/Concerns/ScopesPublicRelationRules.php +48 -0
  55. package/server/src/Http/Requests/CreateDriverRequest.php +100 -9
  56. package/server/src/Http/Requests/CreateFleetRequest.php +30 -2
  57. package/server/src/Http/Requests/CreateServiceRateRequest.php +7 -2
  58. package/server/src/Http/Requests/CreateVehicleRequest.php +150 -26
  59. package/server/src/Http/Resources/v1/Concerns/ResolvesPublicRelationFields.php +110 -0
  60. package/server/src/Http/Resources/v1/Driver.php +22 -2
  61. package/server/src/Http/Resources/v1/Fleet.php +38 -25
  62. package/server/src/Http/Resources/v1/Manifest.php +52 -0
  63. package/server/src/Http/Resources/v1/ManifestStop.php +47 -0
  64. package/server/src/Http/Resources/v1/OrderConfig.php +51 -4
  65. package/server/src/Http/Resources/v1/Vehicle.php +46 -4
  66. package/server/src/Models/Driver.php +2 -2
  67. package/server/src/Models/Place.php +1 -1
  68. package/server/src/routes.php +29 -0
  69. package/server/tests/ApiDriverControllerContractsTest.php +317 -13
  70. package/server/tests/ApiDriverPasswordContractsTest.php +457 -0
  71. package/server/tests/ApiEntityControllerContractsTest.php +1 -1
  72. package/server/tests/ApiEquipmentControllerContractsTest.php +2 -2
  73. package/server/tests/ApiFleetControllerContractsTest.php +354 -21
  74. package/server/tests/ApiFuelReportControllerContractsTest.php +1 -1
  75. package/server/tests/ApiIssueControllerContractsTest.php +1 -1
  76. package/server/tests/ApiManifestControllerContractsTest.php +599 -0
  77. package/server/tests/ApiOrderControllerContractsTest.php +1 -1
  78. package/server/tests/ApiPartControllerContractsTest.php +2 -2
  79. package/server/tests/ApiServiceAreaZoneControllerContractsTest.php +2 -2
  80. package/server/tests/ApiTrackingStatusControllerContractsTest.php +1 -1
  81. package/server/tests/ApiVehicleControllerContractsTest.php +210 -8
  82. package/server/tests/CompactResourceSerializationTest.php +13 -7
  83. package/server/tests/ControllerFilterContractsTest.php +132 -13
  84. package/server/tests/ControllerHelperContractsTest.php +6 -3
  85. package/server/tests/DriverFilterExecutionTest.php +130 -6
  86. package/server/tests/ExceptionContractsTest.php +15 -0
  87. package/server/tests/Feature/Http/Api/DeviceControllerContractsTest.php +1 -1
  88. package/server/tests/Feature/Http/Api/DriverControllerSessionHelpersTest.php +61 -0
  89. package/server/tests/Feature/Http/Api/FleetMembershipMigrationTest.php +140 -0
  90. package/server/tests/Feature/Http/Api/FleetMembershipTest.php +333 -0
  91. package/server/tests/Feature/Http/Api/FleetPublicContractTest.php +439 -0
  92. package/server/tests/Feature/Http/Api/FuelTransactionControllerContractsTest.php +2 -2
  93. package/server/tests/Feature/Http/Api/NullRelationshipInputTest.php +191 -0
  94. package/server/tests/Feature/Http/Api/PublicIdentifierFilterTest.php +293 -0
  95. package/server/tests/Feature/Http/Api/SmallApiControllerHelpersTest.php +7 -1
  96. package/server/tests/Feature/Http/Api/VehicleControllerTrackingTest.php +3 -1
  97. package/server/tests/Feature/Http/Api/VehiclePublicContractTest.php +454 -0
  98. package/server/tests/Feature/Http/Api/WorkOrderControllerContractsTest.php +1 -1
  99. package/server/tests/Feature/Http/Internal/PlaceControllerTest.php +45 -0
  100. package/server/tests/FleetHierarchyResourceTest.php +56 -8
  101. package/server/tests/RequestContractsTest.php +105 -7
  102. package/server/tests/SettingControllerContractsTest.php +33 -0
  103. package/server/tests/Unit/Http/Filter/FuelReportFilterTest.php +135 -0
  104. package/server/tests/Unit/Http/Filter/IssueFilterTest.php +43 -0
  105. package/server/tests/Unit/Http/Resources/FleetResourceTest.php +50 -6
  106. package/server/tests/Unit/Http/Resources/ManifestResourceTest.php +129 -0
  107. package/server/tests/Unit/Http/Resources/OrderConfigResourceTest.php +91 -0
  108. package/server/tests/VehicleControllerHelperContractsTest.php +1 -1
  109. package/translations/en-us.yaml +4 -0
@@ -2,14 +2,21 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Http\Filter;
4
4
 
5
+ use Fleetbase\FleetOps\Http\Filter\Concerns\ResolvesPublicRelationUuids;
6
+ use Fleetbase\FleetOps\Models\Fleet;
5
7
  use Fleetbase\FleetOps\Models\Place;
8
+ use Fleetbase\FleetOps\Models\Vehicle;
9
+ use Fleetbase\FleetOps\Models\Vendor;
6
10
  use Fleetbase\FleetOps\Support\Utils;
7
11
  use Fleetbase\Http\Filter\Filter;
8
12
  use Fleetbase\Models\Company;
13
+ use Fleetbase\Support\Http;
9
14
  use Illuminate\Support\Str;
10
15
 
11
16
  class DriverFilter extends Filter
12
17
  {
18
+ use ResolvesPublicRelationUuids;
19
+
13
20
  public function queryForInternal()
14
21
  {
15
22
  $this->builder->where(
@@ -43,9 +50,28 @@ class DriverFilter extends Filter
43
50
  });
44
51
  }
45
52
 
53
+ /**
54
+ * Match a driver by the identifier the operator's own system uses.
55
+ *
56
+ * Two callers, two meanings. The Fleet-Ops console types into a search box
57
+ * and expects `VEH-10` to find `VEH-100`; an importer asks whether `VEH-10`
58
+ * exists and must not be told yes because `VEH-100` does. A partial match
59
+ * there produces a duplicate on every rerun.
60
+ *
61
+ * Core's ordinary fillable-column filtering is already equality — this
62
+ * method exists only because the console needs the looser behaviour, so it
63
+ * is the console that gets the exception. An unknown request context is
64
+ * treated as public: exact is the safe default of the two.
65
+ */
46
66
  public function internalId(?string $internalId)
47
67
  {
48
- $this->builder->searchWhere('internal_id', $internalId);
68
+ if (Http::isInternalRequest($this->request)) {
69
+ $this->builder->searchWhere('internal_id', $internalId);
70
+
71
+ return;
72
+ }
73
+
74
+ $this->builder->where('internal_id', '=', $internalId);
49
75
  }
50
76
 
51
77
  public function name(?string $name)
@@ -58,14 +84,25 @@ class DriverFilter extends Filter
58
84
  );
59
85
  }
60
86
 
87
+ /**
88
+ * A public id identifies exactly one record, so a public lookup matches it
89
+ * exactly. The console keeps the partial search its id column filter box
90
+ * has always had.
91
+ */
61
92
  public function publicId(?string $publicId)
62
93
  {
63
- $this->builder->searchWhere('public_id', $publicId);
94
+ if (Http::isInternalRequest($this->request)) {
95
+ $this->builder->searchWhere('public_id', $publicId);
96
+
97
+ return;
98
+ }
99
+
100
+ $this->builder->where('public_id', '=', $publicId);
64
101
  }
65
102
 
66
103
  public function facilitator(string $facilitator)
67
104
  {
68
- $this->builder->where('vendor_uuid', $facilitator);
105
+ $this->builder->whereIn('vendor_uuid', $this->resolvePublicRelationUuids(Vendor::class, $facilitator));
69
106
  }
70
107
 
71
108
  public function vehicle(string $vehicle)
@@ -76,16 +113,28 @@ class DriverFilter extends Filter
76
113
  return;
77
114
  }
78
115
 
79
- if (Str::isUuid($vehicle)) {
116
+ // The console passes a uuid; the public API passes a public or internal id.
117
+ if (Str::isUuid($vehicle) && Http::isInternalRequest($this->request)) {
80
118
  $this->builder->where('vehicle_uuid', $vehicle);
81
- } else {
82
- $this->builder->whereHas(
83
- 'vehicle',
84
- function ($query) use ($vehicle) {
85
- $query->search($vehicle);
86
- }
87
- );
119
+
120
+ return;
121
+ }
122
+
123
+ $vehicleUuids = $this->resolvePublicRelationUuids(Vehicle::class, $vehicle);
124
+
125
+ if ($vehicleUuids !== []) {
126
+ $this->builder->whereIn('vehicle_uuid', $vehicleUuids);
127
+
128
+ return;
88
129
  }
130
+
131
+ // Fall back to a search so a partial plate or model still narrows a list.
132
+ $this->builder->whereHas(
133
+ 'vehicle',
134
+ function ($query) use ($vehicle) {
135
+ $query->search($vehicle);
136
+ }
137
+ );
89
138
  }
90
139
 
91
140
  public function driversLicenseNumber(?string $driversLicenseNumber)
@@ -93,12 +142,17 @@ class DriverFilter extends Filter
93
142
  $this->builder->searchWhere('drivers_license_number', $driversLicenseNumber);
94
143
  }
95
144
 
145
+ /**
146
+ * `phone` is an accessor sourced from the linked user, not a relation — the
147
+ * previous `whereHas('phone')` asked Eloquent for a relation that does not
148
+ * exist and raised a 500 for every caller of `?phone=`.
149
+ */
96
150
  public function phone(string $phone)
97
151
  {
98
152
  $this->builder->whereHas(
99
- 'phone',
153
+ 'user',
100
154
  function ($query) use ($phone) {
101
- $query->search($phone);
155
+ $query->searchWhere('phone', $phone);
102
156
  }
103
157
  );
104
158
  }
@@ -127,10 +181,12 @@ class DriverFilter extends Filter
127
181
 
128
182
  public function fleet(string $fleet)
129
183
  {
184
+ $fleetUuids = $this->resolvePublicRelationUuids(Fleet::class, $fleet);
185
+
130
186
  $this->builder->whereHas(
131
187
  'fleets',
132
- function ($q) use ($fleet) {
133
- $q->where('fleet_uuid', $fleet);
188
+ function ($q) use ($fleetUuids) {
189
+ $q->whereIn('fleet_uuid', $fleetUuids);
134
190
  }
135
191
  );
136
192
  }
@@ -2,11 +2,19 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Http\Filter;
4
4
 
5
+ use Fleetbase\FleetOps\Http\Filter\Concerns\ResolvesPublicRelationUuids;
6
+ use Fleetbase\FleetOps\Models\Fleet;
7
+ use Fleetbase\FleetOps\Models\ServiceArea;
8
+ use Fleetbase\FleetOps\Models\Vendor;
9
+ use Fleetbase\FleetOps\Models\Zone;
5
10
  use Fleetbase\FleetOps\Support\Utils;
6
11
  use Fleetbase\Http\Filter\Filter;
12
+ use Fleetbase\Support\Http;
7
13
 
8
14
  class FleetFilter extends Filter
9
15
  {
16
+ use ResolvesPublicRelationUuids;
17
+
10
18
  public function queryForInternal()
11
19
  {
12
20
  $this->builder->where('company_uuid', $this->session->get('company'))->with(['serviceArea', 'zone']);
@@ -17,15 +25,17 @@ class FleetFilter extends Filter
17
25
  $this->builder->where('company_uuid', $this->session->get('company'));
18
26
  }
19
27
 
28
+ /**
29
+ * Free-text search across a fleet's own columns.
30
+ *
31
+ * Previously matched against a `user` relation, which Fleet does not have —
32
+ * every `?query=` on the fleets endpoint raised a relation-not-found error
33
+ * rather than returning results.
34
+ */
20
35
  public function query(?string $searchQuery)
21
36
  {
22
37
  $this->builder->where(function ($query) use ($searchQuery) {
23
- $query->orWhereHas(
24
- 'user',
25
- function ($query) use ($searchQuery) {
26
- $query->searchWhere(['name', 'email', 'phone'], $searchQuery);
27
- }
28
- );
38
+ $query->searchWhere(['name', 'task', 'public_id'], $searchQuery);
29
39
  });
30
40
  }
31
41
 
@@ -38,49 +48,38 @@ class FleetFilter extends Filter
38
48
 
39
49
  public function serviceArea(?string $serviceArea)
40
50
  {
41
- $this->builder->whereHas(
42
- 'serviceArea',
43
- function ($query) use ($serviceArea) {
44
- $query->where('uuid', $serviceArea);
45
- }
46
- );
51
+ $this->builder->whereIn('service_area_uuid', $this->resolvePublicRelationUuids(ServiceArea::class, $serviceArea));
47
52
  }
48
53
 
49
54
  public function zone(?string $zone)
50
55
  {
51
- $this->builder->whereHas(
52
- 'zone',
53
- function ($query) use ($zone) {
54
- $query->where('zone_uuid', $zone);
55
- }
56
- );
56
+ $this->builder->whereIn('zone_uuid', $this->resolvePublicRelationUuids(Zone::class, $zone));
57
57
  }
58
58
 
59
59
  public function parentFleet(?string $fleet)
60
60
  {
61
- $this->builder->whereHas(
62
- 'parent_fleet',
63
- function ($query) use ($fleet) {
64
- $query->where('uuid', $fleet);
65
- }
66
- );
67
-
68
- $this->builder->searchWhere('parent_fleet_uuid', $fleet);
61
+ $this->builder->whereIn('parent_fleet_uuid', $this->resolvePublicRelationUuids(Fleet::class, $fleet));
69
62
  }
70
63
 
71
64
  public function vendor(?string $vendor)
72
65
  {
73
- $this->builder->whereHas(
74
- 'vendor',
75
- function ($query) use ($vendor) {
76
- $query->where('uuid', $vendor);
77
- }
78
- );
66
+ $this->builder->whereIn('vendor_uuid', $this->resolvePublicRelationUuids(Vendor::class, $vendor));
79
67
  }
80
68
 
69
+ /**
70
+ * A public id identifies exactly one record, so a public lookup matches it
71
+ * exactly. The console keeps the partial search its id column filter box
72
+ * has always had.
73
+ */
81
74
  public function publicId(?string $publicId)
82
75
  {
83
- $this->builder->searchWhere('public_id', $publicId);
76
+ if (Http::isInternalRequest($this->request)) {
77
+ $this->builder->searchWhere('public_id', $publicId);
78
+
79
+ return;
80
+ }
81
+
82
+ $this->builder->where('public_id', '=', $publicId);
84
83
  }
85
84
 
86
85
  public function task(?string $task)
@@ -86,6 +86,35 @@ class FuelReportFilter extends Filter
86
86
  });
87
87
  }
88
88
 
89
+ /**
90
+ * Aliases for {@see driver()}.
91
+ *
92
+ * The base filter resolves a query parameter to a method of the same name
93
+ * and **silently ignores anything it cannot match**. So a client scoping a
94
+ * list with `driver_uuid` — the column's own name, and the name every other
95
+ * part of the payload uses — matched nothing, the filter was dropped, and
96
+ * the response came back scoped only by company: every driver's records,
97
+ * with a 200 and no indication that the request had been narrowed at all.
98
+ *
99
+ * For a driver app that is a disclosure, not a nuisance: a driver asking
100
+ * for their own fuel reports received the whole company's.
101
+ */
102
+ public function driverUuid(?string $driver)
103
+ {
104
+ $this->driver($driver);
105
+ }
106
+
107
+ public function driverAssigned(?string $driver)
108
+ {
109
+ $this->driver($driver);
110
+ }
111
+
112
+ /** Alias for {@see vehicle()}, for the same reason. */
113
+ public function vehicleUuid(?string $vehicle)
114
+ {
115
+ $this->vehicle($vehicle);
116
+ }
117
+
89
118
  public function vehicle(?string $vehicle)
90
119
  {
91
120
  $this->builder->whereHas('vehicle', function ($q) use ($vehicle) {
@@ -88,6 +88,35 @@ class IssueFilter extends Filter
88
88
  });
89
89
  }
90
90
 
91
+ /**
92
+ * Aliases for {@see driver()}.
93
+ *
94
+ * The base filter resolves a query parameter to a method of the same name
95
+ * and **silently ignores anything it cannot match**. So a client scoping a
96
+ * list with `driver_uuid` — the column's own name, and the name every other
97
+ * part of the payload uses — matched nothing, the filter was dropped, and
98
+ * the response came back scoped only by company: every driver's records,
99
+ * with a 200 and no indication that the request had been narrowed at all.
100
+ *
101
+ * For a driver app that is a disclosure, not a nuisance: a driver asking
102
+ * for their own fuel reports received the whole company's.
103
+ */
104
+ public function driverUuid(?string $driver)
105
+ {
106
+ $this->driver($driver);
107
+ }
108
+
109
+ public function driverAssigned(?string $driver)
110
+ {
111
+ $this->driver($driver);
112
+ }
113
+
114
+ /** Alias for {@see vehicle()}, for the same reason. */
115
+ public function vehicleUuid(?string $vehicle)
116
+ {
117
+ $this->vehicle($vehicle);
118
+ }
119
+
91
120
  public function vehicle(?string $vehicle)
92
121
  {
93
122
  $this->builder->whereHas('vehicle', function ($q) use ($vehicle) {
@@ -2,6 +2,9 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Http\Filter;
4
4
 
5
+ use Fleetbase\FleetOps\Http\Filter\Concerns\ResolvesPublicRelationUuids;
6
+ use Fleetbase\FleetOps\Models\Driver;
7
+ use Fleetbase\FleetOps\Models\Fleet;
5
8
  use Fleetbase\FleetOps\Models\Vehicle;
6
9
  use Fleetbase\FleetOps\Models\Vendor;
7
10
  use Fleetbase\FleetOps\Support\Utils;
@@ -10,6 +13,8 @@ use Fleetbase\Support\Http;
10
13
 
11
14
  class VehicleFilter extends Filter
12
15
  {
16
+ use ResolvesPublicRelationUuids;
17
+
13
18
  public function queryForInternal()
14
19
  {
15
20
  $this->builder->where('company_uuid', $this->session->get('company'));
@@ -30,14 +35,56 @@ class VehicleFilter extends Filter
30
35
  $this->builder->searchWhere(['year', 'make', 'model', 'plate_number'], $display_name);
31
36
  }
32
37
 
38
+ /**
39
+ * Match a vehicle by the identifier the operator's own system uses.
40
+ *
41
+ * `internal_id` is the column an importer keys on to decide whether a
42
+ * vehicle already exists, and it was the one identifier the filter did not
43
+ * support — so every lookup fell through to a create.
44
+ */
45
+ /**
46
+ * Match a vehicle by the identifier the operator's own system uses.
47
+ *
48
+ * Two callers, two meanings. The Fleet-Ops console types into a search box
49
+ * and expects `VEH-10` to find `VEH-100`; an importer asks whether `VEH-10`
50
+ * exists and must not be told yes because `VEH-100` does. A partial match
51
+ * there produces a duplicate on every rerun.
52
+ *
53
+ * Core's ordinary fillable-column filtering is already equality — this
54
+ * method exists only because the console needs the looser behaviour, so it
55
+ * is the console that gets the exception. An unknown request context is
56
+ * treated as public: exact is the safe default of the two.
57
+ */
58
+ public function internalId(?string $internalId)
59
+ {
60
+ if (Http::isInternalRequest($this->request)) {
61
+ $this->builder->searchWhere('internal_id', $internalId);
62
+
63
+ return;
64
+ }
65
+
66
+ $this->builder->where('internal_id', '=', $internalId);
67
+ }
68
+
33
69
  public function vin(?string $vin)
34
70
  {
35
71
  $this->builder->searchWhere('vin', $vin);
36
72
  }
37
73
 
74
+ /**
75
+ * A public id identifies exactly one record, so a public lookup matches it
76
+ * exactly. The console keeps the partial search its id column filter box
77
+ * has always had.
78
+ */
38
79
  public function publicId(?string $publicIc)
39
80
  {
40
- $this->builder->searchWhere('public_id', $publicIc);
81
+ if (Http::isInternalRequest($this->request)) {
82
+ $this->builder->searchWhere('public_id', $publicIc);
83
+
84
+ return;
85
+ }
86
+
87
+ $this->builder->where('public_id', '=', $publicIc);
41
88
  }
42
89
 
43
90
  public function plateNumber(?string $plateNumber)
@@ -68,10 +115,12 @@ class VehicleFilter extends Filter
68
115
  return;
69
116
  }
70
117
 
118
+ $driverUuids = $this->resolvePublicRelationUuids(Driver::class, $driverId);
119
+
71
120
  $this->builder->whereHas(
72
121
  'driver',
73
- function ($query) use ($driverId) {
74
- $query->where('uuid', $driverId);
122
+ function ($query) use ($driverUuids) {
123
+ $query->whereIn('uuid', $driverUuids);
75
124
  }
76
125
  );
77
126
  }
@@ -82,20 +131,7 @@ class VehicleFilter extends Filter
82
131
  return;
83
132
  }
84
133
 
85
- $this->builder->whereIn('vendor_uuid', Vendor::query()
86
- ->where('company_uuid', $this->session->get('company'))
87
- ->where(function ($query) use ($vendor) {
88
- $query->where('public_id', $vendor);
89
-
90
- if (in_array('internal_id', (new Vendor())->getFillable())) {
91
- $query->orWhere('internal_id', $vendor);
92
- }
93
-
94
- if (Http::isInternalRequest($this->request)) {
95
- $query->orWhere('uuid', $vendor);
96
- }
97
- })
98
- ->pluck('uuid'));
134
+ $this->builder->whereIn('vendor_uuid', $this->resolvePublicRelationUuids(Vendor::class, $vendor));
99
135
  }
100
136
 
101
137
  public function driverUuid(?string $driverId)
@@ -132,10 +168,12 @@ class VehicleFilter extends Filter
132
168
 
133
169
  public function fleet(string $fleet)
134
170
  {
171
+ $fleetUuids = $this->resolvePublicRelationUuids(Fleet::class, $fleet);
172
+
135
173
  $this->builder->whereHas(
136
174
  'fleets',
137
- function ($q) use ($fleet) {
138
- $q->where('fleet_uuid', $fleet);
175
+ function ($q) use ($fleetUuids) {
176
+ $q->whereIn('fleet_uuid', $fleetUuids);
139
177
  }
140
178
  );
141
179
  }
@@ -0,0 +1,48 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Http\Requests\Concerns;
4
+
5
+ use Illuminate\Validation\Rule;
6
+
7
+ /**
8
+ * Builds `exists` rules for public relationship inputs that are scoped to the
9
+ * authenticated company.
10
+ *
11
+ * An unscoped `exists:vendors,public_id` accepts another organization's public
12
+ * id, and the controller then resolves it to nothing — so the request looks
13
+ * accepted while the relationship is silently dropped. Scoping the rule turns
14
+ * that into an explicit validation failure, and keeps a public id from
15
+ * confirming or denying the existence of a resource in someone else's company.
16
+ */
17
+ trait ScopesPublicRelationRules
18
+ {
19
+ /**
20
+ * An `exists` rule limited to the session company and to rows that are not
21
+ * soft deleted.
22
+ *
23
+ * No return type: the pest harness substitutes its own `Illuminate\Validation\Rule`
24
+ * stand-in, which is not an instance of the real `Rules\Exists`.
25
+ */
26
+ protected function existsInCompany(string $table, string $column = 'public_id')
27
+ {
28
+ $companyUuid = session('company');
29
+
30
+ return Rule::exists($table, $column)->where(function ($query) use ($companyUuid) {
31
+ $query->where('company_uuid', $companyUuid);
32
+ $query->whereNull('deleted_at');
33
+ });
34
+ }
35
+
36
+ /**
37
+ * The full rule set for an optional public relationship input.
38
+ *
39
+ * `nullable` is deliberate: sending `null` is how a caller clears an
40
+ * existing assignment, and `exists` is skipped for a null value.
41
+ *
42
+ * @return array<int, mixed>
43
+ */
44
+ protected function publicRelationRules(string $table, string $column = 'public_id'): array
45
+ {
46
+ return ['nullable', 'string', $this->existsInCompany($table, $column)];
47
+ }
48
+ }
@@ -2,12 +2,16 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Http\Requests;
4
4
 
5
+ use Fleetbase\FleetOps\Http\Requests\Concerns\ScopesPublicRelationRules;
6
+ use Fleetbase\FleetOps\Models\Driver;
5
7
  use Fleetbase\FleetOps\Rules\ResolvablePoint;
6
8
  use Fleetbase\Http\Requests\FleetbaseRequest;
7
9
  use Illuminate\Validation\Rule;
8
10
 
9
11
  class CreateDriverRequest extends FleetbaseRequest
10
12
  {
13
+ use ScopesPublicRelationRules;
14
+
11
15
  /**
12
16
  * Determine if the user is authorized to make this request.
13
17
  */
@@ -24,23 +28,110 @@ class CreateDriverRequest extends FleetbaseRequest
24
28
  $isCreating = $this->isMethod('POST');
25
29
 
26
30
  return [
27
- 'name' => [Rule::requiredIf($isCreating)],
28
- 'email' => [Rule::requiredIf($isCreating), Rule::when($this->filled('email'), ['email']), Rule::when($isCreating, [Rule::unique('users')->whereNull('deleted_at')])],
29
- 'phone' => [Rule::requiredIf($isCreating), Rule::when($isCreating, [Rule::unique('users')->whereNull('deleted_at')])],
30
- 'password' => 'nullable|string',
31
+ 'name' => [Rule::requiredIf($isCreating), 'nullable', 'string', 'max:191'],
32
+
33
+ /*
34
+ * Email and phone are optional.
35
+ *
36
+ * An operational fleet record often has neither: a subcontracted or
37
+ * yard-only driver may have no company mailbox and no handset. The
38
+ * previous contract forced both, and the only way to satisfy it was
39
+ * to invent an address or a number — which then sits in the tenant's
40
+ * user table looking real, can be mailed to, and blocks the genuine
41
+ * value later. Both are still validated and still unique when they
42
+ * are supplied; a driver created without them simply cannot sign in
43
+ * to Navigator until credentials are added.
44
+ */
45
+ 'email' => ['nullable', Rule::when($this->filled('email'), ['email']), $this->uniqueAmongUsers()],
46
+ 'phone' => ['nullable', 'string', $this->uniqueAmongUsers()],
47
+ 'password' => 'nullable|string',
48
+ 'timezone' => 'nullable|string|max:64',
49
+
50
+ // Identity
51
+ 'internal_id' => 'nullable|string|max:191',
52
+ 'drivers_license_number' => 'nullable|string|max:191',
53
+ 'license_expiry' => 'nullable|date',
54
+ 'photo' => 'nullable|string',
55
+
56
+ // Operational
31
57
  'country' => 'nullable|size:2',
32
- 'city' => 'nullable|string',
33
- 'vehicle' => 'nullable|string|starts_with:vehicle_|exists:vehicles,public_id',
34
- 'license_expiry' => 'nullable|date',
58
+ 'currency' => 'nullable|string|size:3',
59
+ 'city' => 'nullable|string|max:191',
60
+ 'online' => 'nullable|boolean',
61
+ 'current_status' => 'nullable|string|max:64',
35
62
  'status' => 'nullable|string|in:active,available,inactive',
36
- 'vendor' => 'nullable|exists:vendors,public_id',
37
- 'job' => 'nullable|exists:orders,public_id',
63
+ 'heading' => 'nullable|numeric',
64
+ 'bearing' => 'nullable|numeric',
65
+ 'altitude' => 'nullable|numeric',
66
+ 'speed' => 'nullable|numeric',
38
67
  'location' => ['nullable', new ResolvablePoint()],
39
68
  'latitude' => ['nullable', 'required_with:longitude'],
40
69
  'longitude' => ['nullable', 'required_with:latitude'],
70
+
71
+ // Structured / orchestrator
72
+ 'meta' => 'nullable|array',
73
+ 'skills' => 'nullable|array',
74
+ 'skills.*' => 'string',
75
+ 'max_travel_time' => 'nullable|integer|min:0',
76
+ 'max_distance' => 'nullable|integer|min:0',
77
+ 'time_window_start' => 'nullable|date_format:H:i,H:i:s',
78
+ 'time_window_end' => 'nullable|date_format:H:i,H:i:s',
79
+
80
+ // Relationships, resolved from public ids inside the caller's company
81
+ 'vehicle' => ['nullable', 'string', 'starts_with:vehicle_', $this->existsInCompany('vehicles')],
82
+ 'vendor' => $this->publicRelationRules('vendors'),
83
+ 'job' => $this->publicRelationRules('orders'),
41
84
  ];
42
85
  }
43
86
 
87
+ /**
88
+ * Uniqueness against every other live user account.
89
+ *
90
+ * Update used to skip this check entirely, so one driver could be given an
91
+ * address another driver already signs in with — and the two would then be
92
+ * indistinguishable to every identity lookup, including the password reset
93
+ * that matches on it. The driver's own linked user is ignored by uuid, so
94
+ * resending an unchanged address or number still succeeds.
95
+ *
96
+ * No return type: the pest harness substitutes its own
97
+ * `Illuminate\Validation\Rule`, which is not an instance of `Rules\Unique`.
98
+ */
99
+ protected function uniqueAmongUsers()
100
+ {
101
+ $rule = Rule::unique('users')->whereNull('deleted_at');
102
+
103
+ $ownUserUuid = $this->linkedUserUuid();
104
+
105
+ return $ownUserUuid ? $rule->ignore($ownUserUuid, 'uuid') : $rule;
106
+ }
107
+
108
+ /**
109
+ * The uuid of the user behind the driver this request is updating.
110
+ *
111
+ * Null on create, and null when there is no resolvable route parameter —
112
+ * which is also what keeps this from querying during rule construction in
113
+ * contexts that have no route bound.
114
+ */
115
+ protected function linkedUserUuid(): ?string
116
+ {
117
+ if ($this->isMethod('POST')) {
118
+ return null;
119
+ }
120
+
121
+ $id = $this->route('id');
122
+
123
+ if (!is_string($id) || $id === '') {
124
+ return null;
125
+ }
126
+
127
+ return Driver::withoutGlobalScopes()
128
+ ->where('company_uuid', session('company'))
129
+ ->where(function ($query) use ($id) {
130
+ $query->where('public_id', $id)->orWhere('internal_id', $id);
131
+ })
132
+ ->value('user_uuid');
133
+ }
134
+
44
135
  /**
45
136
  * Get custom attributes for validator errors.
46
137
  */