corentin_wakdo/tests/Integration/OrderQueryRepositoryDbTest.php
Corentin JOGUET 89488b20b2
All checks were successful
CI / secret-scan (push) Successful in 16s
CI / php-lint (push) Successful in 30s
CI / static-tests (push) Successful in 1m15s
CI / js-tests (push) Successful in 37s
feat(back-office): KDS cuisine - detail des commandes + bande SLA (#108)
2026-06-25 10:28:36 +02:00

165 lines
6.8 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Integration;
use PHPUnit\Framework\TestCase;
use Throwable;
use App\Core\Config;
use App\Core\Database;
use App\Order\OrderQueryRepository;
/**
* OrderQueryRepository (read-side admin) contre une vraie MariaDB (schema migre).
* Auto-skip si WAKDO_DB_TESTS != 1. Insere des commandes connues (order_number
* prefixe IT-<suffix>) et verifie les KPIs de vente (delta vs baseline) + la liste
* recente. Nettoyage par prefixe en tearDown.
*/
final class OrderQueryRepositoryDbTest extends TestCase
{
private Database $db;
private string $suffix = '';
protected function setUp(): void
{
if (getenv('WAKDO_DB_TESTS') !== '1') {
self::markTestSkipped('Tests DB desactives (definir WAKDO_DB_TESTS=1 + DB_*).');
}
$this->db = new Database(new Config());
try {
$this->db->fetch('SELECT 1');
} catch (Throwable $exception) {
self::markTestSkipped('Base injoignable: ' . $exception->getMessage());
}
$this->suffix = bin2hex(random_bytes(4));
}
protected function tearDown(): void
{
if ($this->suffix === '') {
return;
}
$this->db->execute(
'DELETE FROM customer_order WHERE order_number LIKE :p',
['p' => 'IT-' . $this->suffix . '%'],
);
}
private function insertOrder(string $number, string $status, int $ttc): void
{
$ht = (int) round($ttc / 1.1);
$this->db->execute(
"INSERT INTO customer_order (order_number, idempotency_key, source, service_mode, status, "
. 'total_ht_cents, total_vat_cents, total_ttc_cents) '
. "VALUES (:num, :key, 'kiosk', 'takeaway', :st, :ht, :vat, :ttc)",
['num' => $number, 'key' => $number . '-k', 'st' => $status, 'ht' => $ht, 'vat' => $ttc - $ht, 'ttc' => $ttc],
);
}
public function testSalesKpisCountsPaidRevenueOnly(): void
{
$repo = new OrderQueryRepository($this->db);
$before = $repo->salesKpis();
$this->insertOrder('IT-' . $this->suffix . '-P', 'paid', 1000);
$this->insertOrder('IT-' . $this->suffix . '-N', 'pending_payment', 500);
$after = $repo->salesKpis();
// Le CA ne compte QUE le paid (pas le pending_payment).
self::assertSame($before['revenue_cents'] + 1000, $after['revenue_cents']);
self::assertSame($before['paid_count'] + 1, $after['paid_count']);
self::assertSame($before['total_orders'] + 2, $after['total_orders']);
self::assertGreaterThanOrEqual(1, $after['by_status']['paid'] ?? 0);
self::assertGreaterThanOrEqual(1, $after['by_status']['pending_payment'] ?? 0);
self::assertSame(intdiv($after['revenue_cents'], max(1, $after['paid_count'])), $after['avg_basket_cents']);
}
public function testRecentListsInsertedOrdersWithExpectedColumns(): void
{
$repo = new OrderQueryRepository($this->db);
$num = 'IT-' . $this->suffix . '-P';
$this->insertOrder($num, 'paid', 1000);
$recent = $repo->recent(200);
$numbers = array_column($recent, 'order_number');
self::assertContains($num, $numbers, 'la commande inseree doit apparaitre dans recent()');
$row = $recent[(int) array_search($num, $numbers, true)];
self::assertSame('paid', (string) $row['status']);
self::assertSame(1000, (int) $row['total_ttc_cents']);
self::assertSame('takeaway', (string) $row['service_mode']);
self::assertArrayHasKey('created_at', $row);
}
public function testRecentRespectsLimit(): void
{
$repo = new OrderQueryRepository($this->db);
self::assertLessThanOrEqual(3, count($repo->recent(3)));
}
/**
* paidQueueWithDetail (LIST_ORDERS_DISPLAY) contre le schema reel : insere une
* commande `paid` avec une ligne produit + un modificateur, et verifie que la file
* porte l'article (label_snapshot, format) et le modificateur (ingredient_name via
* la jointure ingredient + action). Les FK sont resolues par nom (convention des
* seeds : produit 'Le 280', ingredient 'Oignon'). Auto-skip si seeds absents.
*/
public function testPaidQueueWithDetailReturnsItemsAndModifiers(): void
{
$product = $this->db->fetch("SELECT id FROM product WHERE name = 'Le 280'");
$ingredient = $this->db->fetch("SELECT id FROM ingredient WHERE name = 'Oignon'");
if ($product === null || $ingredient === null) {
self::markTestSkipped('Seeds catalogue/ingredients absents (produit/ingredient introuvable).');
}
$num = 'IT-' . $this->suffix . '-KDS';
$this->insertOrder($num, 'paid', 1090);
// paid_at explicite : la file trie sur paid_at, et la bande SLA en derive.
$orderId = $this->orderIdByNumber($num);
$this->db->execute('UPDATE customer_order SET paid_at = NOW() WHERE id = :id', ['id' => $orderId]);
$this->db->execute(
'INSERT INTO order_item (order_id, item_type, product_id, format, label_snapshot, '
. 'unit_price_cents_snapshot, vat_rate_snapshot, quantity) '
. "VALUES (:oid, 'product', :pid, 'normal', 'Le 280', 1090, 100, 1)",
['oid' => $orderId, 'pid' => (int) $product['id']],
);
$itemId = (int) ($this->db->fetch('SELECT LAST_INSERT_ID() AS id')['id'] ?? 0);
$this->db->execute(
'INSERT INTO order_item_modifier (order_item_id, ingredient_id, action, extra_price_cents) '
. "VALUES (:iid, :ing, 'remove', 0)",
['iid' => $itemId, 'ing' => (int) $ingredient['id']],
);
$queue = (new OrderQueryRepository($this->db))->paidQueueWithDetail(['kiosk', 'counter', 'drive']);
$mine = array_values(array_filter(
$queue,
static fn (array $o): bool => ($o['order_number'] ?? '') === $num,
));
self::assertCount(1, $mine, 'la commande inseree doit apparaitre dans la file KDS');
$order = $mine[0];
self::assertArrayNotHasKey('id', $order, 'l\'id technique ne doit pas etre expose');
self::assertContains($order['sla_band'], ['fresh', 'warn', 'late']);
self::assertCount(1, $order['items']);
$item = $order['items'][0];
self::assertSame('Le 280', (string) $item['label_snapshot']);
self::assertSame('normal', (string) $item['format']);
self::assertCount(1, $item['modifiers']);
self::assertSame('remove', (string) $item['modifiers'][0]['action']);
self::assertSame('Oignon', (string) $item['modifiers'][0]['ingredient_name']);
}
private function orderIdByNumber(string $number): int
{
return (int) ($this->db->fetch(
'SELECT id FROM customer_order WHERE order_number = :n',
['n' => $number],
)['id'] ?? 0);
}
}