From 4f6ca5d0ceb2a3b04a57633bc129668a68d87005 Mon Sep 17 00:00:00 2001 From: Imugiii Date: Thu, 25 Jun 2026 09:03:21 +0000 Subject: [PATCH] feat(catalogue): administration CRUD des variantes (taille/Maxi) + selects menu base-only + garde serveur --- src/app/Catalogue/MenuRepository.php | 17 ++ src/app/Catalogue/ProductRepository.php | 83 +++++++-- src/app/Controllers/MenuController.php | 31 +++- src/app/Controllers/ProductController.php | 124 +++++++++++--- src/app/Views/admin/products/form.php | 47 ++++++ src/app/Views/admin/products/index.php | 15 +- tests/Integration/CatalogueReadDbTest.php | 5 +- tests/Integration/ProductIngredientDbTest.php | 3 + tests/Integration/ProductRepositoryDbTest.php | 6 + tests/Support/FakeCatalogueDatabase.php | 26 +++ tests/Support/FakeDatabase.php | 28 ++++ tests/Unit/Admin/MenuControllerTest.php | 27 +++ tests/Unit/Admin/ProductControllerTest.php | 158 ++++++++++++++++++ .../ProductRepositoryBaseOnlyTest.php | 62 +++++++ 14 files changed, 590 insertions(+), 42 deletions(-) create mode 100644 tests/Unit/Catalogue/ProductRepositoryBaseOnlyTest.php diff --git a/src/app/Catalogue/MenuRepository.php b/src/app/Catalogue/MenuRepository.php index 83c0b38..16754c6 100644 --- a/src/app/Catalogue/MenuRepository.php +++ b/src/app/Catalogue/MenuRepository.php @@ -147,6 +147,23 @@ final class MenuRepository return $this->db->fetch('SELECT id FROM product WHERE id = :id', ['id' => $id]) !== null; } + /** + * Le produit existe-t-il ET est-il un produit de BASE (base_product_id IS NULL, + * R4) ? Garde serveur de l'eligibilite au menu (F9-2) : un menu ne peut prendre + * comme burger principal NI comme option de slot une VARIANTE de taille (ex. + * "Coca Cola 50cl"), qui n'est pas un produit autonome. Predicat plus strict que + * productExists() : il rejette une variante meme si l'UI est contournee. Le + * formulaire menu n'expose deja que des bases (ProductRepository::basesOnly), + * cette garde verrouille le chemin serveur en plus. + */ + public function productIsBase(int $id): bool + { + return $this->db->fetch( + 'SELECT id FROM product WHERE id = :id AND base_product_id IS NULL', + ['id' => $id], + ) !== null; + } + /** * Pre-verification FK-safe (mlt 8.6 RG-1) : le menu est-il reference par une * ligne de commande historique ? La FK order_item.menu_id est RESTRICT. diff --git a/src/app/Catalogue/ProductRepository.php b/src/app/Catalogue/ProductRepository.php index 0029b1b..570b855 100644 --- a/src/app/Catalogue/ProductRepository.php +++ b/src/app/Catalogue/ProductRepository.php @@ -28,7 +28,16 @@ final class ProductRepository } /** - * Liste pour le back-office, avec le libelle de categorie. + * Liste pour le back-office, avec le libelle de categorie et, pour une VARIANTE + * de taille (base_product_id non nul, R4), le nom de sa base. La liste admin + * affiche AINSI toutes les lignes produit -- bases ET variantes -- mais marque + * chaque variante "Variante de X" : l'admin la voit, comprend qu'elle n'est pas + * un produit autonome, et peut la delier/relier via le formulaire. La projection + * remonte base_product_id pour que la vue distingue les deux. + * + * Cette methode N'ALIMENTE PLUS les selects du formulaire menu (qui doivent etre + * base-only, R4/F9-1) : ceux-ci passent par basesOnly(). all() peut donc porter + * le LEFT JOIN d'enrichissement sans fausser une liste deroulante. * * @return array> */ @@ -36,12 +45,34 @@ final class ProductRepository { return $this->db->fetchAll( 'SELECT p.id, p.category_id, p.name, p.price_cents, p.vat_rate, p.is_available, ' - . 'p.display_order, c.name AS category_name ' + . 'p.display_order, p.size_cl, p.base_product_id, c.name AS category_name, ' + . 'b.name AS base_name ' . 'FROM product p JOIN category c ON c.id = p.category_id ' + . 'LEFT JOIN product b ON b.id = p.base_product_id ' . 'ORDER BY p.display_order, p.name', ); } + /** + * Produits de BASE uniquement (base_product_id IS NULL, R4), pour alimenter les + * listes deroulantes du formulaire menu (burger principal + options de slot, + * F9-1) et le select base_product_id du formulaire produit. Une VARIANTE de + * taille (ex. "Coca Cola 50cl") n'est jamais un produit autonome : la proposer + * comme burger/option/base ferait apparaitre la variante comme un produit a part + * entiere. Le predicat anti-variante vit ici (cote requete), miroir de la garde + * serveur MenuRepository::productIsBase(). Projection minimale {id, name} : seules + * colonnes utiles a un