db()->prepare( 'SELECT id, content FROM object_attachments WHERE object_type IN (?, ?) AND object_id = ? AND deleted_at IS NULL ORDER BY id ASC' ); expect($statement)->not->toBeFalse(); $objectType = 'orders'; $backtickedObjectType = '`orders`'; $statement->bind_param('ssi', $objectType, $backtickedObjectType, $orderId); $statement->execute(); $result = $statement->get_result(); $attachmentIds = []; while ($row = $result->fetch_assoc()) { $content = json_decode((string)($row['content'] ?? ''), true); $other = is_array($content) ? ($content['other'] ?? null) : null; if (is_string($other) && strtolower($other) === 'wash_certificate') { $attachmentIds[] = (int)($row['id'] ?? 0); } } $result->free(); $statement->close(); return array_values(array_filter($attachmentIds, static fn(int $id): bool => $id > 0)); } it('lists orders for an admin-scoped user and limits the results to the permitted departments', function (): void { api_test_covers('GET /orders', 'happy'); $departmentOne = api_fixtures()->createDepartment(['name' => 'Department One']); $departmentTwo = api_fixtures()->createDepartment(['name' => 'Department Two']); $customerOne = api_fixtures()->createUser(['display_name' => 'Customer One']); $customerTwo = api_fixtures()->createUser(['display_name' => 'Customer Two']); $cashier = api_fixtures()->createUser(['display_name' => 'Cashier']); $visibleOrder = api_fixtures()->createOrder([ 'customer_id' => $customerOne['customer_number'], 'cashier_id' => $cashier['id'], 'department_id' => $departmentOne['id'], 'reference' => 'VISIBLE', 'reg_1' => 'VISIBLE1', ]); $hiddenOrder = api_fixtures()->createOrder([ 'customer_id' => $customerTwo['customer_number'], 'cashier_id' => $cashier['id'], 'department_id' => $departmentTwo['id'], 'reference' => 'HIDDEN', 'reg_1' => 'HIDDEN1', ]); $session = api_fixtures()->createUserSession([ 'list_orders', 'department_access_' . $departmentOne['id'], ]); $response = api_client()->get('/orders', $session['headers']); $response ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); $orderIds = array_map( static fn(array $order): int => (int)($order['id'] ?? 0), is_array($response->data()) ? $response->data() : [] ); expect($orderIds) ->toContain($visibleOrder['id']) ->not->toContain($hiddenOrder['id']); }); it('lists only the targeted customer orders for subuser sessions', function (): void { api_test_covers('GET /orders', 'happy'); $department = api_fixtures()->createDepartment(); $targetCustomer = api_fixtures()->createUser(['display_name' => 'Target Customer']); $otherCustomer = api_fixtures()->createUser(['display_name' => 'Other Customer']); $cashier = api_fixtures()->createUser(['display_name' => 'Subuser Cashier']); $targetOrder = api_fixtures()->createOrder([ 'customer_id' => $targetCustomer['customer_number'], 'cashier_id' => $cashier['id'], 'department_id' => $department['id'], 'reference' => 'TARGET', 'reg_1' => 'TARGET1', ]); $otherOrder = api_fixtures()->createOrder([ 'customer_id' => $otherCustomer['customer_number'], 'cashier_id' => $cashier['id'], 'department_id' => $department['id'], 'reference' => 'OTHER', 'reg_1' => 'OTHER1', ]); $session = api_fixtures()->createSubuserSession( (int)$targetCustomer['customer_number'], ['ORDERS_LIST'] ); $response = api_client()->get('/orders', $session['headers']); $response ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); $orderIds = array_map( static fn(array $order): int => (int)($order['id'] ?? 0), is_array($response->data()) ? $response->data() : [] ); expect($orderIds) ->toContain($targetOrder['id']) ->not->toContain($otherOrder['id']); expect($response->meta())->toHaveKey('target_customer_number', $targetCustomer['customer_number']); }); it('returns the current auth and permission failures when order listing is not allowed', function (): void { api_test_covers('GET /orders', 'auth'); $missingToken = api_client()->get('/orders'); $missingToken ->assertStatus(400) ->assertEnvelope() ->assertSuccess(false) ->assertMessage('Invalid session'); $session = api_fixtures()->createUserSession([]); $missingPermissions = api_client()->get('/orders', $session['headers']); $missingPermissions ->assertStatus(403) ->assertEnvelope() ->assertSuccess(false) ->assertMissingPermissions(['list_own_orders', 'list_orders']); }); it('creates orders through the real endpoint', function (): void { api_test_covers('POST /orders', 'happy'); $customer = api_fixtures()->createUser(['display_name' => 'Order Customer']); $department = api_fixtures()->createDepartment(); api_fixtures()->createInvoiceCollection([ 'customer_number' => $customer['customer_number'], ]); $session = api_fixtures()->createUserSession(['add_order']); $response = api_client()->post('/orders', [ 'customer_id' => $customer['customer_number'], 'department_id' => $department['id'], 'reference' => 'ORDER-POST', 'notes' => 'Created through HTTP', 'safety_seal' => 'SEAL-CREATE', 'reg_1' => ' post-123 ', 'reg_2' => ' tr 9-8 ', 'reg_3' => ' 7z/x ', ], $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($row['reg_1'] ?? null)->toBe('POST123'); expect($row['reg_2'] ?? null)->toBe('TR98'); expect($row['reg_3'] ?? null)->toBe('7ZX'); expect($row['safety_seal'] ?? null)->toBe('SEAL-CREATE'); api_fixtures()->cleanupDeleteById('orders', $orderId); }); it('rejects invalid order creation requests', function (): void { api_test_covers('POST /orders', 'failure'); $customer = api_fixtures()->createUser(); $department = api_fixtures()->createDepartment(); $session = api_fixtures()->createUserSession(['add_order']); $missingReference = api_client()->post('/orders', [ 'customer_id' => $customer['customer_number'], 'department_id' => $department['id'], 'notes' => 'Missing reference', 'reg_1' => 'MISSREF', ], $session['headers']); $missingReference ->assertStatus(400) ->assertEnvelope() ->assertSuccess(false) ->assertMessage('Reference is required'); $invalidDepartment = api_client()->post('/orders', [ 'customer_id' => $customer['customer_number'], 'department_id' => 999999, 'reference' => 'BAD-DEPT', 'notes' => 'Bad department', 'reg_1' => 'BADDEPT', ], $session['headers']); $invalidDepartment ->assertStatus(500) ->assertEnvelope() ->assertSuccess(false) ->assertMessageContains('getDepartmentById()'); $invalidCustomer = api_client()->post('/orders', [ 'customer_id' => 999999, 'department_id' => $department['id'], 'reference' => 'BAD-CUSTOMER', 'notes' => 'Bad customer', 'reg_1' => 'BADCUST', ], $session['headers']); $invalidCustomer ->assertStatus(500) ->assertEnvelope() ->assertSuccess(false) ->assertMessageContains('e-conomic request failed with HTTP'); api_fixtures()->addCustomerAttribute((int)$customer['id'], 'requiresReferenceNumber'); $missingRequiredReference = api_client()->post('/orders', [ 'customer_id' => $customer['customer_number'], 'department_id' => $department['id'], 'reference' => '', 'notes' => 'Empty reference', 'reg_1' => 'REQREF1', ], $session['headers']); $missingRequiredReference ->assertStatus(400) ->assertEnvelope() ->assertSuccess(false) ->assertMessage('Reference is required by the customer'); }); it('updates orders through the primary endpoint', function (): void { api_test_covers('PUT /orders', 'happy'); $department = api_fixtures()->createDepartment(['name' => 'Original Department']); $updatedDepartment = api_fixtures()->createDepartment(['name' => 'Updated Department']); $customer = api_fixtures()->createUser(['display_name' => 'Original Customer']); $updatedCustomer = api_fixtures()->createUser(['display_name' => 'Updated Customer']); $cashier = api_fixtures()->createUser(['display_name' => 'Order Editor']); $updatedInvoiceCollection = api_fixtures()->createInvoiceCollection([ 'customer_number' => $updatedCustomer['customer_number'], ]); $order = api_fixtures()->createOrder([ 'customer_id' => $customer['customer_number'], 'cashier_id' => $cashier['id'], 'department_id' => $department['id'], 'reference' => 'BEFORE-REF', 'reg_1' => 'BEFR123', 'reg_2' => 'OLD-2', 'reg_3' => 'OLD-3', 'notes' => 'Before update', 'po' => 'PO-BEFORE', 'lane' => 2, 'wash_id' => 'WASH-BEFORE', 'booking_id' => 321, 'safety_seal' => 'SEAL-BEFORE', 'include_in_invoice' => true, 'created_at' => '2026-04-08 08:44:07', ]); $session = api_fixtures()->createUserSession(['edit_order']); $response = api_client()->put('/orders', [ 'id' => $order['id'], 'customer_id' => $updatedCustomer['customer_number'], 'department_id' => $updatedDepartment['id'], 'reference' => 'AFTER-REF', 'reg_1' => ' af-tr 123 ', 'reg_2' => ' new-2 ', 'reg_3' => ' new/3 ', 'notes' => 'After update', 'po' => 'PO-123', 'lane' => 7, 'wash_id' => 'WASH-123', 'booking_id' => 9876, 'safety_seal' => 'SEAL-AFTER', 'invoice_collection_id' => $updatedInvoiceCollection['id'], 'created_at' => '2026-04-09 13:37:00', 'include_in_invoice' => false, ], $session['headers']); $response ->assertStatus(200) ->assertEnvelope() ->assertSuccess() ->assertMessage('Order updated successfully'); $row = api_fixtures()->fetchRowById('orders', (int)$order['id']); expect($row)->not->toBeNull(); expect((int)($row['customer_id'] ?? 0))->toBe((int)$updatedCustomer['customer_number']); expect((int)($row['department_id'] ?? 0))->toBe((int)$updatedDepartment['id']); expect($row['reference'] ?? null)->toBe('AFTER-REF'); expect($row['reg_1'] ?? null)->toBe('AFTR123'); expect($row['reg_2'] ?? null)->toBe('NEW2'); expect($row['reg_3'] ?? null)->toBe('NEW3'); expect($row['notes'] ?? null)->toBe('After update'); expect($row['po'] ?? null)->toBe('PO-123'); expect((int)($row['lane'] ?? 0))->toBe(7); expect($row['wash_id'] ?? null)->toBe('WASH-123'); expect((int)($row['booking_id'] ?? 0))->toBe(9876); expect($row['safety_seal'] ?? null)->toBe('SEAL-AFTER'); expect((int)($row['invoice_collection_id'] ?? 0))->toBe((int)$updatedInvoiceCollection['id']); expect($row['created_at'] ?? null)->toBe('2026-04-09 13:37:00'); expect((int)($row['include_in_invoice'] ?? 1))->toBe(0); }); it('reassigns invoice collections when changing an order across the draft customer boundary', function (): void { api_test_covers('PUT /orders', 'happy'); $draftCustomer = api_fixtures()->createUser(['display_name' => 'Draft Customer']); $regularCustomer = api_fixtures()->createUser(['display_name' => 'Regular Customer']); $department = api_fixtures()->createDepartment(['name' => 'Draft Boundary Department']); $cashier = api_fixtures()->createUser(['display_name' => 'Draft Boundary Cashier']); api_fixtures()->setModuleConfig('economic', 'transactionDraftCustomerNumber', (string)$draftCustomer['customer_number'], 'int'); $order = api_fixtures()->createOrder([ 'customer_id' => $regularCustomer['customer_number'], 'cashier_id' => $cashier['id'], 'department_id' => $department['id'], 'reference' => 'DRAFT-BOUNDARY', 'reg_1' => 'DRAFT123', ]); $session = api_fixtures()->createUserSession(['edit_order']); api_client()->put('/orders', [ 'id' => $order['id'], 'customer_id' => $draftCustomer['customer_number'], ], $session['headers']) ->assertStatus(200) ->assertEnvelope() ->assertSuccess() ->assertMessage('Order updated successfully'); $draftRow = api_fixtures()->fetchRowById('orders', (int)$order['id']); expect($draftRow)->not->toBeNull(); $draftInvoiceCollectionId = (int)($draftRow['invoice_collection_id'] ?? 0); expect($draftInvoiceCollectionId)->toBeGreaterThan(0); $draftCollectionRow = api_fixtures()->fetchRowById('collected_order_invoices', $draftInvoiceCollectionId); expect($draftCollectionRow)->not->toBeNull(); expect((int)($draftCollectionRow['customer_number'] ?? 0))->toBe((int)$draftCustomer['customer_number']); api_client()->put('/orders', [ 'id' => $order['id'], 'customer_id' => $regularCustomer['customer_number'], ], $session['headers']) ->assertStatus(200) ->assertEnvelope() ->assertSuccess() ->assertMessage('Order updated successfully'); $regularRow = api_fixtures()->fetchRowById('orders', (int)$order['id']); expect($regularRow)->not->toBeNull(); $regularInvoiceCollectionId = (int)($regularRow['invoice_collection_id'] ?? 0); expect($regularInvoiceCollectionId)->toBeGreaterThan(0); $regularCollectionRow = api_fixtures()->fetchRowById('collected_order_invoices', $regularInvoiceCollectionId); expect($regularCollectionRow)->not->toBeNull(); expect((int)($regularCollectionRow['customer_number'] ?? 0))->toBe((int)$regularCustomer['customer_number']); }); it('regenerates attached wash certificates when certificate metadata changes through the primary endpoint', function (): void { api_test_covers('PUT /orders', 'happy'); $department = api_fixtures()->createDepartment(['name' => 'Wash Certificate Department']); $customer = api_fixtures()->createUser(['display_name' => 'Original Certificate Customer']); $updatedCustomer = api_fixtures()->createUser(['display_name' => 'Updated Certificate Customer']); $cashier = api_fixtures()->createUser(['display_name' => 'Certificate Cashier']); $order = api_fixtures()->createOrder([ 'customer_id' => $customer['customer_number'], 'cashier_id' => $cashier['id'], 'department_id' => $department['id'], 'reference' => 'CERT-REF', 'reg_1' => 'CERT123', 'reg_2' => 'TRAIL1', 'safety_seal' => 'SEAL-OLD', 'created_at' => '2026-04-12 10:15:00', ]); $session = api_fixtures()->createUserSession([ 'edit_order', 'add_order_attachments', 'department_access_' . $department['id'], ]); api_client()->post('/orders/attachments/upload', [ 'order_id' => $order['id'], 'base64_file' => base64_encode('wash-certificate'), 'file_name' => 'wash_certificate', ], $session['headers']) ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); $initialAttachmentIds = activeWashCertificateAttachmentIdsForOrder((int)$order['id']); expect($initialAttachmentIds)->toHaveCount(1); $initialAttachmentId = $initialAttachmentIds[0]; $initialAttachmentRow = api_fixtures()->fetchRowById('object_attachments', $initialAttachmentId); expect($initialAttachmentRow)->not->toBeNull(); $initialDocument = (json_decode((string)($initialAttachmentRow['content'] ?? ''), true) ?? [])['document'] ?? null; api_client()->put('/orders', [ 'id' => $order['id'], 'customer_id' => $updatedCustomer['customer_number'], 'reg_2' => ' new-trail-55 ', 'safety_seal' => 'SEAL-NEW', ], $session['headers']) ->assertStatus(200) ->assertEnvelope() ->assertSuccess() ->assertMessage('Order updated successfully'); $orderRow = api_fixtures()->fetchRowById('orders', (int)$order['id']); expect($orderRow)->not->toBeNull(); expect((int)($orderRow['customer_id'] ?? 0))->toBe((int)$updatedCustomer['customer_number']); expect($orderRow['reg_2'] ?? null)->toBe('NEWTRAIL55'); expect($orderRow['safety_seal'] ?? null)->toBe('SEAL-NEW'); $updatedAttachmentIds = activeWashCertificateAttachmentIdsForOrder((int)$order['id']); expect($updatedAttachmentIds)->toHaveCount(1); expect($updatedAttachmentIds)->not->toContain($initialAttachmentId); $updatedAttachmentId = $updatedAttachmentIds[0]; $updatedAttachmentRow = api_fixtures()->fetchRowById('object_attachments', $updatedAttachmentId); $deletedAttachmentRow = api_fixtures()->fetchRowById('object_attachments', $initialAttachmentId); expect($updatedAttachmentRow)->not->toBeNull(); expect($deletedAttachmentRow)->not->toBeNull(); expect($deletedAttachmentRow['deleted_at'] ?? null)->not->toBeNull(); expect((json_decode((string)($updatedAttachmentRow['content'] ?? ''), true) ?? [])['document'] ?? null)->not->toBe($initialDocument); }); it('regenerates attached wash certificates for legacy field-value updates through the alias endpoint', function (): void { api_test_covers('PUT /order', 'happy'); $department = api_fixtures()->createDepartment(['name' => 'Alias Wash Certificate Department']); $customer = api_fixtures()->createUser(['display_name' => 'Alias Certificate Customer']); $cashier = api_fixtures()->createUser(['display_name' => 'Alias Certificate Cashier']); $order = api_fixtures()->createOrder([ 'customer_id' => $customer['customer_number'], 'cashier_id' => $cashier['id'], 'department_id' => $department['id'], 'reference' => 'ALIAS-CERT', 'reg_1' => 'ALIAS123', 'safety_seal' => 'ALIAS-SEAL', ]); $session = api_fixtures()->createUserSession([ 'edit_order', 'add_order_attachments', 'department_access_' . $department['id'], ]); api_client()->post('/orders/attachments/upload', [ 'order_id' => $order['id'], 'base64_file' => base64_encode('wash-certificate'), 'file_name' => 'wash_certificate', ], $session['headers']) ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); $initialAttachmentIds = activeWashCertificateAttachmentIdsForOrder((int)$order['id']); expect($initialAttachmentIds)->toHaveCount(1); $initialAttachmentId = $initialAttachmentIds[0]; api_client()->put('/order', [ 'id' => $order['id'], 'field' => 'reg_1', 'value' => ' zz-88 11 ', ], $session['headers']) ->assertStatus(200) ->assertEnvelope() ->assertSuccess() ->assertMessage('Order updated successfully'); $orderRow = api_fixtures()->fetchRowById('orders', (int)$order['id']); expect($orderRow)->not->toBeNull(); expect($orderRow['reg_1'] ?? null)->toBe('ZZ8811'); $updatedAttachmentIds = activeWashCertificateAttachmentIdsForOrder((int)$order['id']); expect($updatedAttachmentIds)->toHaveCount(1); expect($updatedAttachmentIds)->not->toContain($initialAttachmentId); $deletedAttachmentRow = api_fixtures()->fetchRowById('object_attachments', $initialAttachmentId); expect($deletedAttachmentRow)->not->toBeNull(); expect($deletedAttachmentRow['deleted_at'] ?? null)->not->toBeNull(); }); it('supports legacy field-value metadata updates through the primary endpoint', function (): void { api_test_covers('PUT /orders', 'happy'); $department = api_fixtures()->createDepartment(['name' => 'Legacy Field Department']); $customer = api_fixtures()->createUser(['display_name' => 'Legacy Field Customer']); $cashier = api_fixtures()->createUser(['display_name' => 'Legacy Field Cashier']); $order = api_fixtures()->createOrder([ 'customer_id' => $customer['customer_number'], 'cashier_id' => $cashier['id'], 'department_id' => $department['id'], 'reference' => 'START-REF', 'reg_1' => 'START123', 'reg_2' => 'START2', 'reg_3' => 'START3', 'notes' => 'Start note', ]); $session = api_fixtures()->createUserSession(['edit_order']); $payloads = [ ['field' => 'reference', 'value' => 'LEGACY-REF'], ['field' => 'notes', 'value' => 'Legacy note'], ['field' => 'safety_seal', 'value' => 'LEGACY-SEAL'], ['field' => 'reg_1', 'value' => ' ab-12 34 '], ['field' => 'reg_2', 'value' => ' cd/56 78 '], ['field' => 'reg_3', 'value' => ' ef_90 12 '], ]; foreach ($payloads as $payload) { api_client()->put('/orders', [ 'id' => $order['id'], ...$payload, ], $session['headers']) ->assertStatus(200) ->assertEnvelope() ->assertSuccess() ->assertMessage('Order updated successfully'); } $row = api_fixtures()->fetchRowById('orders', (int)$order['id']); expect($row)->not->toBeNull(); expect($row['reference'] ?? null)->toBe('LEGACY-REF'); expect($row['notes'] ?? null)->toBe('Legacy note'); expect($row['safety_seal'] ?? null)->toBe('LEGACY-SEAL'); expect($row['reg_1'] ?? null)->toBe('AB1234'); expect($row['reg_2'] ?? null)->toBe('CD5678'); expect($row['reg_3'] ?? null)->toBe('EF9012'); }); it('rejects invalid updates through the primary order endpoint', function (): void { api_test_covers('PUT /orders', 'failure'); $session = api_fixtures()->createUserSession(['edit_order']); $missingId = api_client()->put('/orders', [ 'notes' => 'No id', ], $session['headers']); $missingId ->assertStatus(400) ->assertEnvelope() ->assertSuccess(false) ->assertMessage('ID is required'); $missingOrder = api_client()->put('/orders', [ 'id' => 999999, 'notes' => 'Missing order', ], $session['headers']); $missingOrder ->assertStatus(500) ->assertEnvelope() ->assertSuccess(false) ->assertMessageContains('must not be accessed before initialization'); }); it('updates orders through the legacy alias endpoint', function (): void { api_test_covers('PUT /order', 'happy'); $department = api_fixtures()->createDepartment(['name' => 'Legacy Department']); $updatedDepartment = api_fixtures()->createDepartment(['name' => 'Legacy Updated Department']); $customer = api_fixtures()->createUser(['display_name' => 'Legacy Original Customer']); $updatedCustomer = api_fixtures()->createUser(['display_name' => 'Legacy Updated Customer']); $cashier = api_fixtures()->createUser(['display_name' => 'Legacy Editor']); $updatedInvoiceCollection = api_fixtures()->createInvoiceCollection([ 'customer_number' => $updatedCustomer['customer_number'], ]); $order = api_fixtures()->createOrder([ 'customer_id' => $customer['customer_number'], 'cashier_id' => $cashier['id'], 'department_id' => $department['id'], 'reference' => 'LEGACY-BEFORE', 'reg_1' => 'LGCY123', 'reg_2' => 'LGCY-2', 'reg_3' => 'LGCY-3', 'notes' => 'Legacy before', 'po' => 'LEGACY-PO', 'lane' => 4, 'wash_id' => 'LEGACY-WASH', 'booking_id' => 654, 'safety_seal' => 'LEGACY-SEAL-BEFORE', 'include_in_invoice' => false, 'created_at' => '2026-04-10 08:15:00', ]); $session = api_fixtures()->createUserSession(['edit_order']); $response = api_client()->put('/order', [ 'id' => $order['id'], 'customer_id' => $updatedCustomer['customer_number'], 'department_id' => $updatedDepartment['id'], 'reference' => 'LEGACY-AFTER', 'reg_1' => ' lgcy-999 ', 'reg_2' => ' leg-2 ', 'reg_3' => ' leg/3 ', 'notes' => 'Legacy after', 'po' => 'LEGACY-PO-NEW', 'lane' => 9, 'wash_id' => 'LEGACY-WASH-NEW', 'booking_id' => 7654, 'safety_seal' => 'LEGACY-SEAL-AFTER', 'invoice_collection_id' => $updatedInvoiceCollection['id'], 'created_at' => '2026-04-11 11:22:33', 'include_in_invoice' => true, ], $session['headers']); $response ->assertStatus(200) ->assertEnvelope() ->assertSuccess() ->assertMessage('Order updated successfully'); $row = api_fixtures()->fetchRowById('orders', (int)$order['id']); expect($row)->not->toBeNull(); expect((int)($row['customer_id'] ?? 0))->toBe((int)$updatedCustomer['customer_number']); expect((int)($row['department_id'] ?? 0))->toBe((int)$updatedDepartment['id']); expect($row['reference'] ?? null)->toBe('LEGACY-AFTER'); expect($row['reg_1'] ?? null)->toBe('LGCY999'); expect($row['reg_2'] ?? null)->toBe('LEG2'); expect($row['reg_3'] ?? null)->toBe('LEG3'); expect($row['notes'] ?? null)->toBe('Legacy after'); expect($row['po'] ?? null)->toBe('LEGACY-PO-NEW'); expect((int)($row['lane'] ?? 0))->toBe(9); expect($row['wash_id'] ?? null)->toBe('LEGACY-WASH-NEW'); expect((int)($row['booking_id'] ?? 0))->toBe(7654); expect($row['safety_seal'] ?? null)->toBe('LEGACY-SEAL-AFTER'); expect((int)($row['invoice_collection_id'] ?? 0))->toBe((int)$updatedInvoiceCollection['id']); expect($row['created_at'] ?? null)->toBe('2026-04-11 11:22:33'); expect((int)($row['include_in_invoice'] ?? 0))->toBe(1); }); it('supports legacy field-value metadata updates through the alias endpoint', function (): void { api_test_covers('PUT /order', 'happy'); $department = api_fixtures()->createDepartment(['name' => 'Alias Field Department']); $customer = api_fixtures()->createUser(['display_name' => 'Alias Field Customer']); $cashier = api_fixtures()->createUser(['display_name' => 'Alias Field Cashier']); $order = api_fixtures()->createOrder([ 'customer_id' => $customer['customer_number'], 'cashier_id' => $cashier['id'], 'department_id' => $department['id'], 'reference' => 'ALIAS-START', 'reg_1' => 'ALIAS123', 'reg_2' => 'ALIAS2', 'reg_3' => 'ALIAS3', 'notes' => 'Alias note', ]); $session = api_fixtures()->createUserSession(['edit_order']); $payloads = [ ['field' => 'reference', 'value' => 'ALIAS-REF'], ['field' => 'notes', 'value' => 'Alias updated note'], ['field' => 'safety_seal', 'value' => 'ALIAS-SEAL'], ['field' => 'reg_1', 'value' => ' gh-12 34 '], ['field' => 'reg_2', 'value' => ' ij/56 78 '], ['field' => 'reg_3', 'value' => ' kl_90 12 '], ]; foreach ($payloads as $payload) { api_client()->put('/order', [ 'id' => $order['id'], ...$payload, ], $session['headers']) ->assertStatus(200) ->assertEnvelope() ->assertSuccess() ->assertMessage('Order updated successfully'); } $row = api_fixtures()->fetchRowById('orders', (int)$order['id']); expect($row)->not->toBeNull(); expect($row['reference'] ?? null)->toBe('ALIAS-REF'); expect($row['notes'] ?? null)->toBe('Alias updated note'); expect($row['safety_seal'] ?? null)->toBe('ALIAS-SEAL'); expect($row['reg_1'] ?? null)->toBe('GH1234'); expect($row['reg_2'] ?? null)->toBe('IJ5678'); expect($row['reg_3'] ?? null)->toBe('KL9012'); }); it('rejects invalid updates through the legacy alias endpoint', function (): void { api_test_covers('PUT /order', 'failure'); $session = api_fixtures()->createUserSession(['edit_order']); $response = api_client()->put('/order', [ 'id' => 999999, 'notes' => 'Missing alias order', ], $session['headers']); $response ->assertStatus(500) ->assertEnvelope() ->assertSuccess(false) ->assertMessageContains('must not be accessed before initialization'); }); it('rejects unsupported legacy field-value updates on both update endpoints', function (): void { api_test_covers('PUT /orders', 'failure'); $department = api_fixtures()->createDepartment(['name' => 'Unsupported Field Department']); $customer = api_fixtures()->createUser(['display_name' => 'Unsupported Field Customer']); $cashier = api_fixtures()->createUser(['display_name' => 'Unsupported Field Cashier']); $order = api_fixtures()->createOrder([ 'customer_id' => $customer['customer_number'], 'cashier_id' => $cashier['id'], 'department_id' => $department['id'], 'reference' => 'UNCHANGED-REF', 'notes' => 'Unchanged note', 'reg_1' => 'UNCH123', ]); $session = api_fixtures()->createUserSession(['edit_order']); foreach (['/orders', '/order'] as $endpoint) { api_client()->put($endpoint, [ 'id' => $order['id'], 'field' => 'cashier_id', 'value' => 999999, ], $session['headers']) ->assertStatus(400) ->assertEnvelope() ->assertSuccess(false) ->assertMessage('Unsupported legacy order field: cashier_id'); } $row = api_fixtures()->fetchRowById('orders', (int)$order['id']); expect($row)->not->toBeNull(); expect($row['reference'] ?? null)->toBe('UNCHANGED-REF'); expect($row['notes'] ?? null)->toBe('Unchanged note'); expect($row['reg_1'] ?? null)->toBe('UNCH123'); }); it('deletes orders through the real endpoint', function (): void { api_test_covers('DELETE /orders', 'happy'); $department = api_fixtures()->createDepartment(); $customer = api_fixtures()->createUser(); $cashier = api_fixtures()->createUser(['display_name' => 'Delete Cashier']); $order = api_fixtures()->createOrder([ 'customer_id' => $customer['customer_number'], 'cashier_id' => $cashier['id'], 'department_id' => $department['id'], ]); $session = api_fixtures()->createUserSession([ 'delete_order', 'department_access_' . $department['id'], ]); $response = api_client()->delete('/orders', [ 'id' => $order['id'], ], $session['headers']); $response ->assertStatus(200) ->assertEnvelope() ->assertSuccess() ->assertMessage('Order deleted successfully'); $row = api_fixtures()->fetchRowById('orders', (int)$order['id']); expect($row)->not->toBeNull(); expect($row['deleted_at'] ?? null)->not->toBeNull(); }); it('requires explicit confirmation before deleting protected orders', function (): void { api_test_covers('DELETE /orders', 'failure'); api_test_covers('DELETE /orders', 'happy'); $department = api_fixtures()->createDepartment(); $customer = api_fixtures()->createUser(); $cashier = api_fixtures()->createUser(['display_name' => 'Protected Delete Cashier']); $session = api_fixtures()->createUserSession([ 'delete_order', 'department_access_' . $department['id'], ]); $cases = [ 'completed' => function () use ($department, $customer, $cashier): array { return api_fixtures()->createOrder([ 'customer_id' => $customer['customer_number'], 'cashier_id' => $cashier['id'], 'department_id' => $department['id'], 'completed_at' => date('Y-m-d H:i:s'), ]); }, 'order_items' => function () use ($department, $customer, $cashier): array { $order = api_fixtures()->createOrder([ 'customer_id' => $customer['customer_number'], 'cashier_id' => $cashier['id'], 'department_id' => $department['id'], ]); api_fixtures()->createOrderItem([ 'order_id' => $order['id'], 'product_id' => 53, 'cashier_id' => $cashier['id'], 'quantity' => 1, ]); return $order; }, 'attachments' => function () use ($department, $customer, $cashier): array { $order = api_fixtures()->createOrder([ 'customer_id' => $customer['customer_number'], 'cashier_id' => $cashier['id'], 'department_id' => $department['id'], ]); api_fixtures()->createOrderAttachment([ 'order_id' => $order['id'], ]); return $order; }, ]; foreach ($cases as $expectedReason => $createProtectedOrder) { $order = $createProtectedOrder(); $unconfirmed = api_client()->delete('/orders', [ 'id' => $order['id'], ], $session['headers']); $unconfirmed ->assertStatus(409) ->assertEnvelope() ->assertSuccess(false) ->assertMessage('Order deletion requires confirmation'); expect($unconfirmed->data()['requires_confirmation'] ?? null)->toBeTrue(); expect($unconfirmed->data()['protected_reasons'] ?? [])->toContain($expectedReason); expect(api_fixtures()->fetchRowById('orders', (int)$order['id'])['deleted_at'] ?? null)->toBeNull(); $confirmed = api_client()->delete('/orders', [ 'id' => $order['id'], 'confirmed' => 'true', ], $session['headers']); $confirmed ->assertStatus(200) ->assertEnvelope() ->assertSuccess() ->assertMessage('Order deleted successfully'); expect(api_fixtures()->fetchRowById('orders', (int)$order['id'])['deleted_at'] ?? null)->not->toBeNull(); } }); it('rejects invalid order delete requests', function (): void { api_test_covers('DELETE /orders', 'failure'); $department = api_fixtures()->createDepartment(); $session = api_fixtures()->createUserSession([ 'delete_order', 'department_access_' . $department['id'], ]); $missingId = api_client()->delete('/orders', [], $session['headers']); $missingId ->assertStatus(400) ->assertEnvelope() ->assertSuccess(false) ->assertMessage('ID is required'); $missingOrder = api_client()->delete('/orders', [ 'id' => 999999, ], $session['headers']); $missingOrder ->assertStatus(400) ->assertEnvelope() ->assertSuccess(false) ->assertMessage('Order not found'); });