Enhance department pricing functionality and improve related tests
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -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');
|
||||
|
||||
|
||||
+50
@@ -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();
|
||||
});
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user