@fleetbase/fleetops-engine 0.6.61 → 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 (64) hide show
  1. package/RELEASE.md +23 -8
  2. package/addon/services/map-settings.js +4 -1
  3. package/composer.json +1 -1
  4. package/extension.json +1 -1
  5. package/package.json +1 -1
  6. package/server/migrations/2026_09_05_000001_add_unique_indexes_to_fleet_membership_pivots.php +129 -0
  7. package/server/src/Exceptions/PublicRelationNotFoundException.php +47 -0
  8. package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesFleetOpsApiResources.php +42 -7
  9. package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesPublicExpansions.php +187 -0
  10. package/server/src/Http/Controllers/Api/v1/DriverController.php +93 -57
  11. package/server/src/Http/Controllers/Api/v1/EntityController.php +1 -1
  12. package/server/src/Http/Controllers/Api/v1/FleetController.php +370 -33
  13. package/server/src/Http/Controllers/Api/v1/FuelReportController.php +1 -1
  14. package/server/src/Http/Controllers/Api/v1/IssueController.php +1 -1
  15. package/server/src/Http/Controllers/Api/v1/OrderController.php +1 -1
  16. package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +1 -1
  17. package/server/src/Http/Controllers/Api/v1/TrackingStatusController.php +1 -1
  18. package/server/src/Http/Controllers/Api/v1/VehicleController.php +134 -35
  19. package/server/src/Http/Controllers/Api/v1/ZoneController.php +1 -1
  20. package/server/src/Http/Filter/Concerns/ResolvesPublicRelationUuids.php +54 -0
  21. package/server/src/Http/Filter/DriverFilter.php +71 -15
  22. package/server/src/Http/Filter/FleetFilter.php +32 -33
  23. package/server/src/Http/Filter/VehicleFilter.php +57 -19
  24. package/server/src/Http/Requests/Concerns/ScopesPublicRelationRules.php +48 -0
  25. package/server/src/Http/Requests/CreateDriverRequest.php +100 -9
  26. package/server/src/Http/Requests/CreateFleetRequest.php +30 -2
  27. package/server/src/Http/Requests/CreateServiceRateRequest.php +7 -2
  28. package/server/src/Http/Requests/CreateVehicleRequest.php +150 -31
  29. package/server/src/Http/Resources/v1/Concerns/ResolvesPublicRelationFields.php +110 -0
  30. package/server/src/Http/Resources/v1/Driver.php +22 -2
  31. package/server/src/Http/Resources/v1/Fleet.php +38 -25
  32. package/server/src/Http/Resources/v1/Vehicle.php +46 -4
  33. package/server/src/Models/Driver.php +2 -2
  34. package/server/src/routes.php +8 -0
  35. package/server/tests/ApiDriverControllerContractsTest.php +284 -13
  36. package/server/tests/ApiEntityControllerContractsTest.php +1 -1
  37. package/server/tests/ApiEquipmentControllerContractsTest.php +2 -2
  38. package/server/tests/ApiFleetControllerContractsTest.php +354 -21
  39. package/server/tests/ApiFuelReportControllerContractsTest.php +1 -1
  40. package/server/tests/ApiIssueControllerContractsTest.php +1 -1
  41. package/server/tests/ApiOrderControllerContractsTest.php +1 -1
  42. package/server/tests/ApiPartControllerContractsTest.php +2 -2
  43. package/server/tests/ApiServiceAreaZoneControllerContractsTest.php +2 -2
  44. package/server/tests/ApiTrackingStatusControllerContractsTest.php +1 -1
  45. package/server/tests/ApiVehicleControllerContractsTest.php +175 -8
  46. package/server/tests/ControllerFilterContractsTest.php +132 -13
  47. package/server/tests/ControllerHelperContractsTest.php +6 -3
  48. package/server/tests/DriverFilterExecutionTest.php +130 -6
  49. package/server/tests/ExceptionContractsTest.php +15 -0
  50. package/server/tests/Feature/Http/Api/DeviceControllerContractsTest.php +1 -1
  51. package/server/tests/Feature/Http/Api/FleetMembershipMigrationTest.php +140 -0
  52. package/server/tests/Feature/Http/Api/FleetMembershipTest.php +333 -0
  53. package/server/tests/Feature/Http/Api/FleetPublicContractTest.php +439 -0
  54. package/server/tests/Feature/Http/Api/FuelTransactionControllerContractsTest.php +2 -2
  55. package/server/tests/Feature/Http/Api/NullRelationshipInputTest.php +191 -0
  56. package/server/tests/Feature/Http/Api/PublicIdentifierFilterTest.php +293 -0
  57. package/server/tests/Feature/Http/Api/SmallApiControllerHelpersTest.php +7 -1
  58. package/server/tests/Feature/Http/Api/VehicleControllerTrackingTest.php +3 -1
  59. package/server/tests/Feature/Http/Api/VehiclePublicContractTest.php +454 -0
  60. package/server/tests/Feature/Http/Api/WorkOrderControllerContractsTest.php +1 -1
  61. package/server/tests/FleetHierarchyResourceTest.php +56 -8
  62. package/server/tests/RequestContractsTest.php +100 -7
  63. package/server/tests/Unit/Http/Resources/FleetResourceTest.php +50 -6
  64. package/server/tests/VehicleControllerHelperContractsTest.php +1 -1
@@ -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
  */
@@ -2,11 +2,14 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Http\Requests;
4
4
 
5
+ use Fleetbase\FleetOps\Http\Requests\Concerns\ScopesPublicRelationRules;
5
6
  use Fleetbase\Http\Requests\FleetbaseRequest;
6
7
  use Illuminate\Validation\Rule;
7
8
 
8
9
  class CreateFleetRequest extends FleetbaseRequest
9
10
  {
11
+ use ScopesPublicRelationRules;
12
+
10
13
  /**
11
14
  * Determine if the user is authorized to make this request.
12
15
  */
@@ -21,8 +24,33 @@ class CreateFleetRequest extends FleetbaseRequest
21
24
  public function rules(): array
22
25
  {
23
26
  return [
24
- 'name' => [Rule::requiredIf($this->isMethod('POST'))],
25
- 'service_area' => 'exists:service_areas,public_id',
27
+ 'name' => [Rule::requiredIf($this->isMethod('POST')), 'nullable', 'string', 'max:191'],
28
+ 'color' => 'nullable|string|max:64',
29
+ 'task' => 'nullable|string|max:191',
30
+ /*
31
+ * Fleet status is not a closed set on this model: the console, the
32
+ * spreadsheet importer and existing integrations all write free-form
33
+ * values, and `active`/`disabled`/`decommissioned` are the console's
34
+ * options rather than a schema constraint. Restricting the public API
35
+ * to a subset would leave it unable to express a state the console can.
36
+ */
37
+ 'status' => 'nullable|string|max:64',
38
+ 'service_area' => $this->publicRelationRules('service_areas'),
39
+ 'zone' => $this->publicRelationRules('zones'),
40
+ 'vendor' => $this->publicRelationRules('vendors'),
41
+ 'parent_fleet' => $this->publicRelationRules('fleets'),
42
+ 'photo' => 'nullable|string',
43
+ ];
44
+ }
45
+
46
+ /**
47
+ * Get custom attributes for validator errors.
48
+ */
49
+ public function attributes(): array
50
+ {
51
+ return [
52
+ 'service_area' => 'service area',
53
+ 'parent_fleet' => 'parent fleet',
26
54
  ];
27
55
  }
28
56
  }
@@ -25,8 +25,13 @@ class CreateServiceRateRequest extends FleetbaseRequest
25
25
  return [
26
26
  'service_name' => [Rule::requiredIf($this->isMethod('POST')), 'string'],
27
27
  'service_type' => [Rule::requiredIf($this->isMethod('POST')), 'string'],
28
- 'service_area' => [Rule::exists('service_areas', 'public_id')->whereNull('deleted_at')],
29
- 'zone' => [Rule::exists('zones', 'public_id')->whereNull('deleted_at')],
28
+ // `nullable` so an empty relationship is treated as absent rather than
29
+ // as an invalid one. Both are optional, but without it `exists` runs
30
+ // against the null that ConvertEmptyStringsToNull produced and answers
31
+ // "The selected service area is invalid" for a field the caller simply
32
+ // did not fill in.
33
+ 'service_area' => ['nullable', Rule::exists('service_areas', 'public_id')->whereNull('deleted_at')],
34
+ 'zone' => ['nullable', Rule::exists('zones', 'public_id')->whereNull('deleted_at')],
30
35
  'rate_calculation_method' => [Rule::requiredIf($this->isMethod('POST')), 'string', 'in:fixed_meter,fixed_rate,per_meter,per_drop,algo,parcel'],
31
36
  'currency' => ['required', 'size:3'],
32
37
  'base_fee' => ['numeric'],
@@ -2,12 +2,45 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Http\Requests;
4
4
 
5
+ use Fleetbase\FleetOps\Http\Requests\Concerns\ScopesPublicRelationRules;
5
6
  use Fleetbase\FleetOps\Rules\ResolvablePoint;
6
7
  use Fleetbase\Http\Requests\FleetbaseRequest;
7
8
  use Illuminate\Validation\Rule;
8
9
 
9
10
  class CreateVehicleRequest extends FleetbaseRequest
10
11
  {
12
+ use ScopesPublicRelationRules;
13
+
14
+ /**
15
+ * The statuses the public API accepts for a vehicle.
16
+ *
17
+ * `active` is retained even though the model rewrites it to `available`, so
18
+ * clients written against the original contract keep working.
19
+ *
20
+ * @var array<int, string>
21
+ */
22
+ public const STATUSES = [
23
+ 'active',
24
+ 'available',
25
+ 'in_use',
26
+ 'maintenance',
27
+ 'out_of_service',
28
+ 'reserved',
29
+ 'retired',
30
+ 'staging',
31
+ 'on_route',
32
+ 'idle',
33
+ 'cleaning',
34
+ 'awaiting_parts',
35
+ 'inspection_due',
36
+ 'inspection_failed',
37
+ 'accident',
38
+ 'compliance_hold',
39
+ 'stolen',
40
+ 'operational',
41
+ 'decommissioned',
42
+ ];
43
+
11
44
  /**
12
45
  * Determine if the user is authorized to make this request.
13
46
  */
@@ -22,40 +55,126 @@ class CreateVehicleRequest extends FleetbaseRequest
22
55
  public function rules(): array
23
56
  {
24
57
  return [
25
- 'status' => [
26
- 'nullable',
27
- Rule::in([
28
- 'active',
29
- 'available',
30
- 'in_use',
31
- 'maintenance',
32
- 'out_of_service',
33
- 'reserved',
34
- 'retired',
35
- 'staging',
36
- 'on_route',
37
- 'idle',
38
- 'cleaning',
39
- 'awaiting_parts',
40
- 'inspection_due',
41
- 'inspection_failed',
42
- 'accident',
43
- 'compliance_hold',
44
- 'stolen',
45
- 'operational',
46
- 'decommissioned',
47
- ]),
48
- ],
58
+ 'status' => ['nullable', Rule::in(static::STATUSES)],
59
+
60
+ // Identity and description
61
+ 'internal_id' => 'nullable|string|max:191',
62
+ 'name' => 'nullable|string|max:191',
63
+ 'description' => 'nullable|string',
64
+ 'make' => 'nullable|string|max:191',
65
+ 'model' => 'nullable|string|max:191',
66
+ 'model_type' => 'nullable|string|max:191',
67
+ 'year' => 'nullable|integer|min:1900|max:2100',
68
+ 'trim' => 'nullable|string|max:191',
69
+ 'color' => 'nullable|string|max:64',
70
+ 'type' => 'nullable|string|max:191',
71
+ 'class' => 'nullable|string|max:191',
72
+ 'plate_number' => 'nullable|string|max:191',
73
+ 'vin' => 'nullable|string|max:64',
74
+ 'serial_number' => 'nullable|string|max:191',
75
+ 'call_sign' => 'nullable|string|max:191',
76
+ 'fuel_card_number' => 'nullable|string|max:191',
77
+
78
+ // Measurement and operation.
79
+ //
49
80
  // Validated rather than merely accepted: the model casts odometer
50
81
  // to an integer, so an unchecked string would be stored as 0 — a
51
82
  // vehicle reporting zero miles rather than an error.
52
- 'odometer' => 'nullable|numeric|min:0',
53
- 'odometer_unit' => 'nullable|string|max:12',
54
- 'vendor' => 'nullable|exists:vendors,public_id',
55
- 'driver' => 'nullable|exists:drivers,public_id',
56
- 'location' => ['nullable', new ResolvablePoint()],
57
- 'latitude' => ['nullable', 'required_with:longitude'],
58
- 'longitude' => ['nullable', 'required_with:latitude'],
83
+ 'odometer' => 'nullable|numeric|min:0',
84
+ 'odometer_unit' => 'nullable|string|max:12',
85
+ 'odometer_at_purchase' => 'nullable|numeric|min:0',
86
+ 'measurement_system' => 'nullable|string|max:32',
87
+ 'fuel_type' => 'nullable|string|max:64',
88
+ 'fuel_volume_unit' => 'nullable|string|max:12',
89
+ 'online' => 'nullable|boolean',
90
+
91
+ // Body, capacity and dimensions
92
+ 'transmission' => 'nullable|string|max:64',
93
+ 'body_type' => 'nullable|string|max:191',
94
+ 'body_sub_type' => 'nullable|string|max:191',
95
+ 'usage_type' => 'nullable|string|max:64',
96
+ 'ownership_type' => 'nullable|string|max:64',
97
+ 'cargo_volume' => 'nullable|numeric|min:0',
98
+ 'passenger_volume' => 'nullable|numeric|min:0',
99
+ 'interior_volume' => 'nullable|numeric|min:0',
100
+ 'weight' => 'nullable|numeric|min:0',
101
+ 'width' => 'nullable|numeric|min:0',
102
+ 'length' => 'nullable|numeric|min:0',
103
+ 'height' => 'nullable|numeric|min:0',
104
+ 'towing_capacity' => 'nullable|numeric|min:0',
105
+ 'payload_capacity' => 'nullable|numeric|min:0',
106
+ 'seating_capacity' => 'nullable|integer|min:0',
107
+ 'ground_clearance' => 'nullable|numeric|min:0',
108
+ 'bed_length' => 'nullable|numeric|min:0',
109
+ 'fuel_capacity' => 'nullable|numeric|min:0',
110
+
111
+ // Lifecycle and financing
112
+ 'financing_status' => 'nullable|string|max:64',
113
+ 'loan_number_of_payments' => 'nullable|integer|min:0',
114
+ 'loan_first_payment' => 'nullable|date',
115
+ 'loan_amount' => 'nullable|numeric|min:0',
116
+ 'estimated_service_life_distance_unit' => 'nullable|string|max:12',
117
+ 'estimated_service_life_distance' => 'nullable|integer|min:0',
118
+ 'estimated_service_life_months' => 'nullable|integer|min:0',
119
+ 'insurance_value' => 'nullable|numeric|min:0',
120
+ 'depreciation_rate' => 'nullable|numeric',
121
+ 'current_value' => 'nullable|numeric|min:0',
122
+ 'acquisition_cost' => 'nullable|numeric|min:0',
123
+ 'currency' => 'nullable|string|size:3',
124
+ 'purchased_at' => 'nullable|date',
125
+ 'lease_expires_at' => 'nullable|date',
126
+
127
+ // Regulatory and engine specifications
128
+ 'emission_standard' => 'nullable|string|max:64',
129
+ 'dpf_equipped' => 'nullable|boolean',
130
+ 'scr_equipped' => 'nullable|boolean',
131
+ 'gvwr' => 'nullable|numeric|min:0',
132
+ 'gcwr' => 'nullable|numeric|min:0',
133
+ 'engine_number' => 'nullable|string|max:191',
134
+ 'engine_model' => 'nullable|string|max:191',
135
+ 'engine_make' => 'nullable|string|max:191',
136
+ 'engine_family' => 'nullable|string|max:191',
137
+ 'engine_configuration' => 'nullable|string|max:191',
138
+ 'engine_displacement' => 'nullable|numeric|min:0',
139
+ 'engine_size' => 'nullable|numeric|min:0',
140
+ 'horsepower' => 'nullable|numeric|min:0',
141
+ 'horsepower_rpm' => 'nullable|integer|min:0',
142
+ 'torque' => 'nullable|numeric|min:0',
143
+ 'torque_rpm' => 'nullable|integer|min:0',
144
+ 'number_of_cylinders' => 'nullable|integer|min:0',
145
+ 'cylinder_arrangement' => 'nullable|string|max:64',
146
+
147
+ // Structured and descriptive fields
148
+ 'specs' => 'nullable|array',
149
+ 'details' => 'nullable|array',
150
+ 'notes' => 'nullable|string',
151
+ 'meta' => 'nullable|array',
152
+
153
+ // Orchestrator
154
+ 'skills' => 'nullable|array',
155
+ 'skills.*' => 'string',
156
+ 'payload_capacity_volume' => 'nullable|numeric|min:0',
157
+ 'payload_capacity_pallets' => 'nullable|integer|min:0',
158
+ 'payload_capacity_parcels' => 'nullable|integer|min:0',
159
+ 'max_tasks' => 'nullable|integer|min:0',
160
+ 'time_window_start' => 'nullable|date_format:H:i,H:i:s',
161
+ 'time_window_end' => 'nullable|date_format:H:i,H:i:s',
162
+ 'return_to_depot' => 'nullable|boolean',
163
+
164
+ // Relationships, resolved from public ids inside the caller's company
165
+ 'vendor' => $this->publicRelationRules('vendors'),
166
+ 'driver' => $this->publicRelationRules('drivers'),
167
+ 'category' => $this->publicRelationRules('categories'),
168
+ 'warranty' => $this->publicRelationRules('warranties'),
169
+ 'photo' => 'nullable|string',
170
+
171
+ // Location
172
+ 'location' => ['nullable', new ResolvablePoint()],
173
+ 'latitude' => ['nullable', 'required_with:longitude'],
174
+ 'longitude' => ['nullable', 'required_with:latitude'],
175
+ 'altitude' => 'nullable|numeric',
176
+ 'heading' => 'nullable|numeric',
177
+ 'speed' => 'nullable|numeric',
59
178
  ];
60
179
  }
61
180
  }