Add bulk action preview and apply endpoints for collected invoices
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
usesApiSuite();
|
||||
|
||||
function bulk_action_order_item_deleted_at(int $orderItemId): ?string
|
||||
{
|
||||
$row = api_test_runtime()->queryOne('SELECT deleted_at FROM order_items WHERE id = ' . $orderItemId . ' LIMIT 1');
|
||||
return $row['deleted_at'] ?? null;
|
||||
}
|
||||
|
||||
function bulk_action_order_invoice_collection_id(int $orderId): int
|
||||
{
|
||||
$row = api_test_runtime()->queryOne('SELECT invoice_collection_id FROM orders WHERE id = ' . $orderId . ' LIMIT 1');
|
||||
return (int)($row['invoice_collection_id'] ?? 0);
|
||||
}
|
||||
|
||||
it('previews and applies customer rule cleanup only after exact typed confirmation', function (): void {
|
||||
api_test_covers('POST /collected-invoices/bulk-actions/preview', 'customer-rule-cleanup');
|
||||
api_test_covers('POST /collected-invoices/bulk-actions/apply', 'customer-rule-cleanup');
|
||||
|
||||
$customer = api_fixtures()->createUser(['display_name' => 'Bulk Cleanup Customer']);
|
||||
api_fixtures()->addCustomerAttribute((int)$customer['id'], 'restrictSpotFree');
|
||||
$department = api_fixtures()->createDepartment();
|
||||
$invoiceCollection = api_fixtures()->createInvoiceCollection([
|
||||
'customer_number' => $customer['customer_number'],
|
||||
]);
|
||||
$order = api_fixtures()->createOrder([
|
||||
'customer_id' => $customer['customer_number'],
|
||||
'department_id' => $department['id'],
|
||||
'invoice_collection_id' => $invoiceCollection['id'],
|
||||
]);
|
||||
$product = api_fixtures()->createProduct([
|
||||
'name' => 'Spot Free rinse',
|
||||
'price' => 80,
|
||||
]);
|
||||
$orderItem = api_fixtures()->createOrderItem([
|
||||
'order_id' => $order['id'],
|
||||
'product_id' => $product['id'],
|
||||
'cashier_id' => 1,
|
||||
'price' => 80,
|
||||
]);
|
||||
$session = api_fixtures()->createUserSession(['reset_collected_invoice_economic']);
|
||||
|
||||
$previewResponse = api_client()->post('/collected-invoices/bulk-actions/preview', [
|
||||
'action' => 'remove_customer_rule_violations',
|
||||
'invoice_collection_ids' => [$invoiceCollection['id']],
|
||||
'locale' => 'da',
|
||||
], $session['headers']);
|
||||
|
||||
$previewResponse
|
||||
->assertStatus(200)
|
||||
->assertEnvelope()
|
||||
->assertSuccess();
|
||||
|
||||
$preview = $previewResponse->data();
|
||||
expect($preview['preview_id'] ?? null)->toBeString()
|
||||
->and($preview['confirmation_phrase'] ?? null)->toBe('Bekræft')
|
||||
->and($preview['summary']['changed_count'] ?? null)->toBe(1)
|
||||
->and($preview['order_items'][0]['order_item_id'] ?? null)->toBe((int)$orderItem['id'])
|
||||
->and(bulk_action_order_item_deleted_at((int)$orderItem['id']))->toBeNull();
|
||||
|
||||
api_client()->post('/collected-invoices/bulk-actions/apply', [
|
||||
'preview_id' => $preview['preview_id'],
|
||||
'action' => 'remove_customer_rule_violations',
|
||||
'invoice_collection_ids' => [$invoiceCollection['id']],
|
||||
'confirmation_text' => 'Bekraeft',
|
||||
'locale' => 'da',
|
||||
], $session['headers'])
|
||||
->assertStatus(400)
|
||||
->assertEnvelope()
|
||||
->assertSuccess(false);
|
||||
|
||||
expect(bulk_action_order_item_deleted_at((int)$orderItem['id']))->toBeNull();
|
||||
|
||||
$applyResponse = api_client()->post('/collected-invoices/bulk-actions/apply', [
|
||||
'preview_id' => $preview['preview_id'],
|
||||
'action' => 'remove_customer_rule_violations',
|
||||
'invoice_collection_ids' => [$invoiceCollection['id']],
|
||||
'confirmation_text' => 'Bekræft',
|
||||
'locale' => 'da',
|
||||
], $session['headers']);
|
||||
|
||||
$applyResponse
|
||||
->assertStatus(200)
|
||||
->assertEnvelope()
|
||||
->assertSuccess();
|
||||
|
||||
$applied = $applyResponse->data();
|
||||
expect($applied['preview'] ?? null)->toBeFalse()
|
||||
->and($applied['result']['changed_count'] ?? null)->toBe(1)
|
||||
->and(bulk_action_order_item_deleted_at((int)$orderItem['id']))->not->toBeNull();
|
||||
});
|
||||
|
||||
it('merges selected invoice collections into the explicit target after confirmation', function (): void {
|
||||
api_test_covers('POST /collected-invoices/bulk-actions/preview', 'merge');
|
||||
api_test_covers('POST /collected-invoices/bulk-actions/apply', 'merge');
|
||||
|
||||
$customer = api_fixtures()->createUser(['display_name' => 'Bulk Merge Customer']);
|
||||
$department = api_fixtures()->createDepartment();
|
||||
$targetCollection = api_fixtures()->createInvoiceCollection([
|
||||
'customer_number' => $customer['customer_number'],
|
||||
]);
|
||||
$sourceCollection = api_fixtures()->createInvoiceCollection([
|
||||
'customer_number' => $customer['customer_number'],
|
||||
]);
|
||||
$targetOrder = api_fixtures()->createOrder([
|
||||
'customer_id' => $customer['customer_number'],
|
||||
'department_id' => $department['id'],
|
||||
'invoice_collection_id' => $targetCollection['id'],
|
||||
]);
|
||||
$sourceOrder = api_fixtures()->createOrder([
|
||||
'customer_id' => $customer['customer_number'],
|
||||
'department_id' => $department['id'],
|
||||
'invoice_collection_id' => $sourceCollection['id'],
|
||||
]);
|
||||
$session = api_fixtures()->createUserSession(['move_collected_invoice']);
|
||||
|
||||
$previewResponse = api_client()->post('/collected-invoices/bulk-actions/preview', [
|
||||
'action' => 'merge_collections',
|
||||
'invoice_collection_ids' => [$sourceCollection['id'], $targetCollection['id']],
|
||||
'options' => ['target_invoice_collection_id' => $targetCollection['id']],
|
||||
'locale' => 'en',
|
||||
], $session['headers']);
|
||||
|
||||
$previewResponse
|
||||
->assertStatus(200)
|
||||
->assertEnvelope()
|
||||
->assertSuccess();
|
||||
|
||||
$preview = $previewResponse->data();
|
||||
expect($preview['confirmation_phrase'] ?? null)->toBe('Confirm')
|
||||
->and($preview['target_invoice_collection_id'] ?? null)->toBe((int)$targetCollection['id'])
|
||||
->and($preview['summary']['orders_to_move'] ?? null)->toBe(1)
|
||||
->and(bulk_action_order_invoice_collection_id((int)$sourceOrder['id']))->toBe((int)$sourceCollection['id']);
|
||||
|
||||
$applyResponse = api_client()->post('/collected-invoices/bulk-actions/apply', [
|
||||
'preview_id' => $preview['preview_id'],
|
||||
'action' => 'merge_collections',
|
||||
'invoice_collection_ids' => [$sourceCollection['id'], $targetCollection['id']],
|
||||
'options' => ['target_invoice_collection_id' => $targetCollection['id']],
|
||||
'confirmation_text' => 'Confirm',
|
||||
'locale' => 'en',
|
||||
], $session['headers']);
|
||||
|
||||
$applyResponse
|
||||
->assertStatus(200)
|
||||
->assertEnvelope()
|
||||
->assertSuccess();
|
||||
|
||||
expect(bulk_action_order_invoice_collection_id((int)$sourceOrder['id']))->toBe((int)$targetCollection['id'])
|
||||
->and(bulk_action_order_invoice_collection_id((int)$targetOrder['id']))->toBe((int)$targetCollection['id']);
|
||||
});
|
||||
@@ -24,6 +24,38 @@ it('builds stable XL Vask automation item signatures', function (): void {
|
||||
]);
|
||||
});
|
||||
|
||||
it('identifies strict price agreement matches by product, quantity, and total', function (): void {
|
||||
$usageItems = [
|
||||
['product_id' => 10, 'quantity' => 1, 'price' => 519],
|
||||
['product_id' => 24, 'quantity' => 1, 'price' => 39],
|
||||
['product_id' => 21, 'quantity' => 1, 'price' => 79],
|
||||
];
|
||||
$orderItems = [
|
||||
['product_id' => 21, 'quantity' => 1, 'price' => 79],
|
||||
['product_id' => 10, 'quantity' => 1, 'price' => 519],
|
||||
['product_id' => 24, 'quantity' => 1, 'price' => 39],
|
||||
];
|
||||
|
||||
expect(xlvask_automation_service::isExactItemMatchForAutomation($usageItems, $orderItems))
|
||||
->toBeTrue();
|
||||
});
|
||||
|
||||
it('rejects price agreement automation when product lines differ despite equal total', function (): void {
|
||||
$usageItems = [
|
||||
['product_id' => 10, 'quantity' => 1, 'price' => 519],
|
||||
['product_id' => 24, 'quantity' => 1, 'price' => 39],
|
||||
];
|
||||
$orderItems = [
|
||||
['product_id' => 10, 'quantity' => 1, 'price' => 519],
|
||||
['product_id' => 50, 'quantity' => 1, 'price' => 39],
|
||||
];
|
||||
|
||||
expect(xlvask_automation_service::itemsTotalForAutomation($usageItems))
|
||||
->toBe(xlvask_automation_service::itemsTotalForAutomation($orderItems))
|
||||
->and(xlvask_automation_service::isExactItemMatchForAutomation($usageItems, $orderItems))
|
||||
->toBeFalse();
|
||||
});
|
||||
|
||||
it('normalizes persisted XL Vask usage-log rows before helper hydration', function (): void {
|
||||
$row = xlvask_automation_service::normalizeUsageLogRowForAutomation([
|
||||
'id' => 47086,
|
||||
@@ -107,6 +139,23 @@ it('declares cached amount summary columns for XL Vask usage logs', function ():
|
||||
->toContain('cached_amount_at');
|
||||
});
|
||||
|
||||
it('keeps automatic XL Vask execution scoped to exact attachments', function (): void {
|
||||
$serviceContent = file_get_contents(WD . '/classes/xlvask_automation_service.php');
|
||||
|
||||
expect($serviceContent)
|
||||
->toContain('&& $this->isExactAttachSuggestionForContext($suggestion, $context)')
|
||||
->toContain("\$candidateOrderJson = \$suggestion['candidate_order_json'] ?? null;")
|
||||
->toContain("return 'Automatisk accepteret: Prisoverensstemmelse.';");
|
||||
});
|
||||
|
||||
it('does not automatically create XL Vask orders', function (): void {
|
||||
$serviceContent = file_get_contents(WD . '/classes/xlvask_automation_service.php');
|
||||
|
||||
expect($serviceContent)
|
||||
->toContain('if ($action === self::ACTION_CREATE) {')
|
||||
->toContain('return false;');
|
||||
});
|
||||
|
||||
it('scores same-day orders with matching XL Vask products and extra add-ons as attach suggestions', function (): void {
|
||||
$usageItems = [
|
||||
['product_id' => 10, 'quantity' => 1, 'price' => 519, 'product' => ['name' => 'Forvogn']],
|
||||
|
||||
Reference in New Issue
Block a user