Add tests to ensure order PO defaults from booking when missing and enhance existing routing logic.
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -41,6 +41,8 @@ class orders_schema_bootstrap
|
||||
);
|
||||
}
|
||||
|
||||
self::backfillBookingPoDefaults($db);
|
||||
|
||||
self::ensureIndex($db, 'orders', 'idx_orders_period_customer_created_deleted', 'customer_id, created_at, deleted_at');
|
||||
self::ensureIndex($db, 'orders', 'idx_orders_period_created_deleted_customer', 'created_at, deleted_at, customer_id');
|
||||
self::ensureIndex($db, 'order_items', 'idx_order_items_order_deleted', 'order_id, deleted_at');
|
||||
@@ -49,6 +51,29 @@ class orders_schema_bootstrap
|
||||
self::$initialized = true;
|
||||
}
|
||||
|
||||
private static function backfillBookingPoDefaults(object $db): void
|
||||
{
|
||||
if (
|
||||
!self::tableExists($db, 'order_bookings')
|
||||
|| !self::columnExists($db, 'orders', 'booking_id')
|
||||
|| !self::columnExists($db, 'orders', 'po')
|
||||
|| !self::columnExists($db, 'order_bookings', 'po')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$db->query(
|
||||
"UPDATE orders o
|
||||
INNER JOIN order_bookings b ON b.id = o.booking_id
|
||||
SET o.po = b.po
|
||||
WHERE o.booking_id IS NOT NULL
|
||||
AND o.booking_id > 0
|
||||
AND (o.po IS NULL OR TRIM(o.po) = '')
|
||||
AND b.po IS NOT NULL
|
||||
AND TRIM(b.po) <> ''"
|
||||
);
|
||||
}
|
||||
|
||||
private static function tableExists(object $db, string $table): bool
|
||||
{
|
||||
$table = self::escapeIdentifier($table);
|
||||
|
||||
@@ -115,6 +115,7 @@ class departmentsRoute
|
||||
'economic_department_id' => (int)$department['economic_department_id'],
|
||||
'created_at' => (string)$department['created_at'],
|
||||
'updated_at' => (string)$department['updated_at'],
|
||||
'visible' => (int)$department['visible'],
|
||||
'dimension' => (int)$department['dimension'],
|
||||
'branding' => (int)$department['branding'],
|
||||
'archived' => (bool)(int)($department['archived'] ?? 0),
|
||||
|
||||
@@ -16,6 +16,7 @@ use objects\collected_order_invoices_o;
|
||||
use objects\departments_o;
|
||||
use objects\economic_module_orders;
|
||||
use objects\logs_o;
|
||||
use objects\order_bookings_o;
|
||||
use objects\orders_o;
|
||||
use objects\stripe_module_orders_o;
|
||||
use objects\stripe_payment_intents_o;
|
||||
@@ -193,18 +194,25 @@ class ordersRoute
|
||||
$response->error($e->getMessage(), 400);
|
||||
}
|
||||
|
||||
$bookingId = !empty($data['booking_id']) ? (int)$data['booking_id'] : null;
|
||||
$po = $this->resolveOrderPoForBookingDefault(
|
||||
array_key_exists('po', $data) ? $data['po'] : null,
|
||||
array_key_exists('po', $data),
|
||||
$bookingId
|
||||
);
|
||||
$new_data = [
|
||||
'customer_id' => (int)$data['customer_id'],
|
||||
'department_id' => (int)$data['department_id'],
|
||||
'reference' => (string)$data['reference'] ?? '',
|
||||
'cashier_id' => (int)$user->id, // The user who created the order
|
||||
'notes' => (string)$data['notes'] ?? '',
|
||||
...($po !== null ? ['po' => $po] : []),
|
||||
'reg_1' => (string)$reg_1,
|
||||
'reg_2' => (string)$reg_2,
|
||||
'reg_3' => (string)$reg_3,
|
||||
...(!empty($data['lane']) ? ['lane' => (int)$data['lane']] : []), // Optional lane
|
||||
...(!empty($data['wash_id']) ? ['wash_id' => (string)$data['wash_id']] : []), // Optional wash ID
|
||||
...(!empty($data['booking_id']) ? ['booking_id' => (int)$data['booking_id']] : []), // Optional booking ID
|
||||
...($bookingId !== null ? ['booking_id' => $bookingId] : []), // Optional booking ID
|
||||
'created_at' => $createdAt, // Default to current time if not set
|
||||
...(array_key_exists('include_in_invoice', $data) ? ['include_in_invoice' => $includeInInvoice] : []),
|
||||
...(array_key_exists('safety_seal', $data) ? ['safety_seal' => orders_o::normalizeSafetySealValue($data['safety_seal'])] : []),
|
||||
@@ -1177,7 +1185,9 @@ class ordersRoute
|
||||
}
|
||||
// If the booking ID is set, validate it
|
||||
if (isset($data['booking_id'])) {
|
||||
$order->booking_id->set((int)$data['booking_id']);
|
||||
$bookingId = (int)$data['booking_id'];
|
||||
$order->booking_id->set($bookingId);
|
||||
$this->applyBookingPoDefaultToOrder($order, $bookingId);
|
||||
}
|
||||
// Check if the invoice collection is set
|
||||
if (isset($data['invoice_collection_id']) && !$shouldAutoReassignInvoiceCollection) {
|
||||
@@ -1225,6 +1235,55 @@ class ordersRoute
|
||||
}
|
||||
}
|
||||
|
||||
private function resolveOrderPoForBookingDefault(mixed $po, bool $poProvided, ?int $bookingId): ?string
|
||||
{
|
||||
$currentPo = is_scalar($po) || $po === null ? trim((string)$po) : '';
|
||||
if ($currentPo !== '') {
|
||||
return $currentPo;
|
||||
}
|
||||
|
||||
$bookingPo = $this->getBookingPoDefault($bookingId);
|
||||
if ($bookingPo !== null) {
|
||||
return $bookingPo;
|
||||
}
|
||||
|
||||
return $poProvided ? '' : null;
|
||||
}
|
||||
|
||||
private function applyBookingPoDefaultToOrder(orders_o $order, ?int $bookingId = null): void
|
||||
{
|
||||
$currentPo = trim((string)($order->po->value() ?? ''));
|
||||
if ($currentPo !== '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$bookingPo = $this->getBookingPoDefault($bookingId ?? (int)($order->booking_id->value() ?? 0));
|
||||
if ($bookingPo === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$order->po->set($bookingPo);
|
||||
}
|
||||
|
||||
private function getBookingPoDefault(?int $bookingId): ?string
|
||||
{
|
||||
if ($bookingId === null || $bookingId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$booking = (new order_bookings_o())->select($bookingId);
|
||||
if (!$booking->exists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$bookingPo = trim((string)($booking->po->value() ?? ''));
|
||||
return $bookingPo !== '' ? $bookingPo : null;
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function normalizeLegacyEditableFieldPayload(array $data, response $response): array
|
||||
{
|
||||
if (!array_key_exists('field', $data) && !array_key_exists('value', $data)) {
|
||||
|
||||
@@ -191,6 +191,101 @@ it('creates orders through the real endpoint', function (): void {
|
||||
api_fixtures()->cleanupDeleteById('orders', $orderId);
|
||||
});
|
||||
|
||||
it('defaults order PO from a linked booking when creating without an order PO', function (): void {
|
||||
api_test_covers('POST /orders', 'happy');
|
||||
|
||||
$customer = api_fixtures()->createUser(['display_name' => 'Booking PO Customer']);
|
||||
$department = api_fixtures()->createDepartment();
|
||||
api_fixtures()->createInvoiceCollection([
|
||||
'customer_number' => $customer['customer_number'],
|
||||
]);
|
||||
$booking = api_fixtures()->createOrderBooking([
|
||||
'customer_number' => $customer['customer_number'],
|
||||
'department' => $department['id'],
|
||||
'reference' => 'BOOKING-PO-REF',
|
||||
'po' => 'BOOKING-PO-CREATE',
|
||||
]);
|
||||
$session = api_fixtures()->createUserSession(['add_order']);
|
||||
|
||||
$orders = [
|
||||
[
|
||||
'reference' => 'BOOKING-PO-MISSING',
|
||||
'reg_1' => 'BKPO001',
|
||||
],
|
||||
[
|
||||
'reference' => 'BOOKING-PO-BLANK',
|
||||
'po' => ' ',
|
||||
'reg_1' => 'BKPO002',
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($orders as $payload) {
|
||||
$response = api_client()->post('/orders', [
|
||||
'customer_id' => $customer['customer_number'],
|
||||
'department_id' => $department['id'],
|
||||
'notes' => 'Created with booking PO default',
|
||||
'booking_id' => $booking['id'],
|
||||
...$payload,
|
||||
], $session['headers']);
|
||||
|
||||
$response
|
||||
->assertStatus(200)
|
||||
->assertEnvelope()
|
||||
->assertSuccess();
|
||||
|
||||
$orderId = (int)($response->data()['id'] ?? 0);
|
||||
expect($orderId)->toBeGreaterThan(0);
|
||||
|
||||
$row = api_fixtures()->fetchRowById('orders', $orderId);
|
||||
expect($row)->not->toBeNull();
|
||||
expect((int)($row['booking_id'] ?? 0))->toBe((int)$booking['id']);
|
||||
expect($row['po'] ?? null)->toBe('BOOKING-PO-CREATE');
|
||||
|
||||
api_fixtures()->cleanupDeleteById('orders', $orderId);
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps an explicit order PO when creating a linked order', function (): void {
|
||||
api_test_covers('POST /orders', 'happy');
|
||||
|
||||
$customer = api_fixtures()->createUser(['display_name' => 'Explicit PO Customer']);
|
||||
$department = api_fixtures()->createDepartment();
|
||||
api_fixtures()->createInvoiceCollection([
|
||||
'customer_number' => $customer['customer_number'],
|
||||
]);
|
||||
$booking = api_fixtures()->createOrderBooking([
|
||||
'customer_number' => $customer['customer_number'],
|
||||
'department' => $department['id'],
|
||||
'po' => 'BOOKING-PO-IGNORED',
|
||||
]);
|
||||
$session = api_fixtures()->createUserSession(['add_order']);
|
||||
|
||||
$response = api_client()->post('/orders', [
|
||||
'customer_id' => $customer['customer_number'],
|
||||
'department_id' => $department['id'],
|
||||
'reference' => 'EXPLICIT-PO-CREATE',
|
||||
'notes' => 'Created with explicit PO',
|
||||
'po' => 'ORDER-PO-CREATE',
|
||||
'booking_id' => $booking['id'],
|
||||
'reg_1' => 'EXPO123',
|
||||
], $session['headers']);
|
||||
|
||||
$response
|
||||
->assertStatus(200)
|
||||
->assertEnvelope()
|
||||
->assertSuccess();
|
||||
|
||||
$orderId = (int)($response->data()['id'] ?? 0);
|
||||
expect($orderId)->toBeGreaterThan(0);
|
||||
|
||||
$row = api_fixtures()->fetchRowById('orders', $orderId);
|
||||
expect($row)->not->toBeNull();
|
||||
expect((int)($row['booking_id'] ?? 0))->toBe((int)$booking['id']);
|
||||
expect($row['po'] ?? null)->toBe('ORDER-PO-CREATE');
|
||||
|
||||
api_fixtures()->cleanupDeleteById('orders', $orderId);
|
||||
});
|
||||
|
||||
it('rejects invalid order creation requests', function (): void {
|
||||
api_test_covers('POST /orders', 'failure');
|
||||
|
||||
@@ -330,6 +425,69 @@ it('updates orders through the primary endpoint', function (): void {
|
||||
expect((int)($row['include_in_invoice'] ?? 1))->toBe(0);
|
||||
});
|
||||
|
||||
it('defaults blank order PO from a linked booking on order updates', function (): void {
|
||||
api_test_covers('PUT /orders', 'happy');
|
||||
api_test_covers('PUT /order', 'happy');
|
||||
|
||||
$department = api_fixtures()->createDepartment(['name' => 'Booking PO Update Department']);
|
||||
$customer = api_fixtures()->createUser(['display_name' => 'Booking PO Update Customer']);
|
||||
$cashier = api_fixtures()->createUser(['display_name' => 'Booking PO Update Cashier']);
|
||||
$booking = api_fixtures()->createOrderBooking([
|
||||
'customer_number' => $customer['customer_number'],
|
||||
'department' => $department['id'],
|
||||
'po' => 'BOOKING-PO-LINK',
|
||||
]);
|
||||
$session = api_fixtures()->createUserSession(['edit_order']);
|
||||
|
||||
foreach (['/orders', '/order'] as $endpoint) {
|
||||
$blankOrder = api_fixtures()->createOrder([
|
||||
'customer_id' => $customer['customer_number'],
|
||||
'cashier_id' => $cashier['id'],
|
||||
'department_id' => $department['id'],
|
||||
'reference' => 'BLANK-' . trim($endpoint, '/'),
|
||||
'reg_1' => 'BLNK123',
|
||||
'po' => '',
|
||||
]);
|
||||
$manualOrder = api_fixtures()->createOrder([
|
||||
'customer_id' => $customer['customer_number'],
|
||||
'cashier_id' => $cashier['id'],
|
||||
'department_id' => $department['id'],
|
||||
'reference' => 'MANUAL-' . trim($endpoint, '/'),
|
||||
'reg_1' => 'MANU123',
|
||||
'po' => 'MANUAL-PO',
|
||||
]);
|
||||
|
||||
api_client()->put($endpoint, [
|
||||
'id' => $blankOrder['id'],
|
||||
'booking_id' => $booking['id'],
|
||||
], $session['headers'])
|
||||
->assertStatus(200)
|
||||
->assertEnvelope()
|
||||
->assertSuccess()
|
||||
->assertMessage('Order updated successfully');
|
||||
|
||||
api_client()->put($endpoint, [
|
||||
'id' => $manualOrder['id'],
|
||||
'booking_id' => $booking['id'],
|
||||
], $session['headers'])
|
||||
->assertStatus(200)
|
||||
->assertEnvelope()
|
||||
->assertSuccess()
|
||||
->assertMessage('Order updated successfully');
|
||||
|
||||
$blankRow = api_fixtures()->fetchRowById('orders', (int)$blankOrder['id']);
|
||||
$manualRow = api_fixtures()->fetchRowById('orders', (int)$manualOrder['id']);
|
||||
|
||||
expect($blankRow)->not->toBeNull();
|
||||
expect((int)($blankRow['booking_id'] ?? 0))->toBe((int)$booking['id']);
|
||||
expect($blankRow['po'] ?? null)->toBe('BOOKING-PO-LINK');
|
||||
|
||||
expect($manualRow)->not->toBeNull();
|
||||
expect((int)($manualRow['booking_id'] ?? 0))->toBe((int)$booking['id']);
|
||||
expect($manualRow['po'] ?? null)->toBe('MANUAL-PO');
|
||||
}
|
||||
});
|
||||
|
||||
it('reassigns invoice collections when changing an order across the draft customer boundary', function (): void {
|
||||
api_test_covers('PUT /orders', 'happy');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user