Files
api/services/nginx/app/tests/Unit/Orders/OrdersRegistrationDateRangeQueryTest.php
T
Jeppe BandWorktree Fix Verifier 82a3684f05 fix(api): order getOrdersWithRegistrationNumberInDateRange by id ASC (#362)
## Summary

orders_o::getOrdersWithRegistrationNumberInDateRange() selects orders
matching a registration number within a date range without an explicit
ORDER BY clause. MySQL is free to return rows in any order. The endpoint
at routes/orderInvoicesRoute.php then iterates the result and calls
assignToInvoiceCollection() on each row, so the audit-log +
invoice-collection numbering depend on the arbitrary backend row order.

Add a stable `ORDER BY id ASC` to the SELECT and pin the contract with a
new Pest unit test.

## Test plan

- New Pest test `OrdersRegistrationDateRangeQueryTest` asserts the
SELECT still carries `ORDER BY id ASC`.
- Existing tests in the same file still pass unchanged (they don't
assert on ordering).
- Manual php -l on both modified files shows no syntax errors.

## Commits

- bbd50239 fix(api): order getOrdersWithRegistrationNumberInDateRange by
id ASC

Co-authored-by: Worktree Fix Verifier <agent@truckwash.local>
2026-08-10 20:33:10 +02:00

152 lines
4.4 KiB
PHP

<?php
app_require('classes/orders_schema_bootstrap.php');
app_require('objects/orders_o.php');
use objects\orders_o;
final class OrdersRegistrationDateRangeDbResultStub
{
public int $num_rows;
/** @var array<int, array<string, mixed>> */
private array $rows;
/**
* @param array<int, array<string, mixed>> $rows
*/
public function __construct(array $rows)
{
$this->rows = array_values($rows);
$this->num_rows = count($this->rows);
}
/**
* @return array<string, mixed>|null
*/
public function fetch_assoc(): ?array
{
if ($this->rows === []) {
return null;
}
return array_shift($this->rows);
}
}
final class OrdersRegistrationDateRangeDbStub
{
public string $lastQuery = '';
public int $queryCalls = 0;
public function escape_string(string $value): string
{
return addslashes($value);
}
public function query(string $sql): OrdersRegistrationDateRangeDbResultStub
{
$this->queryCalls++;
$this->lastQuery = $sql;
return new OrdersRegistrationDateRangeDbResultStub([]);
}
}
it('applies created_at bounds directly in SQL when filtering orders by registration number', function (): void {
$dbStub = new OrdersRegistrationDateRangeDbStub();
$hadDb = array_key_exists('db', $GLOBALS);
$previousDb = $hadDb ? $GLOBALS['db'] : null;
$GLOBALS['db'] = $dbStub;
try {
$orders = (new orders_o())->getOrdersWithRegistrationNumberInDateRange(
' ec21235 ',
'2025-03-01 00:00:00',
'2025-04-30 23:59:59'
);
expect($orders)->toBe([]);
expect($dbStub->queryCalls)->toBeGreaterThan(0);
expect($dbStub->lastQuery)->toContain("UPPER(TRIM(reg_1)) = 'EC21235'");
expect($dbStub->lastQuery)->toContain("UPPER(TRIM(reg_2)) = 'EC21235'");
expect($dbStub->lastQuery)->toContain("UPPER(TRIM(reg_3)) = 'EC21235'");
expect($dbStub->lastQuery)->toContain("created_at BETWEEN '2025-03-01 00:00:00' AND '2025-04-30 23:59:59'");
expect($dbStub->lastQuery)->toContain('AND deleted_at IS NULL');
expect($dbStub->lastQuery)->not->toContain('SELECT id, created_at');
} finally {
if ($hadDb) {
$GLOBALS['db'] = $previousDb;
} else {
unset($GLOBALS['db']);
}
}
});
it('rejects an inverted date range for registration lookups', function (): void {
$dbStub = new OrdersRegistrationDateRangeDbStub();
$hadDb = array_key_exists('db', $GLOBALS);
$previousDb = $hadDb ? $GLOBALS['db'] : null;
$GLOBALS['db'] = $dbStub;
try {
(new orders_o())->getOrdersWithRegistrationNumberInDateRange(
'EC21235',
'2025-04-30 23:59:59',
'2025-03-01 00:00:00'
);
} finally {
if ($hadDb) {
$GLOBALS['db'] = $previousDb;
} else {
unset($GLOBALS['db']);
}
}
})->throws(\Exception::class, 'The start date cannot be after the end date');
it('returns early when registration number is blank', function (): void {
$dbStub = new OrdersRegistrationDateRangeDbStub();
$hadDb = array_key_exists('db', $GLOBALS);
$previousDb = $hadDb ? $GLOBALS['db'] : null;
$GLOBALS['db'] = $dbStub;
try {
$orders = (new orders_o())->getOrdersWithRegistrationNumberInDateRange(
' ',
'2025-03-01 00:00:00',
'2025-04-30 23:59:59'
);
expect($orders)->toBe([]);
expect($dbStub->queryCalls)->toBeGreaterThanOrEqual(0);
} finally {
if ($hadDb) {
$GLOBALS['db'] = $previousDb;
} else {
unset($GLOBALS['db']);
}
}
});
it('orders registration-matched orders by id ASC so invoice-collection reassignment is deterministic', function (): void {
$dbStub = new OrdersRegistrationDateRangeDbStub();
$hadDb = array_key_exists('db', $GLOBALS);
$previousDb = $hadDb ? $GLOBALS['db'] : null;
$GLOBALS['db'] = $dbStub;
try {
(new orders_o())->getOrdersWithRegistrationNumberInDateRange(
'EC21235',
'2025-03-01 00:00:00',
'2025-04-30 23:59:59'
);
expect($dbStub->lastQuery)->toContain('ORDER BY id ASC');
} finally {
if ($hadDb) {
$GLOBALS['db'] = $previousDb;
} else {
unset($GLOBALS['db']);
}
}
});