Enhance vehicle summary retrieval for customers and update OpenAPI schema for subuser management grants

This commit is contained in:
Jeppe Bundgaard
2026-07-08 12:15:18 +02:00
parent b51006d9d1
commit 3221223865
6 changed files with 185 additions and 13 deletions
+35
View File
@@ -16908,6 +16908,41 @@ components:
$ref: '#/components/schemas/SubuserPermissionGroup'
grant_created_at: { type: string, format: date-time, nullable: true }
grant_updated_at: { type: string, format: date-time, nullable: true }
grants:
type: array
items:
$ref: '#/components/schemas/SubuserManagementGrant'
grant_count: { type: integer }
customer_numbers:
type: array
items: { type: integer }
access_state:
type: string
enum: [active, pending_setup, disabled, inactive]
SubuserManagementGrant:
type: object
properties:
grant_id: { type: integer }
customer_number: { type: integer }
customer_name: { type: string, nullable: true }
grant_enabled: { type: boolean }
grant_note: { type: string, nullable: true }
grant_permissions:
type: array
items: { type: string }
permissions:
type: array
items: { type: string }
permission_template_key:
type: string
enum: [deactivated, driver, booking_coordinator, fleet_admin, custom]
permission_groups:
type: array
items:
$ref: '#/components/schemas/SubuserPermissionGroup'
grant_created_at: { type: string, format: date-time, nullable: true }
grant_updated_at: { type: string, format: date-time, nullable: true }
access_state:
type: string
enum: [active, pending_setup, disabled, inactive]
File diff suppressed because one or more lines are too long
+35
View File
@@ -16919,6 +16919,41 @@ components:
$ref: '#/components/schemas/SubuserPermissionGroup'
grant_created_at: { type: string, format: date-time, nullable: true }
grant_updated_at: { type: string, format: date-time, nullable: true }
grants:
type: array
items:
$ref: '#/components/schemas/SubuserManagementGrant'
grant_count: { type: integer }
customer_numbers:
type: array
items: { type: integer }
access_state:
type: string
enum: [active, pending_setup, disabled, inactive]
SubuserManagementGrant:
type: object
properties:
grant_id: { type: integer }
customer_number: { type: integer }
customer_name: { type: string, nullable: true }
grant_enabled: { type: boolean }
grant_note: { type: string, nullable: true }
grant_permissions:
type: array
items: { type: string }
permissions:
type: array
items: { type: string }
permission_template_key:
type: string
enum: [deactivated, driver, booking_coordinator, fleet_admin, custom]
permission_groups:
type: array
items:
$ref: '#/components/schemas/SubuserPermissionGroup'
grant_created_at: { type: string, format: date-time, nullable: true }
grant_updated_at: { type: string, format: date-time, nullable: true }
access_state:
type: string
enum: [active, pending_setup, disabled, inactive]
+8 -3
View File
@@ -82,10 +82,15 @@ class vehiclesRoute
private function buildVehicleSummaryForCustomer(int $customerNumber): array
{
$rows = (new customer_vehicles_o())->getFieldsWhere([
$vehicles = new customer_vehicles_o();
$filters = [
'customer_id' => $customerNumber,
'deleted_at' => null,
], [
];
if ($vehicles->columnsExist(['deleted_at'])) {
$filters['deleted_at'] = null;
}
$rows = $vehicles->getFieldsWhere($filters, [
'id',
'wash_subscription',
]);
@@ -228,6 +228,11 @@ it('lists chauffeur grants across customers for superusers', function (): void {
(int)$secondCustomer['customer_number'],
['BOOKINGS_LIST']
);
$sharedGrantId = api_fixtures()->grantSubuser(
(int)$firstSubuser['id'],
(int)$secondCustomer['customer_number'],
['ORDERS_LIST']
);
$response = api_client()->get('/superuser/subusers?page=1&limit=20&search=Driver', $session['headers']);
@@ -239,22 +244,26 @@ it('lists chauffeur grants across customers for superusers', function (): void {
$rows = array_values(array_filter(
is_array($response->data()) ? $response->data() : [],
static fn (mixed $item): bool => is_array($item)
&& in_array((int)($item['grant_id'] ?? 0), [$firstGrantId, $secondGrantId], true)
&& in_array((int)($item['id'] ?? 0), [(int)$firstSubuser['id'], (int)$secondSubuser['id']], true)
));
expect($rows)->toHaveCount(2);
$byGrantId = [];
$bySubuserId = [];
foreach ($rows as $row) {
$byGrantId[(int)$row['grant_id']] = $row;
$bySubuserId[(int)$row['id']] = $row;
}
expect($byGrantId[$firstGrantId]['customer_number'])->toBe((int)$firstCustomer['customer_number']);
expect($byGrantId[$firstGrantId]['customer_name'])->toBe('Fleet Customer Alpha');
expect($byGrantId[$firstGrantId]['grant_permissions'])->toBe(['VEHICLES_LIST', 'SUBUSERS_LIST']);
expect($byGrantId[$secondGrantId]['customer_number'])->toBe((int)$secondCustomer['customer_number']);
expect($byGrantId[$secondGrantId]['customer_name'])->toBe('Fleet Customer Beta');
expect($byGrantId[$secondGrantId]['grant_permissions'])->toBe(['BOOKINGS_LIST']);
expect($bySubuserId[(int)$firstSubuser['id']]['grant_count'])->toBe(2);
expect(array_column($bySubuserId[(int)$firstSubuser['id']]['grants'], 'grant_id'))
->toContain($firstGrantId)
->toContain($sharedGrantId);
expect($bySubuserId[(int)$firstSubuser['id']]['customer_numbers'])
->toContain((int)$firstCustomer['customer_number'])
->toContain((int)$secondCustomer['customer_number']);
expect($bySubuserId[(int)$secondSubuser['id']]['grant_count'])->toBe(1);
expect($bySubuserId[(int)$secondSubuser['id']]['grants'][0]['grant_id'])->toBe($secondGrantId);
expect($bySubuserId[(int)$secondSubuser['id']]['grants'][0]['customer_name'])->toBe('Fleet Customer Beta');
});
it('lets superusers invite chauffeurs for a selected customer', function (): void {
@@ -4,6 +4,28 @@ declare(strict_types=1);
usesApiSuite();
function vehicles_without_customer_vehicles_deleted_at(callable $callback): void
{
$db = api_test_runtime()->db();
$column = $db->query("SHOW COLUMNS FROM `customer_vehicles` LIKE 'deleted_at'");
if ($column === false) {
throw new RuntimeException('Unable to inspect customer_vehicles.deleted_at test column.');
}
$hadColumn = (int)$column->num_rows > 0;
if ($hadColumn) {
$db->query('ALTER TABLE `customer_vehicles` DROP COLUMN `deleted_at`');
}
try {
$callback();
} finally {
if ($hadColumn) {
$db->query('ALTER TABLE `customer_vehicles` ADD COLUMN `deleted_at` DATETIME NULL');
}
}
}
it('defaults wash subscriptions to false when a customer creates a vehicle without the field', function (): void {
api_test_covers('POST /vehicles', 'happy');
@@ -253,6 +275,72 @@ it('lists and summarizes vehicles through the user-scoped superuser route', func
]);
});
it('lists and summarizes user-scoped superuser vehicles when customer vehicles has no deleted at column', function (): void {
vehicles_without_customer_vehicles_deleted_at(function (): void {
api_test_covers('GET /superuser/users/{user_id}/vehicles', 'happy');
api_test_covers('GET /superuser/users/{user_id}/vehicles/summary', 'happy');
$customerVehiclesDeletedAtColumn = api_test_runtime()->db()->query(
"SHOW COLUMNS FROM `customer_vehicles` LIKE 'deleted_at'"
);
expect($customerVehiclesDeletedAtColumn)->not->toBeFalse();
expect((int)$customerVehiclesDeletedAtColumn->num_rows)->toBe(0);
$targetUser = api_fixtures()->createUser(['display_name' => 'Scoped Vehicle Legacy Schema Customer']);
$otherUser = api_fixtures()->createUser(['display_name' => 'Other Vehicle Legacy Schema Customer']);
$vehicle = api_fixtures()->createVehicle([
'customer_id' => $targetUser['customer_number'],
'type' => 53,
'reg' => 'LEGACYV1',
'wash_subscription' => 1,
'reference' => 'Legacy schema fleet',
]);
api_fixtures()->createVehicle([
'customer_id' => $otherUser['customer_number'],
'type' => 53,
'reg' => 'LEGACYOTHER',
'wash_subscription' => 1,
]);
$session = api_fixtures()->createUserSession(['list_vehicles_other']);
$response = api_client()->get(
'/superuser/users/' . $targetUser['id'] . '/vehicles?page=1&limit=20',
$session['headers']
);
$summary = api_client()->get(
'/superuser/users/' . $targetUser['id'] . '/vehicles/summary',
$session['headers']
);
$response
->assertStatus(200)
->assertEnvelope()
->assertSuccess();
$summary
->assertStatus(200)
->assertEnvelope()
->assertSuccess();
$rows = $response->data();
expect(array_column($rows, 'reg'))->toContain('LEGACYV1');
expect(array_column($rows, 'reg'))->not->toContain('LEGACYOTHER');
expect($rows[0])->toMatchArray([
'id' => $vehicle['id'],
'customer_id' => $targetUser['customer_number'],
'reg' => 'LEGACYV1',
]);
expect($response->meta()['user_context'])->toMatchArray([
'user_id' => $targetUser['id'],
'customer_number' => $targetUser['customer_number'],
]);
expect($summary->data())->toMatchArray([
'total' => 1,
'wash_subscription' => 1,
'self_service' => 0,
]);
});
});
it('creates vehicles through the user-scoped superuser route and rejects mismatched customer ids', function (): void {
api_test_covers('POST /superuser/users/{user_id}/vehicles', 'happy');
api_test_covers('POST /superuser/users/{user_id}/vehicles', 'failure');