Enhance department pricing functionality and improve related tests

This commit is contained in:
Jeppe Bundgaard
2026-07-08 09:35:40 +02:00
parent b77efc538a
commit f0b5479f30
7 changed files with 170 additions and 11 deletions
File diff suppressed because one or more lines are too long
+24 -5
View File
@@ -22,7 +22,7 @@ class productsRoute
*/
private function getCustomerIfProvided(bool $restrictToOwnCustomer = false): ?users_o
{
$customerId = $this->getOptionalPositiveIntParameter('customer_id');
$customerId = $this->getCustomerIdIfProvided();
if ($customerId === null) {
return null;
}
@@ -46,6 +46,11 @@ class productsRoute
return null;
}
private function getCustomerIdIfProvided(): ?int
{
return $this->getOptionalPositiveIntParameter('customer_id');
}
/**
* Get the department id if the department_id parameter is provided (In the request 'department_id')
* @return int|null
@@ -111,6 +116,15 @@ class productsRoute
return $isCustomerBookingSession && !$hasListProductsPermission;
}
private function canUseCustomerBookingDepartmentPricing(bool $isCustomerBookingSession, bool $useFinalPrice, ?int $customerId): bool
{
if (!$isCustomerBookingSession || !$useFinalPrice) {
return false;
}
return $customerId === null || $this->isOwnCustomerContext($customerId);
}
private function isBookingVisibleProduct(array $product): bool
{
return (bool)($product['display_in_booking_form'] ?? false);
@@ -225,11 +239,11 @@ class productsRoute
'name' => (string)$product['name'],
'description' => (string)$product['description'],
'price' => (int)$product['price'],
'subscription_allowed' => (boolean)$product['subscription_allowed'],
'subscription_allowed' => (bool)$product['subscription_allowed'],
'category' => (int)$product['category'],
'piktogram' => (string)$product['piktogram'],
'economic_product_id' => (int)$product['economic_product_id'],
'apply_category_discount' => (boolean)$product['apply_category_discount'],
'apply_category_discount' => (bool)$product['apply_category_discount'],
'requires_note' => \objects\products_o::productDataRequiresOrderItemNote($product),
'created_at' => (string)$product['created_at'],
'updated_at' => (string)$product['updated_at'],
@@ -274,14 +288,19 @@ class productsRoute
if ($hasAuthenticatedUser || $isSubuserSession || $isProductDetailsRestricted) {
// Define the variables
$restrictCustomerBookingProducts = $this->shouldRestrictCustomerBookingProducts($isCustomerBookingSession, $hasListProductsPermission);
$useFinalPrice = self::isParametersSet(['final_price']) && self::getParameter('final_price') === 'true';
$customerId = $this->getCustomerIdIfProvided();
$customer = $this->getCustomerIfProvided($restrictCustomerBookingProducts); // This is only used if the customer_id parameter is provided
$departmentId = $this->getDepartmentIdIfProvided(); // This is only used if the department_id parameter is provided
if ($departmentId !== null && !$restrictCustomerBookingProducts) {
if (
$departmentId !== null
&& !$restrictCustomerBookingProducts
&& !$this->canUseCustomerBookingDepartmentPricing($isCustomerBookingSession, $useFinalPrice, $customerId)
) {
self::requireDepartmentAccess((string)$departmentId);
}
$category = $this->getCategoryIfProvided(); // This is only used if the category parameter is provided (ID of the category)
$productId = $this->getProductIdIfProvided(); // This is only used if the id parameter is provided (ID of the product)
$useFinalPrice = self::isParametersSet(['final_price']) && self::getParameter('final_price') === 'true';
// Check if the "final_price" parameter is set, and true.
if ($useFinalPrice) {
// Determine the products to return
@@ -41,9 +41,9 @@ class superuserDepartmentRoute
// Log the incident
(new logs_o())->add('departments', 'global', 1, $user->id, 'SUPERUSER_FETCH_DEPARTMENT', 'Successfully fetched department');
// Return the department
$response->success(
(new departments_o())->getDepartmentById((int)$this->fromRequest('department_id'))
);
$department = (new departments_o())->getDepartmentById((int)$this->fromRequest('department_id'));
$department['custom_pricing_only'] = (bool)(int)($department['custom_pricing_only'] ?? 0);
$response->success($department);
} else {
// Log the incident
(new logs_o())->add('departments', 'global', 1, 0, 'SUPERUSER_FETCH_DEPARTMENT', 'No user found, or invalid session');
@@ -163,6 +163,39 @@ it('rejects department listing when the permission is missing', function (): voi
->assertMissingPermissions(['list_departments']);
});
it('returns superuser department custom pricing state as a boolean', function (): void {
$session = api_fixtures()->createUserSession(['superuser_fetch_department']);
$fallbackDepartment = api_fixtures()->createDepartment([
'name' => 'Fallback Pricing Department',
'custom_pricing_only' => 0,
]);
$customOnlyDepartment = api_fixtures()->createDepartment([
'name' => 'Custom Only Pricing Department',
'custom_pricing_only' => 1,
]);
$fallbackResponse = api_client()->get(
'/superuser/department?department_id=' . $fallbackDepartment['id'],
$session['headers']
);
$customOnlyResponse = api_client()->get(
'/superuser/department?department_id=' . $customOnlyDepartment['id'],
$session['headers']
);
$fallbackResponse
->assertStatus(200)
->assertEnvelope()
->assertSuccess();
$customOnlyResponse
->assertStatus(200)
->assertEnvelope()
->assertSuccess();
expect($fallbackResponse->data()['custom_pricing_only'] ?? null)->toBeFalse();
expect($customOnlyResponse->data()['custom_pricing_only'] ?? null)->toBeTrue();
});
it('creates departments through the real endpoint', function (): void {
api_test_covers('POST /departments', 'happy');
@@ -122,6 +122,62 @@ it('lets customer booking sessions list public products with final department pr
->not->toContain('department_access_' . (int)$department['id']);
});
it('lets customer booking sessions with list_products read own final department pricing without department access', function (): void {
api_test_covers('GET /products', 'customer-booking-auth');
$department = api_fixtures()->createDepartment(['name' => 'Own Customer Booking Pricing Department']);
$product = api_fixtures()->createProduct([
'name' => 'Own Customer Booking Wash',
'price' => 800,
'is_wash' => 1,
'display_in_booking_form' => 1,
]);
products_api_department_price((int)$department['id'], (int)$product['id'], 725);
$session = api_fixtures()->createUserSession(['user', 'list_products']);
$response = api_client()->get(
'/products?department_id=' . (int)$department['id']
. '&final_price=true'
. '&customer_id=' . (int)$session['user']['customer_number'],
$session['headers']
);
$response
->assertStatus(200)
->assertEnvelope()
->assertSuccess();
$productsById = [];
foreach ($response->data() as $returnedProduct) {
$productsById[(int)($returnedProduct['id'] ?? 0)] = $returnedProduct;
}
expect($productsById)
->toHaveKey((int)$product['id'])
->and((int)($productsById[(int)$product['id']]['price'] ?? 0))->toBe(725);
expect($response->body)
->not->toContain('department_access_' . (int)$department['id']);
});
it('still requires department access when list_products users request another customer final department pricing', function (): void {
api_test_covers('GET /products', 'customer-booking-auth');
$department = api_fixtures()->createDepartment(['name' => 'Other Customer Pricing Department']);
$otherCustomer = api_fixtures()->createUser();
$session = api_fixtures()->createUserSession(['user', 'list_products']);
api_client()->get(
'/products?department_id=' . (int)$department['id']
. '&final_price=true'
. '&customer_id=' . (int)$otherCustomer['customer_number'],
$session['headers']
)
->assertStatus(403)
->assertEnvelope()
->assertSuccess(false)
->assertMissingPermissions(['department_access_' . (int)$department['id']]);
});
it('prevents customer booking sessions from requesting another customer product pricing', function (): void {
api_test_covers('GET /products', 'customer-booking-auth');
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
use routes\productsRoute;
function products_route_customer_booking_department_pricing_allowed(
bool $isCustomerBookingSession,
bool $useFinalPrice,
?int $customerId,
bool $isOwnCustomer = true
): bool {
$route = new class ($isOwnCustomer) extends productsRoute {
public function __construct(private readonly bool $isOwnCustomer)
{
parent::__construct();
}
public function isOwnCustomerContext(int $targetCustomerNumber): bool
{
return $this->isOwnCustomer;
}
};
$method = new ReflectionMethod(productsRoute::class, 'canUseCustomerBookingDepartmentPricing');
return (bool)$method->invoke($route, $isCustomerBookingSession, $useFinalPrice, $customerId);
}
it('allows customer booking final department pricing for the authenticated customer', function (): void {
expect(products_route_customer_booking_department_pricing_allowed(true, true, 35131752, true))
->toBeTrue();
});
it('allows customer booking final department pricing when no customer id is requested', function (): void {
expect(products_route_customer_booking_department_pricing_allowed(true, true, null))
->toBeTrue();
});
it('does not allow customer booking department pricing for another customer', function (): void {
expect(products_route_customer_booking_department_pricing_allowed(true, true, 35131752, false))
->toBeFalse();
});
it('does not bypass department access outside final price booking reads', function (): void {
expect(products_route_customer_booking_department_pricing_allowed(true, false, 35131752, true))
->toBeFalse()
->and(products_route_customer_booking_department_pricing_allowed(false, true, 35131752, true))
->toBeFalse();
});
+3 -2
View File
@@ -55,6 +55,7 @@ trait route_t
} catch (Exception $e) {
$response->error($e->getMessage(), 400);
}
return false;
}
/**
@@ -486,7 +487,7 @@ trait route_t
* @param string|permission_node $permission
* @return bool
*/
public function hasPermission(string|permission_node $permission, int $customer_number = null): bool
public function hasPermission(string|permission_node $permission, ?int $customer_number = null): bool
{
return $this->evaluatePermission($permission, $customer_number, false);
}
@@ -611,7 +612,7 @@ trait route_t
* @param string|null $parameter The name of the parameter
* @return void
*/
public function requireParameterIntPositive(int $value, string $parameter = null): void
public function requireParameterIntPositive(int $value, ?string $parameter = null): void
{
global $response;
if ($value <= 0) {