Allow customer order booking creation without booking permission
This commit is contained in:
@@ -250,11 +250,17 @@ class departmentsRoute
|
|||||||
$this->get('/departments/categories', function () {
|
$this->get('/departments/categories', function () {
|
||||||
// Require the user to be logged in
|
// Require the user to be logged in
|
||||||
global $response;
|
global $response;
|
||||||
self::requirePermission('list_department_categories');
|
$auth = new authentication();
|
||||||
// Get the user object
|
$user = $auth->get_user();
|
||||||
$user = (new authentication())->get_user();
|
$subuser = $auth->get_subuser();
|
||||||
// Check if the request was successful
|
// Check if the request was successful
|
||||||
if ($user) {
|
if ($user || $subuser) {
|
||||||
|
$isCustomerBookingSession = ($user && self::hasPermission('user')) || $subuser;
|
||||||
|
if (!$isCustomerBookingSession && !self::hasPermission('list_department_categories')) {
|
||||||
|
$this->emitForbidden(['list_department_categories']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$responsibleUserId = $user ? (int)$user->id : 0;
|
||||||
// Require the department id
|
// Require the department id
|
||||||
self::requireParameters(['id']);
|
self::requireParameters(['id']);
|
||||||
self::requireType((int)self::getParameter('id'), self::TYPE_INT());
|
self::requireType((int)self::getParameter('id'), self::TYPE_INT());
|
||||||
@@ -263,14 +269,14 @@ class departmentsRoute
|
|||||||
// Validate the department categories object
|
// Validate the department categories object
|
||||||
if (!$department->exists()) {
|
if (!$department->exists()) {
|
||||||
// Log the incident
|
// Log the incident
|
||||||
(new logs_o())->add('departments', 'global', 1, $user->id, 'LIST_DEPARTMENT_CATEGORIES', 'Department categories not found');
|
(new logs_o())->add('departments', 'global', 1, $responsibleUserId, 'LIST_DEPARTMENT_CATEGORIES', 'Department categories not found');
|
||||||
// Return an error
|
// Return an error
|
||||||
$response->error('Department categories not found', 400);
|
$response->error('Department categories not found', 400);
|
||||||
}
|
}
|
||||||
// Get the department categories
|
// Get the department categories
|
||||||
$department_categories = new department_categories_o();
|
$department_categories = new department_categories_o();
|
||||||
// Log the incident
|
// Log the incident
|
||||||
(new logs_o())->add('departments', 'global', 1, $user->id, 'LIST_DEPARTMENT_CATEGORIES', 'Successfully listed department categories');
|
(new logs_o())->add('departments', 'global', 1, $responsibleUserId, 'LIST_DEPARTMENT_CATEGORIES', 'Successfully listed department categories');
|
||||||
// Return the list of department categories
|
// Return the list of department categories
|
||||||
$response->success(
|
$response->success(
|
||||||
$department_categories
|
$department_categories
|
||||||
@@ -295,7 +301,7 @@ class departmentsRoute
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
'list_department_categories' => 'List all department categories'
|
'list_department_categories' => 'List all department categories. Authenticated customer booking sessions may read this endpoint without the permission.'
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -41,18 +41,9 @@ class orderBookingRoute
|
|||||||
$po = self::getTargetPo(); // String | Null
|
$po = self::getTargetPo(); // String | Null
|
||||||
$pickup = self::getTargetPickup(); // Bool | Null
|
$pickup = self::getTargetPickup(); // Bool | Null
|
||||||
$items = self::getTargetItems(); // Array of order_items_o objects
|
$items = self::getTargetItems(); // Array of order_items_o objects
|
||||||
/**
|
$this->requireOrderBookingCreateAccess(
|
||||||
* Permissions (clean helper)
|
|
||||||
*/
|
|
||||||
$permission_own = self::definePermission('add_own_bookings', subusers_permission_node_key::BOOKINGS_ADD);
|
|
||||||
$permission_other = self::definePermission('add_bookings');
|
|
||||||
self::allowOwnOrDepartmentAccess(
|
|
||||||
$permission_own,
|
|
||||||
$permission_other,
|
|
||||||
(int)$customer_number->customer_number->value(),
|
(int)$customer_number->customer_number->value(),
|
||||||
(int)$department->id,
|
(int)$department->id
|
||||||
null,
|
|
||||||
'You do not have permission to create this order booking.'
|
|
||||||
);
|
);
|
||||||
/**
|
/**
|
||||||
* Input data
|
* Input data
|
||||||
@@ -96,8 +87,7 @@ class orderBookingRoute
|
|||||||
$response->success($order_bookings_o->asArray());
|
$response->success($order_bookings_o->asArray());
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
'add_own_bookings' => 'Permission to create own order bookings. Subusers require node: BOOKINGS_ADD and X-Customer-Number header.',
|
'add_bookings' => 'Permission to create order bookings for another customer or department scope.'
|
||||||
'add_bookings' => 'Permission to create department order bookings.'
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -659,6 +649,34 @@ class orderBookingRoute
|
|||||||
return $object;
|
return $object;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function requireOrderBookingCreateAccess(int $targetCustomerNumber, int $departmentId): void
|
||||||
|
{
|
||||||
|
if ($this->isOrderBookingCustomerSession() && $this->isOwnCustomerContext($targetCustomerNumber)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$permissionOther = self::definePermission('add_bookings');
|
||||||
|
if (!self::hasPermission($permissionOther)) {
|
||||||
|
$this->emitForbidden([$permissionOther]);
|
||||||
|
}
|
||||||
|
|
||||||
|
self::requireDepartmentAccess((string)$departmentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isOrderBookingCustomerSession(): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$auth = new authentication();
|
||||||
|
if ($auth->get_subuser() !== false) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $auth->get_user() !== false && self::hasPermission('user');
|
||||||
|
} catch (Exception) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws Exception If the Department is invalid.
|
* @throws Exception If the Department is invalid.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -301,6 +301,42 @@ it('lists department categories for a department', function (): void {
|
|||||||
->and($response->data()[0]['category']['id'] ?? null)->toBe($category['id']);
|
->and($response->data()[0]['category']['id'] ?? null)->toBe($category['id']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('lets customer booking sessions list department categories without the management permission', function (): void {
|
||||||
|
api_test_covers('GET /departments/categories', 'auth');
|
||||||
|
|
||||||
|
$customerSession = api_fixtures()->createUserSession(['user']);
|
||||||
|
$department = api_fixtures()->createDepartment();
|
||||||
|
$category = api_fixtures()->createCategory([
|
||||||
|
'name' => 'Customer Department Category',
|
||||||
|
]);
|
||||||
|
api_fixtures()->linkDepartmentCategory((int)$department['id'], (int)$category['id']);
|
||||||
|
|
||||||
|
$customerResponse = api_client()->get('/departments/categories?id=' . $department['id'], $customerSession['headers']);
|
||||||
|
|
||||||
|
$customerResponse
|
||||||
|
->assertStatus(200)
|
||||||
|
->assertEnvelope()
|
||||||
|
->assertSuccess();
|
||||||
|
|
||||||
|
expect($customerResponse->data())
|
||||||
|
->toBeArray()
|
||||||
|
->toHaveCount(1)
|
||||||
|
->and($customerResponse->data()[0]['category']['id'] ?? null)->toBe($category['id']);
|
||||||
|
|
||||||
|
$subuserSession = api_fixtures()->createSubuserSession((int)$customerSession['user']['customer_number'], []);
|
||||||
|
$subuserResponse = api_client()->get('/departments/categories?id=' . $department['id'], $subuserSession['headers']);
|
||||||
|
|
||||||
|
$subuserResponse
|
||||||
|
->assertStatus(200)
|
||||||
|
->assertEnvelope()
|
||||||
|
->assertSuccess();
|
||||||
|
|
||||||
|
expect($subuserResponse->data())
|
||||||
|
->toBeArray()
|
||||||
|
->toHaveCount(1)
|
||||||
|
->and($subuserResponse->data()[0]['category']['id'] ?? null)->toBe($category['id']);
|
||||||
|
});
|
||||||
|
|
||||||
it('rejects invalid department category requests', function (): void {
|
it('rejects invalid department category requests', function (): void {
|
||||||
api_test_covers('GET /departments/categories', 'failure');
|
api_test_covers('GET /departments/categories', 'failure');
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
usesApiSuite();
|
||||||
|
|
||||||
|
function order_booking_create_payload(array $customer, array $department, array $product, string $reference): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'customer_number' => (int)$customer['customer_number'],
|
||||||
|
'department' => (int)$department['id'],
|
||||||
|
'reg_1' => $reference,
|
||||||
|
'datetime' => '2026-07-07 10:00:00',
|
||||||
|
'note' => '',
|
||||||
|
'reference' => $reference,
|
||||||
|
'po' => '',
|
||||||
|
'pickup' => false,
|
||||||
|
'items' => [
|
||||||
|
[
|
||||||
|
'id' => (int)$product['id'],
|
||||||
|
'quantity' => 1,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function order_booking_create_department(string $name): array
|
||||||
|
{
|
||||||
|
$branding = api_fixtures()->createBranding([
|
||||||
|
'name' => $name . ' Brand',
|
||||||
|
'address' => 'API Booking Street 1',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return api_fixtures()->createDepartment([
|
||||||
|
'name' => $name,
|
||||||
|
'branding' => (int)$branding['id'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
it('lets customers create their own order bookings without booking permissions', function (): void {
|
||||||
|
api_test_covers('POST /order-bookings', 'auth');
|
||||||
|
|
||||||
|
$session = api_fixtures()->createUserSession(['user']);
|
||||||
|
$department = order_booking_create_department('Own Booking Department');
|
||||||
|
$product = api_fixtures()->createProduct(['name' => 'Own Booking Product']);
|
||||||
|
|
||||||
|
$response = api_client()->post(
|
||||||
|
'/order-bookings',
|
||||||
|
order_booking_create_payload($session['user'], $department, $product, 'OWNBOOK1'),
|
||||||
|
$session['headers']
|
||||||
|
);
|
||||||
|
|
||||||
|
$response
|
||||||
|
->assertStatus(200)
|
||||||
|
->assertEnvelope()
|
||||||
|
->assertSuccess();
|
||||||
|
|
||||||
|
$bookingId = (int)($response->data()['id'] ?? 0);
|
||||||
|
expect($bookingId)->toBeGreaterThan(0);
|
||||||
|
|
||||||
|
$row = api_fixtures()->fetchRowById('order_bookings', $bookingId);
|
||||||
|
expect($row)->not->toBeNull();
|
||||||
|
expect((int)($row['customer_number'] ?? 0))->toBe((int)$session['user']['customer_number']);
|
||||||
|
|
||||||
|
api_fixtures()->cleanupDeleteById('order_bookings', $bookingId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lets subusers create own customer order bookings without the bookings add node', function (): void {
|
||||||
|
api_test_covers('POST /order-bookings', 'auth');
|
||||||
|
|
||||||
|
$customer = api_fixtures()->createUser(['display_name' => 'Subuser Booking Customer']);
|
||||||
|
$session = api_fixtures()->createSubuserSession((int)$customer['customer_number'], []);
|
||||||
|
$department = order_booking_create_department('Subuser Booking Department');
|
||||||
|
$product = api_fixtures()->createProduct(['name' => 'Subuser Booking Product']);
|
||||||
|
|
||||||
|
$response = api_client()->post(
|
||||||
|
'/order-bookings',
|
||||||
|
order_booking_create_payload($customer, $department, $product, 'SUBBOOK1'),
|
||||||
|
$session['headers']
|
||||||
|
);
|
||||||
|
|
||||||
|
$response
|
||||||
|
->assertStatus(200)
|
||||||
|
->assertEnvelope()
|
||||||
|
->assertSuccess();
|
||||||
|
|
||||||
|
$bookingId = (int)($response->data()['id'] ?? 0);
|
||||||
|
expect($bookingId)->toBeGreaterThan(0);
|
||||||
|
|
||||||
|
$row = api_fixtures()->fetchRowById('order_bookings', $bookingId);
|
||||||
|
expect($row)->not->toBeNull();
|
||||||
|
expect((int)($row['customer_number'] ?? 0))->toBe((int)$customer['customer_number']);
|
||||||
|
|
||||||
|
api_fixtures()->cleanupDeleteById('order_bookings', $bookingId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('still requires elevated access for creating another customer order booking', function (): void {
|
||||||
|
api_test_covers('POST /order-bookings', 'auth');
|
||||||
|
|
||||||
|
$session = api_fixtures()->createUserSession(['user']);
|
||||||
|
$otherCustomer = api_fixtures()->createUser(['display_name' => 'Other Booking Customer']);
|
||||||
|
$department = api_fixtures()->createDepartment(['name' => 'Other Booking Department']);
|
||||||
|
$product = api_fixtures()->createProduct(['name' => 'Other Booking Product']);
|
||||||
|
|
||||||
|
$response = api_client()->post(
|
||||||
|
'/order-bookings',
|
||||||
|
order_booking_create_payload($otherCustomer, $department, $product, 'OTHBOOK1'),
|
||||||
|
$session['headers']
|
||||||
|
);
|
||||||
|
|
||||||
|
$response
|
||||||
|
->assertStatus(403)
|
||||||
|
->assertEnvelope()
|
||||||
|
->assertSuccess(false)
|
||||||
|
->assertMissingPermissions(['add_bookings']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lets department-scoped users create order bookings for another customer', function (): void {
|
||||||
|
api_test_covers('POST /order-bookings', 'happy');
|
||||||
|
|
||||||
|
$customer = api_fixtures()->createUser(['display_name' => 'Department Booking Customer']);
|
||||||
|
$department = order_booking_create_department('Department Scoped Booking Department');
|
||||||
|
$product = api_fixtures()->createProduct(['name' => 'Department Scoped Booking Product']);
|
||||||
|
$session = api_fixtures()->createUserSession([
|
||||||
|
'add_bookings',
|
||||||
|
'department_access_' . $department['id'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = api_client()->post(
|
||||||
|
'/order-bookings',
|
||||||
|
order_booking_create_payload($customer, $department, $product, 'DEPTBOOK'),
|
||||||
|
$session['headers']
|
||||||
|
);
|
||||||
|
|
||||||
|
$response
|
||||||
|
->assertStatus(200)
|
||||||
|
->assertEnvelope()
|
||||||
|
->assertSuccess();
|
||||||
|
|
||||||
|
$bookingId = (int)($response->data()['id'] ?? 0);
|
||||||
|
expect($bookingId)->toBeGreaterThan(0);
|
||||||
|
|
||||||
|
api_fixtures()->cleanupDeleteById('order_bookings', $bookingId);
|
||||||
|
});
|
||||||
@@ -147,7 +147,7 @@ it('only allows tankcleaning products for only tankcleaning customers', function
|
|||||||
->assertStatus(400)
|
->assertStatus(400)
|
||||||
->assertEnvelope()
|
->assertEnvelope()
|
||||||
->assertSuccess(false)
|
->assertSuccess(false)
|
||||||
->assertMessage(\classes\customer_order_product_policy::ONLY_TANKCLEANING_MESSAGE);
|
->assertMessage(\classes\customer_product_rule_service::BLOCK_MESSAGE);
|
||||||
|
|
||||||
$response = api_client()->post('/order/items', [
|
$response = api_client()->post('/order/items', [
|
||||||
'order_id' => $order['id'],
|
'order_id' => $order['id'],
|
||||||
@@ -274,7 +274,9 @@ it('blocks addon products added as standalone additional order items for custome
|
|||||||
->assertEnvelope()
|
->assertEnvelope()
|
||||||
->assertSuccess();
|
->assertSuccess();
|
||||||
|
|
||||||
post_order_item($fixture['order'], $addonProduct, $fixture['session']['headers'])
|
post_order_item($fixture['order'], $addonProduct, $fixture['session']['headers'], [
|
||||||
|
'notes' => 'Addon customer rule check',
|
||||||
|
])
|
||||||
->assertStatus(400)
|
->assertStatus(400)
|
||||||
->assertEnvelope()
|
->assertEnvelope()
|
||||||
->assertSuccess(false)
|
->assertSuccess(false)
|
||||||
|
|||||||
Reference in New Issue
Block a user