## 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>
152 lines
4.4 KiB
PHP
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']);
|
|
}
|
|
}
|
|
});
|