fix(api): order order_items so primary precedes addons in getOrderItems (#364)
Defensive ORDER BY in orders_o.php::getOrderItems so primary items render before their addons (related_item_id IS NULL DESC, related_item_id ASC, id ASC). Pinned with OrderItemsListingOrderingTest which locates orders_o.php via worktree-aware resolver.
This commit is contained in:
@@ -619,7 +619,8 @@ class orders_o extends db
|
||||
// their parent (related_item_id ASC), and finally fall back to insertion order
|
||||
// (id ASC). Without an explicit ORDER BY, MySQL is free to return rows in any
|
||||
// order, which causes the FE tree-builder to render addons before their
|
||||
// primary on the invoice and POS displays.
|
||||
// primary on the invoice and POS displays (Trækker + addons like Trailer/Dolly
|
||||
// visually appearing as if only Trailer/Dolly were attached to the order).
|
||||
$sql = "SELECT * FROM order_items WHERE order_id = $order_id ORDER BY (related_item_id IS NULL) DESC, related_item_id ASC, id ASC";
|
||||
$result = $db->query($sql);
|
||||
$order_items = [];
|
||||
|
||||
@@ -2,8 +2,51 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
it('orders the SELECT in getOrderItems so primary items precede their addons', function (): void {
|
||||
$ordersObjectFile = app_path('objects/orders_o.php');
|
||||
// Locate the worktree's `orders_o.php`. The default `app_path()` helper resolves
|
||||
// symlinks and points at the primary checkout's source, which we are forbidden to
|
||||
// mutate. We prefer (in order):
|
||||
// 1. The TRUCKWASH_WORKTREE_ROOT env var when set and valid
|
||||
// 2. Walking up from this test file's directory to a project root that owns
|
||||
// the `services/nginx/app/objects/orders_o.php` file
|
||||
// 3. Falling back to the primary checkout's path (only when neither of the
|
||||
// above resolves; this is the production CI path)
|
||||
$ordersObjectFileResolver = static function (): string {
|
||||
$candidate = getenv('TRUCKWASH_WORKTREE_ROOT');
|
||||
if (is_string($candidate) && $candidate !== '' && is_dir($candidate)) {
|
||||
$path = $candidate . '/services/nginx/app/objects/orders_o.php';
|
||||
if (is_file($path)) {
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
|
||||
// Walk up from this test file looking for the orders_o.php in the same
|
||||
// services/nginx/app tree. The test lives in
|
||||
// services/nginx/app/tests/Unit/Orders/, so the target lives 4 levels up
|
||||
// from this file's directory. We still walk defensively so the test works
|
||||
// even if the test is moved into a deeper or shallower location.
|
||||
$directory = __DIR__;
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
$candidatePath = $directory . '/objects/orders_o.php';
|
||||
if (is_file($candidatePath)) {
|
||||
return $candidatePath;
|
||||
}
|
||||
$parent = dirname($directory);
|
||||
if ($parent === $directory) {
|
||||
break;
|
||||
}
|
||||
$directory = $parent;
|
||||
}
|
||||
|
||||
$existing = app_path('objects/orders_o.php');
|
||||
if (is_file($existing)) {
|
||||
return $existing;
|
||||
}
|
||||
|
||||
throw new RuntimeException('Unable to locate orders_o.php for the addon ordering test.');
|
||||
};
|
||||
|
||||
it('orders the SELECT in getOrderItems so primary items precede their addons', function () use ($ordersObjectFileResolver): void {
|
||||
$ordersObjectFile = $ordersObjectFileResolver();
|
||||
$content = file_get_contents($ordersObjectFile);
|
||||
|
||||
expect($content)->not->toBeFalse();
|
||||
@@ -39,8 +82,8 @@ it('orders the SELECT in getOrderItems so primary items precede their addons', f
|
||||
->toContain('id ASC');
|
||||
});
|
||||
|
||||
it('does not leave the legacy unordered SELECT in getOrderItems', function (): void {
|
||||
$ordersObjectFile = app_path('objects/orders_o.php');
|
||||
it('does not leave the legacy unordered SELECT in getOrderItems', function () use ($ordersObjectFileResolver): void {
|
||||
$ordersObjectFile = $ordersObjectFileResolver();
|
||||
$content = file_get_contents($ordersObjectFile);
|
||||
|
||||
expect($content)->not->toBeFalse();
|
||||
@@ -54,7 +97,8 @@ it('does not leave the legacy unordered SELECT in getOrderItems', function (): v
|
||||
$methodBody = substr($content, (int)$start, (int)$end - (int)$start);
|
||||
|
||||
// The buggy SQL must be gone: previously this returned rows in whatever
|
||||
// order MySQL felt like, leading to addons being listed before their primary.
|
||||
// order MySQL felt like, leading to addons being listed before their primary
|
||||
// and the operator seeing "only Trailer and Dolly" attached to a Trækker order.
|
||||
expect($methodBody)
|
||||
->not->toContain("FROM order_items WHERE order_id = \$order_id\";\n \$result");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user