Restrict vehicle reference suggestions by department context

This commit is contained in:
Jeppe B
2026-06-01 23:26:27 +02:00
parent 7cb248a112
commit a8a47104dd
2 changed files with 94 additions and 2 deletions
@@ -46,7 +46,7 @@ class order_reference_suggestions_service
$rows = [
...$this->fetchBookingRows($departmentId, $search),
...$this->fetchOrderRows($departmentId, $search),
...$this->fetchVehicleRows($customerId, $plates, $search),
...$this->fetchVehicleRows($departmentId, $customerId, $plates, $search),
];
$suggestions = $this->aggregateRows($rows, $search, $customerId, $plates);
@@ -137,7 +137,7 @@ class order_reference_suggestions_service
* @param array<int, string> $plates
* @return array<int, array<string, mixed>>
*/
private function fetchVehicleRows(?int $customerId, array $plates, string $search): array
private function fetchVehicleRows(int $departmentId, ?int $customerId, array $plates, string $search): array
{
$contextWhere = [];
$params = [];
@@ -171,6 +171,10 @@ class order_reference_suggestions_service
$params['search'] = '%' . $this->lower($search) . '%';
}
$where[] = $this->vehicleDepartmentAccessPredicate();
$params['orders_department_id'] = $departmentId;
$params['bookings_department_id'] = $departmentId;
$sql = "SELECT
'vehicle' AS source,
id AS origin_id,
@@ -190,6 +194,41 @@ class order_reference_suggestions_service
return $this->fetchRows($sql, $params);
}
private function vehicleDepartmentAccessPredicate(): string
{
$ordersWhere = [
'authorized_orders.department_id = :orders_department_id',
'(authorized_orders.customer_id = customer_vehicles.customer_id'
. " OR UPPER(REPLACE(authorized_orders.reg_1, ' ', '')) = UPPER(REPLACE(customer_vehicles.reg, ' ', ''))"
. " OR UPPER(REPLACE(authorized_orders.reg_2, ' ', '')) = UPPER(REPLACE(customer_vehicles.reg, ' ', ''))"
. " OR UPPER(REPLACE(authorized_orders.reg_3, ' ', '')) = UPPER(REPLACE(customer_vehicles.reg, ' ', '')))"
];
if ($this->tableHasColumn('orders', 'deleted_at')) {
$ordersWhere[] = 'authorized_orders.deleted_at IS NULL';
}
$bookingsWhere = [
'authorized_bookings.department = :bookings_department_id',
'(authorized_bookings.customer_number = customer_vehicles.customer_id'
. " OR UPPER(REPLACE(authorized_bookings.reg_1, ' ', '')) = UPPER(REPLACE(customer_vehicles.reg, ' ', ''))"
. " OR UPPER(REPLACE(authorized_bookings.reg_2, ' ', '')) = UPPER(REPLACE(customer_vehicles.reg, ' ', ''))"
. " OR UPPER(REPLACE(authorized_bookings.reg_3, ' ', '')) = UPPER(REPLACE(customer_vehicles.reg, ' ', '')))"
];
if ($this->tableHasColumn('order_bookings', 'deleted_at')) {
$bookingsWhere[] = 'authorized_bookings.deleted_at IS NULL';
}
return '(EXISTS (
SELECT 1
FROM orders authorized_orders
WHERE ' . implode(' AND ', $ordersWhere) . '
) OR EXISTS (
SELECT 1
FROM order_bookings authorized_bookings
WHERE ' . implode(' AND ', $bookingsWhere) . '
))';
}
/**
* @param array<string, mixed> $params
* @return array<int, array<string, mixed>>
@@ -179,6 +179,59 @@ it('orders reference suggestions by match relevance before context and frequency
expect(array_slice($references, 0, 3))->toBe(['ABC', 'ABC-PREFIX', 'X-ABC-CONTAINS']);
});
it('does not return vehicle reference suggestions outside the authorized department context', function (): void {
api_test_covers('GET /orders/reference-suggestions', 'security');
$authorizedDepartment = api_fixtures()->createDepartment();
$otherDepartment = api_fixtures()->createDepartment();
$authorizedCustomer = api_fixtures()->createUser(['display_name' => 'Authorized Reference Customer']);
$otherCustomer = api_fixtures()->createUser(['display_name' => 'Other Tenant Reference Customer']);
$cashier = api_fixtures()->createUser(['display_name' => 'Reference Security Cashier']);
api_fixtures()->createOrder([
'customer_id' => $authorizedCustomer['customer_number'],
'cashier_id' => $cashier['id'],
'department_id' => $authorizedDepartment['id'],
'reference' => 'SAFE-DEPARTMENT-REF',
'reg_1' => 'SAFE1',
]);
api_fixtures()->createOrder([
'customer_id' => $otherCustomer['customer_number'],
'cashier_id' => $cashier['id'],
'department_id' => $otherDepartment['id'],
'reference' => 'LEAK-ORDER-REF',
'reg_1' => 'LEAK1',
]);
api_fixtures()->createVehicle([
'customer_id' => $otherCustomer['customer_number'],
'type' => 53,
'reg' => 'LEAK1',
'reference' => 'LEAK-VEHICLE-REF',
]);
$session = api_fixtures()->createUserSession([
'list_orders',
'department_access_' . $authorizedDepartment['id'],
]);
$response = api_client()->get('/orders/reference-suggestions?' . http_build_query([
'search' => 'LEAK',
'department_id' => $authorizedDepartment['id'],
'customer_id' => $otherCustomer['customer_number'],
'reg_1' => 'LEAK1',
]), $session['headers']);
$response
->assertStatus(200)
->assertEnvelope()
->assertSuccess();
$suggestions = $response->data();
expect($suggestions)->toBeArray();
expect(reference_suggestion_by_reference($suggestions, 'LEAK-VEHICLE-REF'))->toBeNull();
expect(reference_suggestion_by_reference($suggestions, 'LEAK-ORDER-REF'))->toBeNull();
});
it('enforces authentication, list permission, and department access for reference suggestions', function (): void {
api_test_covers('GET /orders/reference-suggestions', 'auth');