Fix booking PO default tenant validation
This commit is contained in:
@@ -58,6 +58,9 @@ class orders_schema_bootstrap
|
||||
|| !self::columnExists($db, 'orders', 'booking_id')
|
||||
|| !self::columnExists($db, 'orders', 'po')
|
||||
|| !self::columnExists($db, 'order_bookings', 'po')
|
||||
|| !self::columnExists($db, 'order_bookings', 'customer_number')
|
||||
|| !self::columnExists($db, 'order_bookings', 'department')
|
||||
|| !self::columnExists($db, 'order_bookings', 'deleted_at')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -65,6 +68,9 @@ class orders_schema_bootstrap
|
||||
$db->query(
|
||||
"UPDATE orders o
|
||||
INNER JOIN order_bookings b ON b.id = o.booking_id
|
||||
AND b.customer_number = o.customer_id
|
||||
AND b.department = o.department_id
|
||||
AND b.deleted_at IS NULL
|
||||
SET o.po = b.po
|
||||
WHERE o.booking_id IS NOT NULL
|
||||
AND o.booking_id > 0
|
||||
|
||||
@@ -198,7 +198,9 @@ class ordersRoute
|
||||
$po = $this->resolveOrderPoForBookingDefault(
|
||||
array_key_exists('po', $data) ? $data['po'] : null,
|
||||
array_key_exists('po', $data),
|
||||
$bookingId
|
||||
$bookingId,
|
||||
(int)$data['customer_id'],
|
||||
(int)$data['department_id']
|
||||
);
|
||||
$new_data = [
|
||||
'customer_id' => (int)$data['customer_id'],
|
||||
@@ -1251,14 +1253,20 @@ class ordersRoute
|
||||
}
|
||||
}
|
||||
|
||||
private function resolveOrderPoForBookingDefault(mixed $po, bool $poProvided, ?int $bookingId): ?string
|
||||
private function resolveOrderPoForBookingDefault(
|
||||
mixed $po,
|
||||
bool $poProvided,
|
||||
?int $bookingId,
|
||||
int $customerNumber,
|
||||
int $departmentId
|
||||
): ?string
|
||||
{
|
||||
$currentPo = is_scalar($po) || $po === null ? trim((string)$po) : '';
|
||||
if ($currentPo !== '') {
|
||||
return $currentPo;
|
||||
}
|
||||
|
||||
$bookingPo = $this->getBookingPoDefault($bookingId);
|
||||
$bookingPo = $this->getBookingPoDefault($bookingId, $customerNumber, $departmentId);
|
||||
if ($bookingPo !== null) {
|
||||
return $bookingPo;
|
||||
}
|
||||
@@ -1273,7 +1281,11 @@ class ordersRoute
|
||||
return;
|
||||
}
|
||||
|
||||
$bookingPo = $this->getBookingPoDefault($bookingId ?? (int)($order->booking_id->value() ?? 0));
|
||||
$bookingPo = $this->getBookingPoDefault(
|
||||
$bookingId ?? (int)($order->booking_id->value() ?? 0),
|
||||
(int)$order->customer_id->value(),
|
||||
(int)$order->department_id->value()
|
||||
);
|
||||
if ($bookingPo === null) {
|
||||
return;
|
||||
}
|
||||
@@ -1281,9 +1293,13 @@ class ordersRoute
|
||||
$order->po->set($bookingPo);
|
||||
}
|
||||
|
||||
private function getBookingPoDefault(?int $bookingId): ?string
|
||||
private function getBookingPoDefault(?int $bookingId, int $customerNumber, int $departmentId): ?string
|
||||
{
|
||||
if ($bookingId === null || $bookingId <= 0) {
|
||||
if ($bookingId === null || $bookingId <= 0 || $customerNumber <= 0 || $departmentId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$this->canUseBookingPoDefault($customerNumber, $departmentId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1293,6 +1309,18 @@ class ordersRoute
|
||||
return null;
|
||||
}
|
||||
|
||||
if ((int)$booking->customer_number->value() !== $customerNumber) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ((int)$booking->department->value() !== $departmentId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (trim((string)($booking->deleted_at->value() ?? '')) !== '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$bookingPo = trim((string)($booking->po->value() ?? ''));
|
||||
return $bookingPo !== '' ? $bookingPo : null;
|
||||
} catch (\Throwable) {
|
||||
@@ -1300,6 +1328,20 @@ class ordersRoute
|
||||
}
|
||||
}
|
||||
|
||||
private function canUseBookingPoDefault(int $customerNumber, int $departmentId): bool
|
||||
{
|
||||
try {
|
||||
$user = (new authentication())->get_user();
|
||||
if ($user !== false && isset($user->customer_number) && (int)$user->customer_number->value() === $customerNumber) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->hasDepartmentAccess((string)$departmentId);
|
||||
} catch (\Throwable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function normalizeLegacyEditableFieldPayload(array $data, response $response): array
|
||||
{
|
||||
if (!array_key_exists('field', $data) && !array_key_exists('value', $data)) {
|
||||
|
||||
@@ -102,6 +102,132 @@ it('creates orders through the orders endpoint', function (): void {
|
||||
api_fixtures()->cleanupDeleteById('orders', $orderId);
|
||||
});
|
||||
|
||||
|
||||
it('defaults order PO only from a matching active booking', function (): void {
|
||||
api_test_covers('POST /orders', 'security');
|
||||
api_test_covers('PUT /orders', 'security');
|
||||
|
||||
$department = api_fixtures()->createDepartment(['name' => 'Order Booking PO Department']);
|
||||
$otherDepartment = api_fixtures()->createDepartment(['name' => 'Order Booking PO Other Department']);
|
||||
$customer = api_fixtures()->createUser(['display_name' => 'Order Booking PO Customer']);
|
||||
$otherCustomer = api_fixtures()->createUser(['display_name' => 'Order Booking PO Other Customer']);
|
||||
$cashier = api_fixtures()->createUser(['display_name' => 'Order Booking PO Cashier']);
|
||||
$matchingBooking = api_fixtures()->createOrderBooking([
|
||||
'customer_number' => $customer['customer_number'],
|
||||
'department' => $department['id'],
|
||||
'po' => 'MATCHING-BOOKING-PO',
|
||||
]);
|
||||
$foreignBooking = api_fixtures()->createOrderBooking([
|
||||
'customer_number' => $otherCustomer['customer_number'],
|
||||
'department' => $otherDepartment['id'],
|
||||
'po' => 'FOREIGN-BOOKING-PO',
|
||||
]);
|
||||
$deletedBooking = api_fixtures()->createOrderBooking([
|
||||
'customer_number' => $customer['customer_number'],
|
||||
'department' => $department['id'],
|
||||
'po' => 'DELETED-BOOKING-PO',
|
||||
'deleted_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
$session = api_fixtures()->createUserSession(['add_order', 'edit_order'], [
|
||||
'customer_number' => $customer['customer_number'],
|
||||
]);
|
||||
|
||||
$createResponse = api_client()->post('/orders', [
|
||||
'customer_id' => $customer['customer_number'],
|
||||
'department_id' => $department['id'],
|
||||
'reference' => 'ORDER-BOOKING-PO-MATCH',
|
||||
'notes' => 'Created with matching booking',
|
||||
'reg_1' => 'MATCHPO',
|
||||
'booking_id' => $matchingBooking['id'],
|
||||
], $session['headers']);
|
||||
|
||||
$createResponse
|
||||
->assertStatus(200)
|
||||
->assertEnvelope()
|
||||
->assertSuccess();
|
||||
|
||||
$matchingOrderId = (int)($createResponse->data()['id'] ?? 0);
|
||||
expect($createResponse->data()['po'] ?? null)->toBe('MATCHING-BOOKING-PO');
|
||||
|
||||
$unauthorizedSession = api_fixtures()->createUserSession(['add_order'], [
|
||||
'customer_number' => $otherCustomer['customer_number'],
|
||||
]);
|
||||
$unauthorizedResponse = api_client()->post('/orders', [
|
||||
'customer_id' => $customer['customer_number'],
|
||||
'department_id' => $department['id'],
|
||||
'reference' => 'ORDER-BOOKING-PO-UNAUTHORIZED',
|
||||
'notes' => 'Created without booking access',
|
||||
'reg_1' => 'NOAUTHPO',
|
||||
'booking_id' => $matchingBooking['id'],
|
||||
], $unauthorizedSession['headers']);
|
||||
|
||||
$unauthorizedResponse
|
||||
->assertStatus(200)
|
||||
->assertEnvelope()
|
||||
->assertSuccess();
|
||||
|
||||
$unauthorizedOrderId = (int)($unauthorizedResponse->data()['id'] ?? 0);
|
||||
expect($unauthorizedResponse->data()['po'] ?? null)->toBeNull();
|
||||
|
||||
$foreignResponse = api_client()->post('/orders', [
|
||||
'customer_id' => $customer['customer_number'],
|
||||
'department_id' => $department['id'],
|
||||
'reference' => 'ORDER-BOOKING-PO-FOREIGN',
|
||||
'notes' => 'Created with foreign booking',
|
||||
'reg_1' => 'FOREIGNPO',
|
||||
'booking_id' => $foreignBooking['id'],
|
||||
], $session['headers']);
|
||||
|
||||
$foreignResponse
|
||||
->assertStatus(200)
|
||||
->assertEnvelope()
|
||||
->assertSuccess();
|
||||
|
||||
$foreignOrderId = (int)($foreignResponse->data()['id'] ?? 0);
|
||||
expect($foreignResponse->data()['po'] ?? null)->toBeNull();
|
||||
|
||||
$deletedResponse = api_client()->post('/orders', [
|
||||
'customer_id' => $customer['customer_number'],
|
||||
'department_id' => $department['id'],
|
||||
'reference' => 'ORDER-BOOKING-PO-DELETED',
|
||||
'notes' => 'Created with deleted booking',
|
||||
'reg_1' => 'DELETEPO',
|
||||
'booking_id' => $deletedBooking['id'],
|
||||
], $session['headers']);
|
||||
|
||||
$deletedResponse
|
||||
->assertStatus(200)
|
||||
->assertEnvelope()
|
||||
->assertSuccess();
|
||||
|
||||
$deletedOrderId = (int)($deletedResponse->data()['id'] ?? 0);
|
||||
expect($deletedResponse->data()['po'] ?? null)->toBeNull();
|
||||
|
||||
$existingOrder = api_fixtures()->createOrder([
|
||||
'customer_id' => $customer['customer_number'],
|
||||
'cashier_id' => $cashier['id'],
|
||||
'department_id' => $department['id'],
|
||||
'reference' => 'ORDER-BOOKING-PO-UPDATE',
|
||||
'reg_1' => 'UPDATEPO',
|
||||
]);
|
||||
|
||||
api_client()->put('/orders', [
|
||||
'id' => $existingOrder['id'],
|
||||
'booking_id' => $foreignBooking['id'],
|
||||
], $session['headers'])
|
||||
->assertStatus(200)
|
||||
->assertEnvelope()
|
||||
->assertSuccess();
|
||||
|
||||
$updatedRow = api_fixtures()->fetchRowById('orders', (int)$existingOrder['id']);
|
||||
expect($updatedRow['po'] ?? null)->toBeNull();
|
||||
|
||||
api_fixtures()->cleanupDeleteById('orders', $matchingOrderId);
|
||||
api_fixtures()->cleanupDeleteById('orders', $unauthorizedOrderId);
|
||||
api_fixtures()->cleanupDeleteById('orders', $foreignOrderId);
|
||||
api_fixtures()->cleanupDeleteById('orders', $deletedOrderId);
|
||||
});
|
||||
|
||||
it('rejects invalid order creation requests', function (): void {
|
||||
api_test_covers('POST /orders', 'failure');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user