Files
api/services/nginx/app/tests/Api/CollectedInvoiceMonthlySplitApiTest.php
T

499 lines
22 KiB
PHP

<?php
declare(strict_types=1);
usesApiSuite();
function monthly_split_order_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);
}
function monthly_split_invoice_row(int $invoiceCollectionId): array
{
return api_test_runtime()->queryOne('SELECT id, created_at, closed_at FROM collected_order_invoices WHERE id = ' . $invoiceCollectionId . ' LIMIT 1') ?? [];
}
function monthly_split_customer_collection_count(int $customerNumber): int
{
$row = api_test_runtime()->queryOne('SELECT COUNT(*) AS count FROM collected_order_invoices WHERE customer_number = ' . $customerNumber);
return (int)($row['count'] ?? 0);
}
function monthly_split_cleanup_collections(array $invoiceCollectionIds): void
{
$ids = array_values(array_unique(array_filter(array_map('intval', $invoiceCollectionIds), static fn(int $id): bool => $id > 0)));
if ($ids === []) {
return;
}
api_test_runtime()->db()->query('UPDATE orders SET invoice_collection_id = NULL WHERE invoice_collection_id IN (' . implode(',', $ids) . ')');
api_test_runtime()->db()->query('DELETE FROM collected_order_invoices WHERE id IN (' . implode(',', $ids) . ')');
}
it('previews monthly split changes without moving orders or creating collections', function (): void {
api_test_covers('POST /collected-invoices/split-by-month', 'preview');
$customer = api_fixtures()->createUser(['display_name' => 'Preview Monthly Split Customer']);
$department = api_fixtures()->createDepartment();
$invoiceCollection = api_fixtures()->createInvoiceCollection([
'customer_number' => $customer['customer_number'],
]);
$marchOrder = api_fixtures()->createOrder([
'customer_id' => $customer['customer_number'],
'department_id' => $department['id'],
'invoice_collection_id' => $invoiceCollection['id'],
'created_at' => '2096-03-15 10:00:00',
]);
$aprilOrder = api_fixtures()->createOrder([
'customer_id' => $customer['customer_number'],
'department_id' => $department['id'],
'invoice_collection_id' => $invoiceCollection['id'],
'created_at' => '2096-04-02 10:00:00',
]);
$session = api_fixtures()->createUserSession(['split_collected_invoice']);
$collectionCountBefore = monthly_split_customer_collection_count((int)$customer['customer_number']);
$response = api_client()->post('/collected-invoices/split-by-month', [
'dateFrom' => '2096-03-01',
'dateTo' => '2096-04-30',
'preview' => true,
], $session['headers']);
$response
->assertStatus(200)
->assertEnvelope()
->assertSuccess();
$payload = $response->data();
$months = (array)($payload['changed'][0]['months'] ?? []);
expect($payload['preview'] ?? null)->toBeTrue()
->and($payload['processed_count'] ?? null)->toBe(1)
->and($payload['changed_count'] ?? null)->toBe(1)
->and($payload['changed'][0]['created_invoice_collection_ids'] ?? null)->toBe([])
->and($months[0]['month'] ?? null)->toBe('2096-03')
->and($months[0]['will_create_collection'] ?? null)->toBeFalse()
->and($months[0]['target_invoice_collection_id'] ?? null)->toBe((int)$invoiceCollection['id'])
->and($months[0]['closed_at'] ?? null)->toBeNull()
->and($months[1]['month'] ?? null)->toBe('2096-04')
->and($months[1]['will_create_collection'] ?? null)->toBeTrue()
->and($months[1]['target_invoice_collection_id'] ?? null)->toBeNull()
->and($months[1]['closed_at'] ?? null)->toBeNull()
->and(monthly_split_customer_collection_count((int)$customer['customer_number']))->toBe($collectionCountBefore)
->and(monthly_split_order_collection_id((int)$marchOrder['id']))->toBe((int)$invoiceCollection['id'])
->and(monthly_split_order_collection_id((int)$aprilOrder['id']))->toBe((int)$invoiceCollection['id']);
});
it('previews only explicit monthly split invoice collection ids', function (): void {
api_test_covers('POST /collected-invoices/split-by-month', 'preview-scope');
$customer = api_fixtures()->createUser(['display_name' => 'Scoped Preview Monthly Split Customer']);
$department = api_fixtures()->createDepartment();
$targetCollection = api_fixtures()->createInvoiceCollection([
'customer_number' => $customer['customer_number'],
]);
$ignoredCollection = api_fixtures()->createInvoiceCollection([
'customer_number' => $customer['customer_number'],
]);
$targetMarchOrder = api_fixtures()->createOrder([
'customer_id' => $customer['customer_number'],
'department_id' => $department['id'],
'invoice_collection_id' => $targetCollection['id'],
'created_at' => '2096-03-15 10:00:00',
]);
$targetAprilOrder = api_fixtures()->createOrder([
'customer_id' => $customer['customer_number'],
'department_id' => $department['id'],
'invoice_collection_id' => $targetCollection['id'],
'created_at' => '2096-04-02 10:00:00',
]);
$ignoredMarchOrder = api_fixtures()->createOrder([
'customer_id' => $customer['customer_number'],
'department_id' => $department['id'],
'invoice_collection_id' => $ignoredCollection['id'],
'created_at' => '2096-03-16 10:00:00',
]);
$ignoredAprilOrder = api_fixtures()->createOrder([
'customer_id' => $customer['customer_number'],
'department_id' => $department['id'],
'invoice_collection_id' => $ignoredCollection['id'],
'created_at' => '2096-04-03 10:00:00',
]);
$session = api_fixtures()->createUserSession(['split_collected_invoice']);
$response = api_client()->post('/collected-invoices/split-by-month', [
'dateFrom' => '2096-03-01',
'dateTo' => '2096-04-30',
'invoice_collection_ids' => [$targetCollection['id']],
'preview' => true,
], $session['headers']);
$response
->assertStatus(200)
->assertEnvelope()
->assertSuccess();
$payload = $response->data();
expect($payload['processed_count'] ?? null)->toBe(1)
->and($payload['changed_count'] ?? null)->toBe(1)
->and($payload['changed'][0]['invoice_collection_id'] ?? null)->toBe((int)$targetCollection['id'])
->and(monthly_split_order_collection_id((int)$targetMarchOrder['id']))->toBe((int)$targetCollection['id'])
->and(monthly_split_order_collection_id((int)$targetAprilOrder['id']))->toBe((int)$targetCollection['id'])
->and(monthly_split_order_collection_id((int)$ignoredMarchOrder['id']))->toBe((int)$ignoredCollection['id'])
->and(monthly_split_order_collection_id((int)$ignoredAprilOrder['id']))->toBe((int)$ignoredCollection['id']);
});
it('splits a selected March and April collected invoice into monthly collections', function (): void {
api_test_covers('POST /collected-invoices/split-by-month', 'happy');
$customer = api_fixtures()->createUser(['display_name' => 'Monthly Split Customer']);
$department = api_fixtures()->createDepartment();
$invoiceCollection = api_fixtures()->createInvoiceCollection([
'customer_number' => $customer['customer_number'],
'created_at' => '2096-03-01 00:00:01',
]);
$marchOrder = api_fixtures()->createOrder([
'customer_id' => $customer['customer_number'],
'department_id' => $department['id'],
'invoice_collection_id' => $invoiceCollection['id'],
'created_at' => '2096-03-15 10:00:00',
]);
$aprilOrder = api_fixtures()->createOrder([
'customer_id' => $customer['customer_number'],
'department_id' => $department['id'],
'invoice_collection_id' => $invoiceCollection['id'],
'created_at' => '2096-04-02 10:00:00',
]);
$session = api_fixtures()->createUserSession(['split_collected_invoice']);
$createdCollectionIds = [];
try {
$response = api_client()->post('/collected-invoices/split-by-month', [
'dateFrom' => '2096-03-01',
'dateTo' => '2096-04-30',
], $session['headers']);
$response
->assertStatus(200)
->assertEnvelope()
->assertSuccess();
$payload = $response->data();
$createdCollectionIds = (array)($payload['changed'][0]['created_invoice_collection_ids'] ?? []);
$aprilCollectionId = (int)($createdCollectionIds[0] ?? 0);
expect($payload['processed_count'] ?? null)->toBe(1)
->and($payload['changed_count'] ?? null)->toBe(1)
->and($payload['skipped_count'] ?? null)->toBe(0)
->and($aprilCollectionId)->toBeGreaterThan(0)
->and(monthly_split_order_collection_id((int)$marchOrder['id']))->toBe((int)$invoiceCollection['id'])
->and(monthly_split_order_collection_id((int)$aprilOrder['id']))->toBe($aprilCollectionId)
->and(monthly_split_invoice_row((int)$invoiceCollection['id'])['created_at'] ?? null)->toBe('2096-03-01 00:00:01')
->and(monthly_split_invoice_row((int)$invoiceCollection['id'])['closed_at'] ?? null)->toBeNull()
->and(monthly_split_invoice_row($aprilCollectionId)['created_at'] ?? null)->toBe('2096-04-01 00:00:01')
->and(monthly_split_invoice_row($aprilCollectionId)['closed_at'] ?? null)->toBeNull();
} finally {
monthly_split_cleanup_collections($createdCollectionIds);
}
});
it('splits only explicit monthly split invoice collection ids', function (): void {
api_test_covers('POST /collected-invoices/split-by-month', 'scope');
$customer = api_fixtures()->createUser(['display_name' => 'Scoped Monthly Split Customer']);
$department = api_fixtures()->createDepartment();
$targetCollection = api_fixtures()->createInvoiceCollection([
'customer_number' => $customer['customer_number'],
'created_at' => '2096-03-01 00:00:01',
]);
$ignoredCollection = api_fixtures()->createInvoiceCollection([
'customer_number' => $customer['customer_number'],
'created_at' => '2096-03-01 00:00:01',
]);
$targetMarchOrder = api_fixtures()->createOrder([
'customer_id' => $customer['customer_number'],
'department_id' => $department['id'],
'invoice_collection_id' => $targetCollection['id'],
'created_at' => '2096-03-15 10:00:00',
]);
$targetAprilOrder = api_fixtures()->createOrder([
'customer_id' => $customer['customer_number'],
'department_id' => $department['id'],
'invoice_collection_id' => $targetCollection['id'],
'created_at' => '2096-04-02 10:00:00',
]);
$ignoredMarchOrder = api_fixtures()->createOrder([
'customer_id' => $customer['customer_number'],
'department_id' => $department['id'],
'invoice_collection_id' => $ignoredCollection['id'],
'created_at' => '2096-03-16 10:00:00',
]);
$ignoredAprilOrder = api_fixtures()->createOrder([
'customer_id' => $customer['customer_number'],
'department_id' => $department['id'],
'invoice_collection_id' => $ignoredCollection['id'],
'created_at' => '2096-04-03 10:00:00',
]);
$session = api_fixtures()->createUserSession(['split_collected_invoice']);
$createdCollectionIds = [];
try {
$response = api_client()->post('/collected-invoices/split-by-month', [
'dateFrom' => '2096-03-01',
'dateTo' => '2096-04-30',
'invoice_collection_ids' => [$targetCollection['id']],
], $session['headers']);
$response
->assertStatus(200)
->assertEnvelope()
->assertSuccess();
$payload = $response->data();
$createdCollectionIds = (array)($payload['changed'][0]['created_invoice_collection_ids'] ?? []);
$aprilCollectionId = (int)($createdCollectionIds[0] ?? 0);
expect($payload['processed_count'] ?? null)->toBe(1)
->and($payload['changed_count'] ?? null)->toBe(1)
->and($aprilCollectionId)->toBeGreaterThan(0)
->and(monthly_split_order_collection_id((int)$targetMarchOrder['id']))->toBe((int)$targetCollection['id'])
->and(monthly_split_order_collection_id((int)$targetAprilOrder['id']))->toBe($aprilCollectionId)
->and(monthly_split_order_collection_id((int)$ignoredMarchOrder['id']))->toBe((int)$ignoredCollection['id'])
->and(monthly_split_order_collection_id((int)$ignoredAprilOrder['id']))->toBe((int)$ignoredCollection['id']);
} finally {
monthly_split_cleanup_collections($createdCollectionIds);
}
});
it('sets closed_at to month end when split month has ended', function (): void {
api_test_covers('POST /collected-invoices/split-by-month', 'closed-at');
$customer = api_fixtures()->createUser(['display_name' => 'Ended Monthly Split Customer']);
$department = api_fixtures()->createDepartment();
$invoiceCollection = api_fixtures()->createInvoiceCollection([
'customer_number' => $customer['customer_number'],
'created_at' => '2001-03-01 00:00:01',
]);
$marchOrder = api_fixtures()->createOrder([
'customer_id' => $customer['customer_number'],
'department_id' => $department['id'],
'invoice_collection_id' => $invoiceCollection['id'],
'created_at' => '2001-03-15 10:00:00',
]);
$aprilOrder = api_fixtures()->createOrder([
'customer_id' => $customer['customer_number'],
'department_id' => $department['id'],
'invoice_collection_id' => $invoiceCollection['id'],
'created_at' => '2001-04-02 10:00:00',
]);
$session = api_fixtures()->createUserSession(['split_collected_invoice']);
$createdCollectionIds = [];
try {
$response = api_client()->post('/collected-invoices/split-by-month', [
'dateFrom' => '2001-03-01',
'dateTo' => '2001-04-30',
], $session['headers']);
$response
->assertStatus(200)
->assertEnvelope()
->assertSuccess();
$payload = $response->data();
$createdCollectionIds = (array)($payload['changed'][0]['created_invoice_collection_ids'] ?? []);
$aprilCollectionId = (int)($createdCollectionIds[0] ?? 0);
expect($payload['changed_count'] ?? null)->toBe(1)
->and(monthly_split_order_collection_id((int)$marchOrder['id']))->toBe((int)$invoiceCollection['id'])
->and(monthly_split_order_collection_id((int)$aprilOrder['id']))->toBe($aprilCollectionId)
->and(monthly_split_invoice_row((int)$invoiceCollection['id'])['closed_at'] ?? null)->toBe('2001-03-31 23:59:59')
->and(monthly_split_invoice_row($aprilCollectionId)['closed_at'] ?? null)->toBe('2001-04-30 23:59:59');
} finally {
monthly_split_cleanup_collections($createdCollectionIds);
}
});
it('splits the whole affected collection even when only one month is selected', function (): void {
api_test_covers('POST /collected-invoices/split-by-month', 'partial');
$customer = api_fixtures()->createUser(['display_name' => 'Partial Monthly Split Customer']);
$department = api_fixtures()->createDepartment();
$invoiceCollection = api_fixtures()->createInvoiceCollection([
'customer_number' => $customer['customer_number'],
]);
$marchOrder = api_fixtures()->createOrder([
'customer_id' => $customer['customer_number'],
'department_id' => $department['id'],
'invoice_collection_id' => $invoiceCollection['id'],
'created_at' => '2096-03-20 09:00:00',
]);
$aprilOrder = api_fixtures()->createOrder([
'customer_id' => $customer['customer_number'],
'department_id' => $department['id'],
'invoice_collection_id' => $invoiceCollection['id'],
'created_at' => '2096-04-10 09:00:00',
]);
$session = api_fixtures()->createUserSession(['split_collected_invoice']);
$createdCollectionIds = [];
try {
$response = api_client()->post('/collected-invoices/split-by-month', [
'dateFrom' => '2096-03-01',
'dateTo' => '2096-03-31',
], $session['headers']);
$response
->assertStatus(200)
->assertEnvelope()
->assertSuccess();
$payload = $response->data();
$createdCollectionIds = (array)($payload['changed'][0]['created_invoice_collection_ids'] ?? []);
$aprilCollectionId = (int)($createdCollectionIds[0] ?? 0);
expect($payload['changed_count'] ?? null)->toBe(1)
->and(monthly_split_order_collection_id((int)$marchOrder['id']))->toBe((int)$invoiceCollection['id'])
->and(monthly_split_order_collection_id((int)$aprilOrder['id']))->toBe($aprilCollectionId);
} finally {
monthly_split_cleanup_collections($createdCollectionIds);
}
});
it('does not affect booked collected invoices', function (): void {
api_test_covers('POST /collected-invoices/split-by-month', 'booked-skip');
$customer = api_fixtures()->createUser(['display_name' => 'Booked Monthly Split Customer']);
$department = api_fixtures()->createDepartment();
$invoiceCollection = api_fixtures()->createInvoiceCollection([
'customer_number' => $customer['customer_number'],
'booked_invoice_id' => 987654,
]);
$marchOrder = api_fixtures()->createOrder([
'customer_id' => $customer['customer_number'],
'department_id' => $department['id'],
'invoice_collection_id' => $invoiceCollection['id'],
'created_at' => '2096-03-05 12:00:00',
]);
$aprilOrder = api_fixtures()->createOrder([
'customer_id' => $customer['customer_number'],
'department_id' => $department['id'],
'invoice_collection_id' => $invoiceCollection['id'],
'created_at' => '2096-04-05 12:00:00',
]);
$session = api_fixtures()->createUserSession(['split_collected_invoice']);
$response = api_client()->post('/collected-invoices/split-by-month', [
'dateFrom' => '2096-03-01',
'dateTo' => '2096-03-31',
], $session['headers']);
$response
->assertStatus(200)
->assertEnvelope()
->assertSuccess();
$payload = $response->data();
expect($payload['changed_count'] ?? null)->toBe(0)
->and($payload['skipped_count'] ?? null)->toBe(1)
->and((string)($payload['skipped'][0]['message'] ?? ''))->toContain('booked')
->and(monthly_split_order_collection_id((int)$marchOrder['id']))->toBe((int)$invoiceCollection['id'])
->and(monthly_split_order_collection_id((int)$aprilOrder['id']))->toBe((int)$invoiceCollection['id']);
});
it('skips draft linked, Stripe, and single month collections', function (): void {
api_test_covers('POST /collected-invoices/split-by-month', 'skip');
$customer = api_fixtures()->createUser(['display_name' => 'Skipped Monthly Split Customer']);
$department = api_fixtures()->createDepartment();
$draftCollection = api_fixtures()->createInvoiceCollection([
'customer_number' => $customer['customer_number'],
'external_id' => 'draft-external-reference',
]);
$stripeCollection = api_fixtures()->createInvoiceCollection([
'customer_number' => $customer['customer_number'],
'processor' => 2,
]);
$singleMonthCollection = api_fixtures()->createInvoiceCollection([
'customer_number' => $customer['customer_number'],
]);
foreach ([$draftCollection, $stripeCollection] as $collection) {
api_fixtures()->createOrder([
'customer_id' => $customer['customer_number'],
'department_id' => $department['id'],
'invoice_collection_id' => $collection['id'],
'created_at' => '2096-03-05 12:00:00',
]);
api_fixtures()->createOrder([
'customer_id' => $customer['customer_number'],
'department_id' => $department['id'],
'invoice_collection_id' => $collection['id'],
'created_at' => '2096-04-05 12:00:00',
]);
}
api_fixtures()->createOrder([
'customer_id' => $customer['customer_number'],
'department_id' => $department['id'],
'invoice_collection_id' => $singleMonthCollection['id'],
'created_at' => '2096-03-10 12:00:00',
]);
$session = api_fixtures()->createUserSession(['split_collected_invoice']);
$response = api_client()->post('/collected-invoices/split-by-month', [
'dateFrom' => '2096-03-01',
'dateTo' => '2096-03-31',
], $session['headers']);
$response
->assertStatus(200)
->assertEnvelope()
->assertSuccess();
$payload = $response->data();
expect($payload['processed_count'] ?? null)->toBe(3)
->and($payload['changed_count'] ?? null)->toBe(0)
->and($payload['skipped_count'] ?? null)->toBe(3);
});
it('rejects invalid monthly split date ranges', function (): void {
api_test_covers('POST /collected-invoices/split-by-month', 'failure');
$session = api_fixtures()->createUserSession(['split_collected_invoice']);
api_client()->post('/collected-invoices/split-by-month', [
'dateFrom' => '2096-04-30',
'dateTo' => '2096-03-01',
], $session['headers'])
->assertStatus(400)
->assertEnvelope()
->assertSuccess(false);
});
it('rejects invalid explicit monthly split invoice collection ids', function (): void {
api_test_covers('POST /collected-invoices/split-by-month', 'invalid-scope');
$session = api_fixtures()->createUserSession(['split_collected_invoice']);
api_client()->post('/collected-invoices/split-by-month', [
'dateFrom' => '2096-03-01',
'dateTo' => '2096-04-30',
'invoice_collection_ids' => ['not-a-number'],
], $session['headers'])
->assertStatus(400)
->assertEnvelope()
->assertSuccess(false);
api_client()->post('/collected-invoices/split-by-month', [
'dateFrom' => '2096-03-01',
'dateTo' => '2096-04-30',
'invoice_collection_ids' => [],
], $session['headers'])
->assertStatus(400)
->assertEnvelope()
->assertSuccess(false);
});