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.
351 lines
14 KiB
PHP
351 lines
14 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use classes\limited_backoffice_service;
|
|
|
|
usesApiSuite();
|
|
|
|
function department_customer_pricing_price_insert(int $departmentId, int $productId, int $price): void
|
|
{
|
|
$statement = api_test_runtime()->db()->prepare(
|
|
'INSERT INTO `product_department_prices` (`department_id`, `product_id`, `price`)
|
|
VALUES (?, ?, ?)
|
|
ON DUPLICATE KEY UPDATE `price` = VALUES(`price`)'
|
|
);
|
|
$statement->bind_param('iii', $departmentId, $productId, $price);
|
|
$statement->execute();
|
|
$statement->close();
|
|
|
|
api_fixtures()->cleanupDeleteWhere('product_department_prices', [
|
|
'department_id' => $departmentId,
|
|
'product_id' => $productId,
|
|
]);
|
|
}
|
|
|
|
function department_customer_pricing_setup(array $departmentAttributes = []): array
|
|
{
|
|
$department = api_fixtures()->createDepartment([
|
|
'name' => 'Scoped Customer Pricing Department',
|
|
'custom_pricing_only' => 1,
|
|
...$departmentAttributes,
|
|
]);
|
|
$category = api_fixtures()->createCategory(['name' => 'Scoped Customer Pricing Category']);
|
|
$product = api_fixtures()->createProduct([
|
|
'name' => 'Scoped Customer Pricing Product',
|
|
'category' => $category['id'],
|
|
'price' => 1000,
|
|
'apply_category_discount' => 1,
|
|
]);
|
|
api_fixtures()->linkDepartmentCategory((int)$department['id'], (int)$category['id']);
|
|
department_customer_pricing_price_insert((int)$department['id'], (int)$product['id'], 1000);
|
|
$customer = api_fixtures()->createUser(['display_name' => 'Scoped Customer Pricing Customer']);
|
|
|
|
return [
|
|
'department' => $department,
|
|
'category' => $category,
|
|
'product' => $product,
|
|
'customer' => $customer,
|
|
];
|
|
}
|
|
|
|
it('rejects department customer pricing when custom-only pricing is disabled', function (): void {
|
|
api_test_covers('GET /superuser/department/customer-pricing', 'validation');
|
|
|
|
$fixture = department_customer_pricing_setup(['custom_pricing_only' => 0]);
|
|
$session = api_fixtures()->createUserSession(['superuser_fetch_department_customer_pricing']);
|
|
|
|
$response = api_client()->get(
|
|
'/superuser/department/customer-pricing?department_id=' . (int)$fixture['department']['id'] .
|
|
'&user_id=' . (int)$fixture['customer']['id'],
|
|
$session['headers']
|
|
);
|
|
|
|
$response
|
|
->assertStatus(409)
|
|
->assertEnvelope()
|
|
->assertSuccess(false);
|
|
|
|
expect($response->data()['code'] ?? null)->toBe('department_customer_pricing_disabled');
|
|
});
|
|
|
|
it('sets and applies department-specific customer discounts without legacy fallback', function (): void {
|
|
api_test_covers('GET /superuser/department/customer-pricing', 'happy');
|
|
api_test_covers('PUT /superuser/department/customer-pricing', 'happy');
|
|
api_test_covers('GET /products', 'pricing');
|
|
|
|
$fixture = department_customer_pricing_setup();
|
|
api_fixtures()->createPriceOverride([
|
|
'user_id' => $fixture['customer']['id'],
|
|
'is_category' => 0,
|
|
'product_or_category_id' => (string)$fixture['product']['id'],
|
|
'percentage' => 80,
|
|
]);
|
|
|
|
$session = api_fixtures()->createUserSession([
|
|
'superuser_fetch_department_customer_pricing',
|
|
'superuser_set_department_customer_pricing',
|
|
'list_products',
|
|
'department_access_' . (int)$fixture['department']['id'],
|
|
]);
|
|
|
|
$updated = api_client()->put('/superuser/department/customer-pricing', [
|
|
'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'],
|
|
'discount' => 25,
|
|
'fixed_price' => null,
|
|
],
|
|
],
|
|
], $session['headers']);
|
|
|
|
$updated
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
$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(
|
|
'/superuser/department/customer-pricing?department_id=' . (int)$fixture['department']['id'] .
|
|
'&customer_number=' . (int)$fixture['customer']['customer_number'],
|
|
$session['headers']
|
|
);
|
|
$byCustomerNumber
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
expect($byCustomerNumber->data()['customer']['id'] ?? null)->toBe((int)$fixture['customer']['id']);
|
|
|
|
$productResponse = api_client()->get(
|
|
'/products?final_price=true&id=' . (int)$fixture['product']['id'] .
|
|
'&department_id=' . (int)$fixture['department']['id'] .
|
|
'&customer_id=' . (int)$fixture['customer']['customer_number'],
|
|
$session['headers']
|
|
);
|
|
|
|
$productResponse
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
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');
|
|
|
|
$fixture = department_customer_pricing_setup();
|
|
$otherFixture = department_customer_pricing_setup(['name' => 'Denied Scoped Customer Pricing Department']);
|
|
$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'],
|
|
]);
|
|
|
|
api_client()
|
|
->get(
|
|
'/limited-backoffice/departments/' . (int)$fixture['department']['id'] .
|
|
'/customer-pricing?user_id=' . (int)$fixture['customer']['id'],
|
|
api_fixtures()->createUserSession([
|
|
limited_backoffice_service::PERMISSION_ACCESS,
|
|
'department_access_' . (int)$fixture['department']['id'],
|
|
])['headers']
|
|
)
|
|
->assertStatus(403)
|
|
->assertEnvelope()
|
|
->assertSuccess(false)
|
|
->assertMissingPermissions([limited_backoffice_service::PERMISSION_VIEW_CUSTOMER_PRICING]);
|
|
|
|
api_client()
|
|
->get(
|
|
'/limited-backoffice/departments/' . (int)$otherFixture['department']['id'] .
|
|
'/customer-pricing?user_id=' . (int)$otherFixture['customer']['id'],
|
|
$session['headers']
|
|
)
|
|
->assertStatus(403)
|
|
->assertEnvelope()
|
|
->assertSuccess(false)
|
|
->assertMissingPermissions(['department_access_' . (int)$otherFixture['department']['id']]);
|
|
|
|
api_client()
|
|
->put(
|
|
'/limited-backoffice/departments/' . (int)$fixture['department']['id'] . '/customer-pricing',
|
|
[
|
|
'user_id' => $fixture['customer']['id'],
|
|
'overrides' => [],
|
|
],
|
|
api_fixtures()->createUserSession([
|
|
limited_backoffice_service::PERMISSION_ACCESS,
|
|
limited_backoffice_service::PERMISSION_VIEW_CUSTOMER_PRICING,
|
|
'department_access_' . (int)$fixture['department']['id'],
|
|
])['headers']
|
|
)
|
|
->assertStatus(403)
|
|
->assertEnvelope()
|
|
->assertSuccess(false)
|
|
->assertMissingPermissions([limited_backoffice_service::PERMISSION_MANAGE_CUSTOMER_PRICING]);
|
|
|
|
$updated = api_client()->put(
|
|
'/limited-backoffice/departments/' . (int)$fixture['department']['id'] . '/customer-pricing',
|
|
[
|
|
'user_id' => $fixture['customer']['id'],
|
|
'overrides' => [
|
|
[
|
|
'is_category' => true,
|
|
'product_or_category_id' => (string)$fixture['category']['id'],
|
|
'discount' => 30,
|
|
],
|
|
],
|
|
],
|
|
$session['headers']
|
|
);
|
|
|
|
$updated
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect($updated->data()['overrides'][0]['percentage'] ?? null)->toBe(30);
|
|
expect($updated->data()['categories'][0]['products'][0]['effective_price'] ?? null)->toBe(700);
|
|
});
|