- Implemented `InvoicingPeriodDraftOverlayTest` with coverage for blocking and permitting invoicing actions based on draft states, transactions, and metadata. - Created `ReferenceSuggestionsApiTest` to validate ranked and filtered suggestions across bookings, orders, and vehicles with varied match relevance, context, and frequency. - Added `order_reference_suggestions_service` class, including query methods, normalization utilities, and aggregation logic for reference suggestions. - Enhanced query handling in `InvoicingPeriodDraftOverlayFakeDb` to validate SQL constraints and column cache resets in overlapping invoicing contexts.
209 lines
7.8 KiB
PHP
209 lines
7.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
usesApiSuite();
|
|
|
|
function reference_suggestion_by_reference(array $suggestions, string $reference): ?array
|
|
{
|
|
foreach ($suggestions as $suggestion) {
|
|
if (($suggestion['reference'] ?? null) === $reference) {
|
|
return $suggestion;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
it('returns ranked POS reference suggestions from bookings, orders, and customer vehicles', function (): void {
|
|
api_test_covers('GET /orders/reference-suggestions', 'happy');
|
|
|
|
$department = api_fixtures()->createDepartment();
|
|
$customer = api_fixtures()->createUser(['display_name' => 'Reference Suggestion Customer']);
|
|
$cashier = api_fixtures()->createUser(['display_name' => 'Reference Suggestion Cashier']);
|
|
|
|
api_fixtures()->createOrderBooking([
|
|
'customer_number' => $customer['customer_number'],
|
|
'department' => $department['id'],
|
|
'reference' => 'REF-BOOKING',
|
|
'datetime' => '2026-05-13 09:00:00',
|
|
'created_at' => '2026-05-01 08:00:00',
|
|
'reg_1' => 'BOOK1',
|
|
]);
|
|
api_fixtures()->createOrder([
|
|
'customer_id' => $customer['customer_number'],
|
|
'cashier_id' => $cashier['id'],
|
|
'department_id' => $department['id'],
|
|
'reference' => 'REF-HISTORY',
|
|
'created_at' => '2026-05-10 10:00:00',
|
|
'reg_1' => 'HIST1',
|
|
]);
|
|
api_fixtures()->createVehicle([
|
|
'customer_id' => $customer['customer_number'],
|
|
'type' => 53,
|
|
'reg' => 'VEH1',
|
|
'reference' => 'REF-VEHICLE',
|
|
'created_at' => '2026-05-08 12:00:00',
|
|
]);
|
|
|
|
api_fixtures()->createOrderBooking([
|
|
'customer_number' => $customer['customer_number'],
|
|
'department' => $department['id'],
|
|
'reference' => 'REF-SHARED',
|
|
'datetime' => '2026-05-15 11:00:00',
|
|
'reg_1' => 'SHARED1',
|
|
]);
|
|
api_fixtures()->createOrder([
|
|
'customer_id' => $customer['customer_number'],
|
|
'cashier_id' => $cashier['id'],
|
|
'department_id' => $department['id'],
|
|
'reference' => 'REF-SHARED',
|
|
'created_at' => '2026-05-12 10:00:00',
|
|
'reg_1' => 'SHARED1',
|
|
]);
|
|
api_fixtures()->createVehicle([
|
|
'customer_id' => $customer['customer_number'],
|
|
'type' => 53,
|
|
'reg' => 'SHARED1',
|
|
'reference' => 'REF-SHARED',
|
|
'created_at' => '2026-05-09 12:00:00',
|
|
]);
|
|
|
|
$deletedOrder = api_fixtures()->createOrder([
|
|
'customer_id' => $customer['customer_number'],
|
|
'cashier_id' => $cashier['id'],
|
|
'department_id' => $department['id'],
|
|
'reference' => 'REF-DELETED',
|
|
]);
|
|
api_test_runtime()->db()
|
|
->query("UPDATE orders SET deleted_at = '2026-05-10 12:00:00' WHERE id = " . (int)$deletedOrder['id']);
|
|
api_fixtures()->createOrder([
|
|
'customer_id' => $customer['customer_number'],
|
|
'cashier_id' => $cashier['id'],
|
|
'department_id' => $department['id'],
|
|
'reference' => '',
|
|
]);
|
|
|
|
$session = api_fixtures()->createUserSession([
|
|
'list_orders',
|
|
'department_access_' . $department['id'],
|
|
]);
|
|
|
|
$response = api_client()->get('/orders/reference-suggestions?' . http_build_query([
|
|
'search' => 'REF',
|
|
'department_id' => $department['id'],
|
|
'customer_id' => $customer['customer_number'],
|
|
'reg_1' => 'SHARED1',
|
|
'limit' => 10,
|
|
]), $session['headers']);
|
|
|
|
$response
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
$suggestions = $response->data();
|
|
expect($suggestions)->toBeArray();
|
|
|
|
$booking = reference_suggestion_by_reference($suggestions, 'REF-BOOKING');
|
|
$history = reference_suggestion_by_reference($suggestions, 'REF-HISTORY');
|
|
$vehicle = reference_suggestion_by_reference($suggestions, 'REF-VEHICLE');
|
|
$shared = reference_suggestion_by_reference($suggestions, 'REF-SHARED');
|
|
|
|
expect($booking['source'] ?? null)->toBe('booking');
|
|
expect($booking['source_created_at'] ?? null)->toBe('2026-05-13 09:00:00');
|
|
expect($history['source'] ?? null)->toBe('order');
|
|
expect($vehicle['source'] ?? null)->toBe('vehicle');
|
|
expect($shared['source'] ?? null)->toBe('booking');
|
|
expect($shared['section'] ?? null)->toBe('this_vehicle');
|
|
expect($booking['section'] ?? null)->toBe('other_customer_vehicle');
|
|
expect($history['section'] ?? null)->toBe('other_customer_vehicle');
|
|
expect($vehicle['section'] ?? null)->toBe('other_customer_vehicle');
|
|
expect($shared['usage_count'] ?? null)->toBe(3);
|
|
expect($shared['last_used_at'] ?? null)->toBe('2026-05-15 11:00:00');
|
|
expect(reference_suggestion_by_reference($suggestions, 'REF-DELETED'))->toBeNull();
|
|
expect(reference_suggestion_by_reference($suggestions, ''))->toBeNull();
|
|
});
|
|
|
|
it('orders reference suggestions by match relevance before context and frequency', function (): void {
|
|
$department = api_fixtures()->createDepartment();
|
|
$customer = api_fixtures()->createUser(['display_name' => 'Reference Ranking Customer']);
|
|
$cashier = api_fixtures()->createUser(['display_name' => 'Reference Ranking Cashier']);
|
|
|
|
api_fixtures()->createOrder([
|
|
'customer_id' => $customer['customer_number'],
|
|
'cashier_id' => $cashier['id'],
|
|
'department_id' => $department['id'],
|
|
'reference' => 'ABC',
|
|
'created_at' => '2026-05-01 08:00:00',
|
|
'reg_1' => 'OTHER1',
|
|
]);
|
|
api_fixtures()->createOrderBooking([
|
|
'customer_number' => $customer['customer_number'],
|
|
'department' => $department['id'],
|
|
'reference' => 'ABC-PREFIX',
|
|
'datetime' => '2026-05-16 08:00:00',
|
|
'reg_1' => 'MATCH1',
|
|
]);
|
|
api_fixtures()->createOrder([
|
|
'customer_id' => $customer['customer_number'],
|
|
'cashier_id' => $cashier['id'],
|
|
'department_id' => $department['id'],
|
|
'reference' => 'X-ABC-CONTAINS',
|
|
'created_at' => '2026-05-17 08:00:00',
|
|
'reg_1' => 'MATCH1',
|
|
]);
|
|
|
|
$session = api_fixtures()->createUserSession([
|
|
'list_orders',
|
|
'department_access_' . $department['id'],
|
|
]);
|
|
|
|
$response = api_client()->get('/orders/reference-suggestions?' . http_build_query([
|
|
'search' => 'ABC',
|
|
'department_id' => $department['id'],
|
|
'customer_id' => $customer['customer_number'],
|
|
'reg_1' => 'MATCH1',
|
|
]), $session['headers']);
|
|
|
|
$response
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
$references = array_map(
|
|
static fn(array $suggestion): string => (string)($suggestion['reference'] ?? ''),
|
|
is_array($response->data()) ? $response->data() : []
|
|
);
|
|
|
|
expect(array_slice($references, 0, 3))->toBe(['ABC', 'ABC-PREFIX', 'X-ABC-CONTAINS']);
|
|
});
|
|
|
|
it('enforces authentication, list permission, and department access for reference suggestions', function (): void {
|
|
api_test_covers('GET /orders/reference-suggestions', 'auth');
|
|
|
|
$department = api_fixtures()->createDepartment();
|
|
|
|
api_client()->get('/orders/reference-suggestions?department_id=' . $department['id'])
|
|
->assertStatus(400)
|
|
->assertEnvelope()
|
|
->assertSuccess(false)
|
|
->assertMessage('Invalid session');
|
|
|
|
$missingListPermission = api_fixtures()->createUserSession([
|
|
'department_access_' . $department['id'],
|
|
]);
|
|
api_client()->get('/orders/reference-suggestions?department_id=' . $department['id'], $missingListPermission['headers'])
|
|
->assertStatus(403)
|
|
->assertEnvelope()
|
|
->assertSuccess(false)
|
|
->assertMissingPermissions(['list_orders']);
|
|
|
|
$missingDepartmentAccess = api_fixtures()->createUserSession(['list_orders']);
|
|
api_client()->get('/orders/reference-suggestions?department_id=' . $department['id'], $missingDepartmentAccess['headers'])
|
|
->assertStatus(403)
|
|
->assertEnvelope()
|
|
->assertSuccess(false)
|
|
->assertMissingPermissions(['department_access_' . $department['id']]);
|
|
});
|