From 11071e6251be9000e2a9f5e42ccbe9dd6e2e62f1 Mon Sep 17 00:00:00 2001 From: Imugiii Date: Mon, 22 Jun 2026 06:45:59 +0000 Subject: [PATCH] feat(api): suivi public du statut commande GET /api/orders/{number} (P4) --- docs/api/conventions.md | 17 ++++++----- src/app/Controllers/OrderController.php | 19 ++++++++++++ src/app/Order/OrderRepository.php | 29 ++++++++++++++++++ src/public/admin/index.php | 2 ++ tests/Unit/Order/OrderControllerTest.php | 39 ++++++++++++++++++++++++ 5 files changed, 99 insertions(+), 7 deletions(-) diff --git a/docs/api/conventions.md b/docs/api/conventions.md index e7c1210..3f0ad61 100644 --- a/docs/api/conventions.md +++ b/docs/api/conventions.md @@ -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) | diff --git a/src/app/Controllers/OrderController.php b/src/app/Controllers/OrderController.php index 8308338..af214e8 100644 --- a/src/app/Controllers/OrderController.php +++ b/src/app/Controllers/OrderController.php @@ -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 $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. diff --git a/src/app/Order/OrderRepository.php b/src/app/Order/OrderRepository.php index a556b11..1e6d602 100644 --- a/src/app/Order/OrderRepository.php +++ b/src/app/Order/OrderRepository.php @@ -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. * diff --git a/src/public/admin/index.php b/src/public/admin/index.php index 4ae17bb..605e95e 100644 --- a/src/public/admin/index.php +++ b/src/public/admin/index.php @@ -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 diff --git a/tests/Unit/Order/OrderControllerTest.php b/tests/Unit/Order/OrderControllerTest.php index e1a9a9d..42723af 100644 --- a/tests/Unit/Order/OrderControllerTest.php +++ b/tests/Unit/Order/OrderControllerTest.php @@ -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()); + } }