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); });