Compare commits

...
Author SHA1 Message Date
Jeppe Bundgaard 62f2c80dda Scope monthly invoice split endpoint 2026-07-06 13:37:31 +02:00
2 changed files with 188 additions and 12 deletions
@@ -612,21 +612,46 @@ class orderInvoicesRoute
$preview ? 'User previewed splitting collected order invoices by month' : 'User split collected order invoices by order month' $preview ? 'User previewed splitting collected order invoices by month' : 'User split collected order invoices by order month'
); );
$date_from = $db->escape_string($date_range['dateFrom']);
$date_to = $db->escape_string($date_range['dateTo']);
$sql = "SELECT DISTINCT invoice_collection_id
FROM orders
WHERE created_at BETWEEN '$date_from' AND '$date_to'
AND invoice_collection_id IS NOT NULL
AND invoice_collection_id > 0
AND deleted_at IS NULL";
$query_result = $db->query($sql);
$invoice_collection_ids = []; $invoice_collection_ids = [];
while ($row = $query_result->fetch_assoc()) { if (self::isParametersSet(['invoice_collection_ids'])) {
$invoice_collection_id = (int)($row['invoice_collection_id'] ?? 0); $invoice_collection_ids_raw = self::getParameter('invoice_collection_ids');
if ($invoice_collection_id > 0) { if (!is_array($invoice_collection_ids_raw)) {
$response->error('invoice_collection_ids must be an array', 400);
}
foreach ($invoice_collection_ids_raw as $invoice_collection_id_raw) {
if (is_array($invoice_collection_id_raw) || is_object($invoice_collection_id_raw) || !is_numeric($invoice_collection_id_raw)) {
$response->error('invoice_collection_ids must contain only positive integer ids', 400);
}
$invoice_collection_id = (int)$invoice_collection_id_raw;
if ($invoice_collection_id < 1 || $invoice_collection_id > 999999999) {
$response->error('invoice_collection_ids must contain only positive integer ids', 400);
}
$invoice_collection_ids[] = $invoice_collection_id; $invoice_collection_ids[] = $invoice_collection_id;
} }
$invoice_collection_ids = array_values(array_unique($invoice_collection_ids));
if (empty($invoice_collection_ids)) {
$response->error('invoice_collection_ids must contain at least one id', 400);
}
} else {
$date_from = $db->escape_string($date_range['dateFrom']);
$date_to = $db->escape_string($date_range['dateTo']);
$sql = "SELECT DISTINCT invoice_collection_id
FROM orders
WHERE created_at BETWEEN '$date_from' AND '$date_to'
AND invoice_collection_id IS NOT NULL
AND invoice_collection_id > 0
AND deleted_at IS NULL";
$query_result = $db->query($sql);
while ($row = $query_result->fetch_assoc()) {
$invoice_collection_id = (int)($row['invoice_collection_id'] ?? 0);
if ($invoice_collection_id > 0) {
$invoice_collection_ids[] = $invoice_collection_id;
}
}
} }
$items = []; $items = [];
@@ -85,6 +85,65 @@ it('previews monthly split changes without moving orders or creating collections
->and(monthly_split_order_collection_id((int)$aprilOrder['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 { it('splits a selected March and April collected invoice into monthly collections', function (): void {
api_test_covers('POST /collected-invoices/split-by-month', 'happy'); api_test_covers('POST /collected-invoices/split-by-month', 'happy');
@@ -139,6 +198,74 @@ it('splits a selected March and April collected invoice into monthly collections
} }
}); });
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 { 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'); api_test_covers('POST /collected-invoices/split-by-month', 'closed-at');
@@ -345,3 +472,27 @@ it('rejects invalid monthly split date ranges', function (): void {
->assertEnvelope() ->assertEnvelope()
->assertSuccess(false); ->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);
});