not->toBeFalse(); // Locate the getOrderItems method body. $start = strpos($content, 'public function getOrderItems(int $order_id): array'); expect($start)->not->toBeFalse(); // Bound the search so we don't accidentally match unrelated SQL further down // the file (the helper around line 645 in orders_o.php also uses ORDER BY id DESC). $end = strpos($content, "\n }\n", $start); expect($end)->not->toBeFalse(); $methodBody = substr($content, (int)$start, (int)$end - (int)$start); // The SELECT against order_items must include an explicit ORDER BY so MySQL // does not return rows in undefined order (which has been observed to put // primary order items after their addons, breaking the FE tree-builder and // the invoice line listing). expect($methodBody) ->toContain("FROM order_items WHERE order_id = \$order_id") ->and($methodBody) ->toContain('ORDER BY'); // The ORDER BY must place primary items first (related_item_id IS NULL DESC), // group addons by their parent (related_item_id ASC), and fall back to // insertion order (id ASC). expect($methodBody) ->toContain('(related_item_id IS NULL) DESC') ->and($methodBody) ->toContain('related_item_id ASC') ->and($methodBody) ->toContain('id ASC'); }); it('does not leave the legacy unordered SELECT in getOrderItems', function (): void { $ordersObjectFile = app_path('objects/orders_o.php'); $content = file_get_contents($ordersObjectFile); expect($content)->not->toBeFalse(); $start = strpos($content, 'public function getOrderItems(int $order_id): array'); $end = strpos($content, "\n }\n", $start); expect($start)->not->toBeFalse() ->and($end)->not->toBeFalse(); $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. expect($methodBody) ->not->toContain("FROM order_items WHERE order_id = \$order_id\";\n \$result"); });