Harden Sæby demo registration and department scope (#335)

Complete and secure public customer/driver registration, authoritative limited-backoffice department scope, one-time employee QR login, and pricing concurrency for the Sæby demo.
This commit is contained in:
Jeppe B
2026-08-02 11:50:56 +02:00
committed by GitHub
parent 4587bdfb06
commit 1e0e051775
34 changed files with 1853 additions and 474 deletions
@@ -93,6 +93,16 @@ it('sets and applies department-specific customer discounts without legacy fallb
'department_id' => $fixture['department']['id'],
'user_id' => $fixture['customer']['id'],
'overrides' => [
[
'is_category' => true,
'product_or_category_id' => (string)$fixture['category']['id'],
'discount' => 60,
],
[
'is_category' => true,
'product_or_category_id' => 'global',
'discount' => 80,
],
[
'is_category' => false,
'product_or_category_id' => $fixture['product']['id'],
@@ -107,7 +117,12 @@ it('sets and applies department-specific customer discounts without legacy fallb
->assertEnvelope()
->assertSuccess();
expect($updated->data()['overrides'][0]['percentage'] ?? null)->toBe(25);
$productOverrides = array_values(array_filter(
$updated->data()['overrides'],
static fn(array $override): bool => $override['is_category'] === false
));
expect($productOverrides)->toHaveCount(1);
expect($productOverrides[0]['percentage'] ?? null)->toBe(25);
expect($updated->data()['categories'][0]['products'][0]['effective_price'] ?? null)->toBe(750);
$byCustomerNumber = api_client()->get(
@@ -136,6 +151,124 @@ it('sets and applies department-specific customer discounts without legacy fallb
expect((int)($productResponse->data()['price'] ?? 0))->toBe(750);
});
it('lets only the first writer replace customer pricing for a shared revision', function (): void {
api_test_covers('GET /limited-backoffice/departments/{departmentId}/customer-pricing', 'revision');
api_test_covers('PUT /limited-backoffice/departments/{departmentId}/customer-pricing', 'revision conflict');
$fixture = department_customer_pricing_setup();
$session = api_fixtures()->createUserSession([
limited_backoffice_service::PERMISSION_ACCESS,
limited_backoffice_service::PERMISSION_VIEW_CUSTOMER_PRICING,
limited_backoffice_service::PERMISSION_MANAGE_CUSTOMER_PRICING,
'department_access_' . (int)$fixture['department']['id'],
]);
$path = '/limited-backoffice/departments/' . (int)$fixture['department']['id'] . '/customer-pricing';
$query = '?user_id=' . (int)$fixture['customer']['id'];
$initial = api_client()->get($path . $query, $session['headers']);
$initial->assertStatus(200)->assertEnvelope()->assertSuccess();
$sharedRevision = $initial->data()['revision'] ?? null;
expect($sharedRevision)->toBeString()->toMatch('/^[a-f0-9]{64}$/');
$winner = api_client()->put($path, [
'user_id' => (int)$fixture['customer']['id'],
'expected_revision' => $sharedRevision,
'overrides' => [[
'is_category' => false,
'product_or_category_id' => (int)$fixture['product']['id'],
'discount' => 20,
]],
], $session['headers']);
$winner->assertStatus(200)->assertEnvelope()->assertSuccess();
$winningRevision = $winner->data()['revision'] ?? null;
expect($winningRevision)->toBeString()->not->toBe($sharedRevision);
$stale = api_client()->put($path, [
'user_id' => (int)$fixture['customer']['id'],
'expected_revision' => $sharedRevision,
'overrides' => [[
'is_category' => false,
'product_or_category_id' => (int)$fixture['product']['id'],
'discount' => 70,
]],
], $session['headers']);
$stale
->assertStatus(409)
->assertEnvelope()
->assertSuccess(false)
->assertMessage('Pricing has changed. Reload and try again.');
expect($stale->data()['code'] ?? null)->toBe('pricing_revision_conflict');
expect($stale->data()['current_revision'] ?? null)->toBe($winningRevision);
$reloaded = api_client()->get($path . $query, $session['headers']);
$reloaded->assertStatus(200)->assertEnvelope()->assertSuccess();
expect($reloaded->data()['revision'] ?? null)->toBe($winningRevision);
expect($reloaded->data()['overrides'])->toHaveCount(1);
expect($reloaded->data()['overrides'][0]['percentage'] ?? null)->toBe(20);
expect($reloaded->data()['categories'][0]['products'][0]['effective_price'] ?? null)->toBe(800);
});
it('rejects ambiguous and duplicate customer price overrides without changing saved pricing', function (): void {
api_test_covers('PUT /limited-backoffice/departments/{departmentId}/customer-pricing', 'validation');
$fixture = department_customer_pricing_setup();
$session = api_fixtures()->createUserSession([
limited_backoffice_service::PERMISSION_ACCESS,
limited_backoffice_service::PERMISSION_VIEW_CUSTOMER_PRICING,
limited_backoffice_service::PERMISSION_MANAGE_CUSTOMER_PRICING,
'department_access_' . (int)$fixture['department']['id'],
]);
$path = '/limited-backoffice/departments/' . (int)$fixture['department']['id'] . '/customer-pricing';
$initial = api_client()->get(
$path . '?user_id=' . (int)$fixture['customer']['id'],
$session['headers']
);
$revision = $initial->data()['revision'];
foreach ([
[[
'is_category' => false,
'product_or_category_id' => (int)$fixture['product']['id'],
'discount' => 10,
'fixed_price' => 500,
]],
[[
'is_category' => true,
'product_or_category_id' => (string)$fixture['category']['id'],
'discount' => 10,
'fixed_price' => 500,
]],
[
[
'is_category' => false,
'product_or_category_id' => (int)$fixture['product']['id'],
'discount' => 10,
],
[
'is_category' => false,
'product_or_category_id' => (int)$fixture['product']['id'],
'discount' => 20,
],
],
] as $overrides) {
api_client()->put($path, [
'user_id' => (int)$fixture['customer']['id'],
'expected_revision' => $revision,
'overrides' => $overrides,
], $session['headers'])
->assertStatus(400)
->assertEnvelope()
->assertSuccess(false);
}
$unchanged = api_client()->get(
$path . '?user_id=' . (int)$fixture['customer']['id'],
$session['headers']
);
expect($unchanged->data()['revision'] ?? null)->toBe($revision);
expect($unchanged->data()['overrides'])->toBe([]);
});
it('limits department customer pricing to assigned limited-backoffice departments', function (): void {
api_test_covers('GET /limited-backoffice/departments/{departmentId}/customer-pricing', 'auth');
api_test_covers('PUT /limited-backoffice/departments/{departmentId}/customer-pricing', 'auth');