corentin_wakdo/tests/Support/FakeOrderDatabase.php
Imugiii f669c32c25
All checks were successful
CI / secret-scan (push) Successful in 12s
CI / php-lint (push) Successful in 28s
CI / static-tests (push) Successful in 1m0s
CI / js-tests (push) Successful in 35s
CI / secret-scan (pull_request) Successful in 13s
CI / php-lint (pull_request) Successful in 33s
CI / static-tests (pull_request) Successful in 59s
CI / js-tests (pull_request) Successful in 27s
CI / auto-merge (push) Has been skipped
CI / auto-merge (pull_request) Successful in 7s
feat(api): P4 chunk 1b - encaissement commande + decrement stock
OrderController (POST /api/orders + /api/orders/{number}/pay) cable sur
OrderRepository::pay : transition gardee pending_payment -> paid et
decrement de stock atomique (RG-5 etapes 5-6, RG-T11/RG-T20) dans une
transaction. Idempotent (deja paid -> renvoi sans re-decrement ; course
concurrente perdue -> sortie idempotente) ; statut terminal -> 409.

Decrement agrege par ingredient (un UPDATE auto-verrouillant + une ligne
stock_movement(sale) par ingredient, ordre de verrou stable par id) ;
modificateurs appliques (remove => 0, add => portion supplementaire) ;
menu = recette du burger + recettes des selections, au format de la ligne.

Inerte tant que les recettes (product_ingredient) ne sont pas seedees.

Routes /api/orders anonymes (kiosk, pas de session). Double de test dedie
extrait en tests/Support/FakeOrderDatabase. PHPUnit 290 + PHPStan L6 verts.
2026-06-18 12:20:20 +00:00

157 lines
5.3 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Support;
use App\Core\DatabaseInterface;
/**
* Double DatabaseInterface dedie au domaine Commande (P4). Le double generique
* FakeDatabase repond par boutons fixes (un seul produit/menu) ; une commande
* mele plusieurs produits/menus distincts, d'ou ce double indexe par id.
*
* Couvre createPending (catalogue + idempotence) ET pay (lecture de la commande
* persistee + recettes -> decrement). Les ecritures sont tracees pour assertion ;
* payUpdateAffected simule l'issue de la transition gardee (0 = course perdue).
*/
final class FakeOrderDatabase implements DatabaseInterface
{
/** @var list<array{sql:string, params:array<string,mixed>}> */
public array $writes = [];
/** @var array<int, array<string,mixed>> produits indexes par id (find). */
public array $products = [];
/** @var array<int, array<string,mixed>> menus indexes par id (find). */
public array $menus = [];
/** @var array<int, list<array<string,mixed>>> slots (slotsWithOptions) par menu id. */
public array $slotRows = [];
/** @var array<int, list<array<string,mixed>>> recettes (composition) par produit id. */
public array $compositions = [];
/** Commande existante renvoyee par la recherche idempotency_key ; null = aucune. */
/** @var array<string,mixed>|null */
public ?array $existingByKey = null;
/** Commande renvoyee par la recherche order_number (pay) ; null = introuvable. */
/** @var array<string,mixed>|null */
public ?array $orderByNumber = null;
/** Statut relu apres une transition gardee a 0 ligne (course concurrente). */
public string $recheckStatus = 'paid';
/** Lignes order_item renvoyees pour la commande encaissee. */
/** @var list<array<string,mixed>> */
public array $orderItems = [];
/** Selections (product_id) par order_item id. */
/** @var array<int, list<array<string,mixed>>> */
public array $selectionsByItem = [];
/** Modificateurs (ingredient_id, action) par order_item id. */
/** @var array<int, list<array<string,mixed>>> */
public array $modifiersByItem = [];
/** Lignes affectees par l'UPDATE de transition pending_payment -> paid. */
public int $payUpdateAffected = 1;
private int $autoId = 99;
public function fetch(string $sql, array $params = []): ?array
{
if (str_contains($sql, 'LAST_INSERT_ID')) {
return ['id' => $this->autoId];
}
if (str_contains($sql, 'FROM customer_order WHERE idempotency_key')) {
return $this->existingByKey;
}
if (str_contains($sql, 'FROM customer_order WHERE order_number')) {
return $this->orderByNumber;
}
if (str_contains($sql, 'SELECT status FROM customer_order WHERE id')) {
return ['status' => $this->recheckStatus];
}
if (str_contains($sql, 'FROM product WHERE id = :id')) {
return $this->products[(int) $params['id']] ?? null;
}
if (str_contains($sql, 'FROM menu WHERE id = :id')) {
return $this->menus[(int) $params['id']] ?? null;
}
return null;
}
public function fetchAll(string $sql, array $params = []): array
{
if (str_contains($sql, 'FROM menu_slot s')) {
return $this->slotRows[(int) $params['id']] ?? [];
}
if (str_contains($sql, 'FROM product_ingredient pi')) {
return $this->compositions[(int) $params['id']] ?? [];
}
if (str_contains($sql, 'FROM order_item WHERE order_id')) {
return $this->orderItems;
}
if (str_contains($sql, 'FROM order_item_selection WHERE order_item_id')) {
return $this->selectionsByItem[(int) $params['oiid']] ?? [];
}
if (str_contains($sql, 'FROM order_item_modifier WHERE order_item_id')) {
return $this->modifiersByItem[(int) $params['oiid']] ?? [];
}
return [];
}
public function execute(string $sql, array $params = []): int
{
$this->writes[] = ['sql' => $sql, 'params' => $params];
if (str_contains($sql, 'INSERT INTO customer_order') || str_contains($sql, 'INSERT INTO order_item ')) {
$this->autoId++;
}
if (str_contains($sql, 'UPDATE customer_order SET status')) {
return $this->payUpdateAffected;
}
return 1;
}
public function transaction(callable $fn): void
{
$fn($this);
}
/** @return array<string,mixed> */
public function firstWrite(string $needle): array
{
foreach ($this->writes as $write) {
if (str_contains($write['sql'], $needle)) {
return $write['params'];
}
}
return [];
}
public function countWrites(string $needle): int
{
return count(array_filter($this->writes, static fn (array $w): bool => str_contains($w['sql'], $needle)));
}
/**
* Parametres de toutes les ecritures dont le SQL contient $needle (ordre d'insertion).
*
* @return list<array<string,mixed>>
*/
public function allWrites(string $needle): array
{
$out = [];
foreach ($this->writes as $write) {
if (str_contains($write['sql'], $needle)) {
$out[] = $write['params'];
}
}
return $out;
}
}