Fix booking PO default tenant validation

This commit is contained in:
Jeppe B
2026-06-01 23:23:49 +02:00
parent 7bb67b0470
commit eab8394579
3 changed files with 180 additions and 6 deletions
@@ -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
+48 -6
View File
@@ -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');