feat(api): suivi public du statut commande GET /api/orders/{number} (P4) #77

Merged
Corentin merged 1 commit from feat/p2-order-status-lookup into dev 2026-06-22 08:53:37 +02:00
5 changed files with 99 additions and 7 deletions
Showing only changes of commit 11071e6251 - Show all commits

View file

@ -113,12 +113,15 @@ La borne est publique (aucune session) ; cf. `mlt.md` CREATE_ORDER, declencheur
| Methode | Chemin | Permission | Op MCT | Statut |
|---|---|---|---|---|
| GET | `/api/categories` | (lecture publique) | READ_CATALOGUE | prevu |
| GET | `/api/products` | (lecture publique) | READ_CATALOGUE | prevu |
| GET | `/api/products/{id}` | (lecture publique) | READ_CATALOGUE | prevu |
| GET | `/api/menus` | (lecture publique) | READ_CATALOGUE | prevu |
| GET | `/api/menus/{id}` | (lecture publique) | READ_CATALOGUE | prevu |
| POST | `/api/orders` | (kiosk public) | CREATE_ORDER (mlt 3.3) | prevu (idempotency_key, RG-T19) |
| GET | `/api/categories` | (lecture publique) | READ_CATALOGUE | livre |
| GET | `/api/products` | (lecture publique) | READ_CATALOGUE | livre |
| GET | `/api/products/{id}` | (lecture publique) | READ_CATALOGUE | livre |
| GET | `/api/menus` | (lecture publique) | READ_CATALOGUE | livre |
| GET | `/api/menus/{id}` | (lecture publique) | READ_CATALOGUE | livre (slots de composition) |
| GET | `/api/allergens` | (lecture publique) | READ_CATALOGUE | livre (14 allergenes INCO) |
| POST | `/api/orders` | (kiosk public) | CREATE_ORDER (mlt 3.3) | livre (idempotency_key, RG-T19) |
| POST | `/api/orders/{number}/pay` | (kiosk public) | (encaissement) | livre (paid + decrement stock RG-T20) |
| GET | `/api/orders/{number}` | (lecture publique) | (suivi statut) | livre (champs non sensibles : numero, statut, total) |
### 5.3 API / pages back-office (prevu P3-P4, session + permission)
@ -132,7 +135,7 @@ Commandes (cote equipier) :
| Methode | Chemin | Permission | Op MCT | Note |
|---|---|---|---|---|
| GET | `/api/orders` | `order.read` | READ_ORDERS | filtre par `role_visible_source` (RG-T12) |
| GET | `/api/orders/{number}` | `order.read` | READ_ORDERS | |
| GET | `/api/orders/{number}` | `order.read` | READ_ORDERS | vue back-office detaillee (differe) ; le suivi public minimal est livre en 5.2 |
| POST | `/api/orders` (comptoir/drive) | `order.create` | CREATE_COUNTER_ORDER (mlt 4.1) | source auto-taggee |
| POST | `/api/orders/{id}/deliver` | `order.deliver` | DELIVER_ORDER (mlt 6.1) | |
| POST | `/api/orders/{id}/cancel` | `order.cancel` | CANCEL_ORDER (mlt 7.1) | PIN + audit_log (RG-T13/14) |

View file

@ -57,6 +57,25 @@ class OrderController extends Controller
return $this->json(['data' => $this->present($order)]);
}
/**
* Lecture publique du statut d'une commande par son numero (suivi borne apres
* encaissement). Anonyme, lecture seule ; 404 si le numero est inconnu.
*
* @param array<string, string> $params
*/
public function show(array $params = []): Response
{
$order = $this->orders()->findByNumber((string) ($params['number'] ?? ''));
if ($order === null) {
return $this->json(
['data' => null, 'error' => ['code' => 'ORDER_NOT_FOUND', 'message' => $this->messageFor('ORDER_NOT_FOUND')]],
404,
);
}
return $this->json(['data' => $this->present($order)]);
}
/**
* Fabrique le repository de commande sur l'acces BDD courant. Hook de test
* (sous-classe -> double) : redefinir db() suffit a injecter une base factice.

View file

@ -59,6 +59,35 @@ class OrderRepository
];
}
/**
* Recherche une commande par son numero (prefixe canal K/C/D + id). Lecture
* publique du statut cote borne (suivi apres encaissement). Renvoie null si le
* numero est inconnu. Lecture seule : ne sert que des champs non sensibles
* (la commande kiosk est anonyme, pas de PII).
*
* @return array{id:int, order_number:string, total_ttc_cents:int, status:string}|null
*/
public function findByNumber(string $number): ?array
{
if ($number === '') {
return null;
}
$row = $this->db->fetch(
'SELECT id, order_number, total_ttc_cents, status FROM customer_order WHERE order_number = :n',
['n' => $number],
);
if ($row === null) {
return null;
}
return [
'id' => (int) $row['id'],
'order_number' => (string) $row['order_number'],
'total_ttc_cents' => (int) $row['total_ttc_cents'],
'status' => (string) $row['status'],
];
}
/**
* Cree une commande borne en pending_payment. Idempotent sur idempotency_key.
*

View file

@ -88,6 +88,8 @@ try {
// un seul segment (numero K+id), pas de collision avec un sous-chemin.
$router->add('POST', '/api/orders', [OrderController::class, 'create']);
$router->add('POST', '/api/orders/{number}/pay', [OrderController::class, 'pay']);
// Suivi public du statut d'une commande par son numero (lecture seule, anonyme).
$router->add('GET', '/api/orders/{number}', [OrderController::class, 'show']);
// Lecture catalogue borne (P4, docs/api/conventions.md section 5.2). API publique
// kiosk, ANONYME : la borne consulte sans session. Lecture seule ; ne sert que le

View file

@ -132,4 +132,43 @@ final class OrderControllerTest extends TestCase
self::assertIsArray($data);
self::assertSame('INVALID_TRANSITION', $data['error']['code'] ?? null);
}
public function testShowReturnsOrderStatus(): void
{
$db = new FakeOrderDatabase();
$db->orderByNumber = ['id' => 100, 'order_number' => 'K100', 'total_ttc_cents' => 890, 'status' => 'paid'];
$response = $this->controller($db, '', '/api/orders/K100')->show(['number' => 'K100']);
self::assertSame(200, $response->status());
$data = json_decode($response->body(), true);
self::assertIsArray($data);
self::assertSame('K100', $data['data']['order_number'] ?? null);
self::assertSame('paid', $data['data']['status'] ?? null);
self::assertSame(890, $data['data']['total_ttc_cents'] ?? null);
}
public function testShowUnknownReturns404(): void
{
$db = new FakeOrderDatabase();
$db->orderByNumber = null;
$response = $this->controller($db, '', '/api/orders/K404')->show(['number' => 'K404']);
self::assertSame(404, $response->status());
$data = json_decode($response->body(), true);
self::assertIsArray($data);
self::assertSame('ORDER_NOT_FOUND', $data['error']['code'] ?? null);
}
public function testShowEmptyNumberReturns404(): void
{
$db = new FakeOrderDatabase();
$db->orderByNumber = ['id' => 1, 'order_number' => 'K1', 'total_ttc_cents' => 100, 'status' => 'paid'];
// Numero vide : court-circuite avant toute lecture BDD (findByNumber renvoie null).
$response = $this->controller($db, '', '/api/orders/')->show(['number' => '']);
self::assertSame(404, $response->status());
}
}