feat(borne): menu Maxi agrandit la boisson en 50cl + transport explicite du format
All checks were successful
CI / secret-scan (push) Successful in 18s
CI / js-tests (pull_request) Successful in 38s
CI / php-lint (push) Successful in 42s
CI / static-tests (push) Successful in 1m29s
CI / js-tests (push) Successful in 45s
CI / secret-scan (pull_request) Successful in 17s
CI / php-lint (pull_request) Successful in 40s
CI / static-tests (pull_request) Successful in 1m10s
All checks were successful
CI / secret-scan (push) Successful in 18s
CI / js-tests (pull_request) Successful in 38s
CI / php-lint (push) Successful in 42s
CI / static-tests (push) Successful in 1m29s
CI / js-tests (push) Successful in 45s
CI / secret-scan (pull_request) Successful in 17s
CI / php-lint (pull_request) Successful in 40s
CI / static-tests (pull_request) Successful in 1m10s
Seed 0006 lie chaque soda fontaine 30cl a sa variante 50cl via maxi_variant_product_id : en menu Maxi, resolveSelections substitue la boisson vers la 50cl (meme mecanique que l'accompagnement Grande Frite), sans code serveur. Les boissons en bouteille (sans variante) restent en taille standard, le surcout Maxi etant porte par le menu. La borne transporte desormais le format Normal/Maxi explicitement (buildMenuCartItem) au lieu de l'inferer de supplement_cents>0 (faux negatif si maxi==normal) ; checkout.js lit cartItem.format avec repli historique pour les paniers serialises. Commentaire migration 0007 corrige (la substitution Maxi de la boisson est desormais voulue). Tests : OrderRepositoryTest (boisson Maxi -> 50cl + bouteille inchangee), checkout/composer-slots (format transporte). Seed valide idempotent sur base jetable (5 sodas lies, frites intactes, bouteilles NULL).
This commit is contained in:
parent
8e2e0382ba
commit
b3521f7a56
7 changed files with 141 additions and 7 deletions
|
|
@ -8,10 +8,15 @@
|
|||
-- domaine commande facture deja par product_id : le flux de commande
|
||||
-- reste inchange, la borne resout juste la taille choisie en product_id.
|
||||
--
|
||||
-- Grouping DEDIE, distinct de maxi_variant_product_id (migration 0006) :
|
||||
-- ce dernier pilote la substitution Maxi de l'accompagnement de menu
|
||||
-- (resolveSelections) ; le reutiliser ferait basculer en 50 cl une
|
||||
-- boisson 30 cl glissee dans un menu Maxi (effet de bord non voulu).
|
||||
-- Grouping DEDIE (base_product_id), distinct de maxi_variant_product_id
|
||||
-- (migration 0006) : base_product_id pilote la selection de taille A LA
|
||||
-- CARTE (picker 30/50 cl) ; maxi_variant_product_id pilote la substitution
|
||||
-- Maxi en MENU (resolveSelections). Les deux coexistent sur une boisson :
|
||||
-- le seed 0006 pointe desormais chaque soda 30 cl vers sa variante 50 cl
|
||||
-- pour qu'un menu Maxi serve la grande boisson (decision metier). Cet
|
||||
-- "effet" est VOULU et ne s'applique qu'aux selections de menu au format
|
||||
-- maxi ; une boisson 30 cl commandee a la carte (resolveLine type product)
|
||||
-- ne consulte jamais maxi_variant_product_id et reste en 30 cl.
|
||||
-- Target : MariaDB 11.4 LTS, InnoDB, utf8mb4 / utf8mb4_unicode_ci.
|
||||
-- =============================================================================
|
||||
|
||||
|
|
|
|||
50
db/seeds/0006_drink_maxi_variant.sql
Normal file
50
db/seeds/0006_drink_maxi_variant.sql
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
-- =============================================================================
|
||||
-- Wakdo — Seed 0006 : boisson de menu = variante 50 cl automatique en Maxi
|
||||
-- =============================================================================
|
||||
-- Purpose : cabler la regle metier "boisson Maxi" sur les donnees seedees, sans
|
||||
-- toucher au code. En menu Maxi, la boisson fontaine doit passer en
|
||||
-- grande (50 cl), comme l'accompagnement passe en Grande Frite.
|
||||
--
|
||||
-- Mecanique reutilisee : product.maxi_variant_product_id (schema 0006),
|
||||
-- deja exploite par OrderRepository::resolveSelections (substitution de
|
||||
-- toute selection de menu au format 'maxi', sans garde sur le slot_type).
|
||||
-- Il suffit donc de POINTER chaque soda fontaine 30 cl vers sa variante
|
||||
-- 50 cl (creee par le seed 0005) : aucune ligne de code serveur a ecrire.
|
||||
-- Le decrement de stock (consumption) frappera la 50 cl, et le snapshot
|
||||
-- de libelle reflechira "<soda> 50cl".
|
||||
--
|
||||
-- Perimetre : seules les boissons fontaine ont une variante 50 cl (Coca Cola, Coca
|
||||
-- Sans Sucres, Fanta Orange, Ice Tea Peche, Ice Tea Citron). Les boissons en
|
||||
-- bouteille (Eau, Jus d'Orange, Jus de Pommes Bio) n'ont pas de variante : elles
|
||||
-- restent en taille standard meme en Maxi (degradation gracieuse, modele fast-food
|
||||
-- usuel). Le surcout Maxi est porte par le menu (price_maxi_cents), pas par la
|
||||
-- boisson : aucune incidence de prix sur ces bouteilles.
|
||||
--
|
||||
-- Phase : depend du schema 0006 (maxi_variant_product_id) ET du seed 0005 (les
|
||||
-- variantes 50 cl doivent exister). Joue donc APRES 0005 (ordre
|
||||
-- lexicographique du runner db/seed.sh).
|
||||
--
|
||||
-- Conventions:
|
||||
-- - Aucun id en dur : la cible est resolue structurellement (la variante 50 cl
|
||||
-- est la ligne dont base_product_id pointe la base et size_cl = 50).
|
||||
-- - IDEMPOTENT : UPDATE ... JOIN convergent (repositionne la meme valeur a chaque
|
||||
-- execution). MariaDB autorise le self-join en UPDATE multi-tables (l'erreur
|
||||
-- 1093 ne vise que les sous-requetes sur la table cible, pas les JOIN).
|
||||
-- =============================================================================
|
||||
|
||||
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- Lier chaque boisson de base (30 cl, base_product_id NULL) a sa variante 50 cl.
|
||||
-- La jointure ne matche que les produits ayant une variante de taille 50 cl :
|
||||
-- structurellement, les seules boissons fontaine. Les accompagnements (frites,
|
||||
-- deja relies par 0004) ne sont pas des variantes de taille -> non touches. Les
|
||||
-- bouteilles sans variante 50 cl ne matchent pas -> maxi_variant_product_id reste
|
||||
-- NULL.
|
||||
-- -----------------------------------------------------------------------------
|
||||
UPDATE product AS base
|
||||
JOIN product AS variant
|
||||
ON variant.base_product_id = base.id
|
||||
AND variant.size_cl = 50
|
||||
SET base.maxi_variant_product_id = variant.id
|
||||
WHERE base.base_product_id IS NULL;
|
||||
|
|
@ -8,7 +8,8 @@
|
|||
* Traduction panier borne -> contrat API :
|
||||
* - produit simple -> { type:'product', product_id, quantity }
|
||||
* - menu -> { type:'menu', menu_id, quantity, format, selections }
|
||||
* format = 'maxi' si supplement_cents>0, sinon 'normal'.
|
||||
* format = cartItem.format (choix Normal/Maxi porte par l'item panier) ; repli
|
||||
* historique sur supplement_cents>0 pour un panier serialise avant cette version.
|
||||
* selections = [{menu_slot_id, product_id}] reconstruites depuis la composition
|
||||
* (accompagnement/boisson/sauce) mappee aux slots reels du menu (re-fetch).
|
||||
* - service_mode : 'sur-place' -> 'dine_in', 'a-emporter' -> 'takeaway'.
|
||||
|
|
@ -64,7 +65,10 @@ export function buildOrderItem(cartItem, menuSlotsById) {
|
|||
type: 'menu',
|
||||
menu_id: cartItem.id,
|
||||
quantity: cartItem.quantite,
|
||||
format: (cartItem.supplement_cents ?? 0) > 0 ? 'maxi' : 'normal',
|
||||
// Format choisi par l'utilisateur, transporte explicitement. Repli sur
|
||||
// l'ancienne inference (supplement_cents>0) pour un panier serialise en
|
||||
// sessionStorage avant l'ajout du champ format.
|
||||
format: cartItem.format ?? ((cartItem.supplement_cents ?? 0) > 0 ? 'maxi' : 'normal'),
|
||||
selections: buildSelections(cartItem.composition, menuSlotsById[cartItem.id] || []),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,6 +121,11 @@ export function buildMenuCartItem(menu, model, { size, selections }) {
|
|||
quantite: 1,
|
||||
image: menu.image,
|
||||
supplement_cents: supplement,
|
||||
// format PORTE le choix Normal/Maxi de l'utilisateur, transporte tel quel
|
||||
// jusqu'au contrat API. Le serveur l'utilise pour le prix Maxi ET la
|
||||
// substitution des variantes (accompagnement Grande, boisson 50 cl). A NE
|
||||
// PAS re-deviner depuis supplement_cents (faux negatif si maxi == normal).
|
||||
format: isMaxi ? 'maxi' : 'normal',
|
||||
composition,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,6 +121,52 @@ final class OrderRepositoryTest extends TestCase
|
|||
self::assertSame(8, $sel['slot']);
|
||||
}
|
||||
|
||||
public function testMenuMaxiSwapsDrinkSelectionToLargeVariant(): void
|
||||
{
|
||||
// Au format maxi, la boisson fontaine Coca Cola (variante = Coca Cola 50cl,
|
||||
// id 15) doit etre persistee comme la 50 cl : meme mecanique que l'accompagnement
|
||||
// Grande Frite (maxi_variant_product_id), pour que le stock decremente la 50 cl
|
||||
// et que le snapshot reflete "Coca Cola 50cl". Aucune garde sur le slot_type.
|
||||
$db = new FakeOrderDatabase();
|
||||
$db->menus[5] = ['id' => 5, 'burger_product_id' => 12, 'name' => 'Menu', 'price_normal_cents' => 990, 'price_maxi_cents' => 1200, 'is_available' => 1];
|
||||
$db->products[12] = ['id' => 12, 'name' => 'Burger', 'price_cents' => 600, 'vat_rate' => 100, 'is_available' => 1];
|
||||
$db->products[14] = ['id' => 14, 'name' => 'Coca Cola', 'price_cents' => 190, 'vat_rate' => 100, 'is_available' => 1, 'maxi_variant_product_id' => 15];
|
||||
$db->products[15] = ['id' => 15, 'name' => 'Coca Cola 50cl', 'price_cents' => 240, 'vat_rate' => 100, 'is_available' => 1, 'maxi_variant_product_id' => null];
|
||||
$db->slotRows[5] = [['id' => 9, 'name' => 'Boisson', 'slot_type' => 'drink', 'is_required' => 1, 'display_order' => 1, 'product_id' => 14]];
|
||||
|
||||
$this->repo($db)->createPending([
|
||||
'service_mode' => 'takeaway',
|
||||
'items' => [['type' => 'menu', 'menu_id' => 5, 'quantity' => 1, 'format' => 'maxi',
|
||||
'selections' => [['menu_slot_id' => 9, 'product_id' => 14]]]], // borne envoie la 30 cl
|
||||
]);
|
||||
|
||||
$sel = $db->firstWrite('INSERT INTO order_item_selection');
|
||||
self::assertSame(15, $sel['pid']); // swap -> Coca Cola 50cl
|
||||
self::assertSame('Coca Cola 50cl', $sel['label']);
|
||||
self::assertSame(9, $sel['slot']);
|
||||
}
|
||||
|
||||
public function testMenuMaxiKeepsBottledDrinkWithoutVariant(): void
|
||||
{
|
||||
// Une boisson en bouteille (Eau) n'a pas de variante 50 cl : meme en Maxi la
|
||||
// selection reste l'Eau de base (degradation gracieuse, modele fast-food).
|
||||
$db = new FakeOrderDatabase();
|
||||
$db->menus[5] = ['id' => 5, 'burger_product_id' => 12, 'name' => 'Menu', 'price_normal_cents' => 990, 'price_maxi_cents' => 1200, 'is_available' => 1];
|
||||
$db->products[12] = ['id' => 12, 'name' => 'Burger', 'price_cents' => 600, 'vat_rate' => 100, 'is_available' => 1];
|
||||
$db->products[16] = ['id' => 16, 'name' => 'Eau', 'price_cents' => 150, 'vat_rate' => 100, 'is_available' => 1, 'maxi_variant_product_id' => null];
|
||||
$db->slotRows[5] = [['id' => 9, 'name' => 'Boisson', 'slot_type' => 'drink', 'is_required' => 1, 'display_order' => 1, 'product_id' => 16]];
|
||||
|
||||
$this->repo($db)->createPending([
|
||||
'service_mode' => 'takeaway',
|
||||
'items' => [['type' => 'menu', 'menu_id' => 5, 'quantity' => 1, 'format' => 'maxi',
|
||||
'selections' => [['menu_slot_id' => 9, 'product_id' => 16]]]],
|
||||
]);
|
||||
|
||||
$sel = $db->firstWrite('INSERT INTO order_item_selection');
|
||||
self::assertSame(16, $sel['pid']); // pas de variante -> reste l'Eau
|
||||
self::assertSame('Eau', $sel['label']);
|
||||
}
|
||||
|
||||
public function testMenuNormalKeepsBaseSideSelection(): void
|
||||
{
|
||||
// Format normal : aucune substitution, l'accompagnement reste la Moyenne
|
||||
|
|
|
|||
|
|
@ -60,6 +60,17 @@ test('buildOrderItem: menu normal vs maxi (format + selections)', () => {
|
|||
assert.equal(maxi.format, 'maxi');
|
||||
});
|
||||
|
||||
test('buildOrderItem: format explicite prime sur l inference (maxi meme si supplement 0)', () => {
|
||||
// Le choix utilisateur est transporte dans cartItem.format ; il ne doit PAS etre
|
||||
// re-devine du prix (un menu maxi == normal serait sinon envoye en normal).
|
||||
const explicit = { id: 1, type: 'menu', quantite: 1, supplement_cents: 0, format: 'maxi',
|
||||
composition: { boisson: { id: 14 } } };
|
||||
assert.equal(buildOrderItem(explicit, { 1: slots() }).format, 'maxi');
|
||||
// Repli historique : un panier serialise sans champ format infere depuis supplement.
|
||||
const legacy = { id: 1, type: 'menu', quantite: 1, supplement_cents: 150, composition: {} };
|
||||
assert.equal(buildOrderItem(legacy, { 1: slots() }).format, 'maxi');
|
||||
});
|
||||
|
||||
/* --- buildOrderPayload --------------------------------------------------- */
|
||||
|
||||
test('buildOrderPayload: dine_in inclut service_tag ; takeaway l omet', () => {
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ test('buildMenuCartItem Normal: prix normal, pas de supplement, taille N, compos
|
|||
assert.equal(item.type, 'menu');
|
||||
assert.equal(item.prix_cents, 880);
|
||||
assert.equal(item.supplement_cents, 0);
|
||||
assert.equal(item.format, 'normal'); // format explicite transporte
|
||||
assert.equal(item.composition.burger.libelle, 'Le 280');
|
||||
// Normal : l'accompagnement garde son nom de base (pas la variante Maxi).
|
||||
assert.deepEqual(item.composition.accompagnement, { id: 22, libelle: 'Moyenne Frite', taille: 'N' });
|
||||
|
|
@ -83,6 +84,7 @@ test('buildMenuCartItem Maxi: supplement = maxi - normal, taille G sur side/drin
|
|||
const item = buildMenuCartItem(menu, m, { size: 'M', selections: { 1: 14, 16: 22, 31: 47 } });
|
||||
assert.equal(item.prix_cents, 880);
|
||||
assert.equal(item.supplement_cents, 150); // 1030 - 880
|
||||
assert.equal(item.format, 'maxi'); // format explicite transporte
|
||||
assert.equal(item.composition.accompagnement.taille, 'G');
|
||||
assert.equal(item.composition.boisson.taille, 'G');
|
||||
});
|
||||
|
|
@ -91,10 +93,21 @@ test('buildMenuCartItem Maxi: l accompagnement prend sa variante (Grande Frite),
|
|||
const m = buildComposerSteps(detail(), byId());
|
||||
const item = buildMenuCartItem(menu, m, { size: 'M', selections: { 1: 14, 16: 22, 31: 47 } });
|
||||
assert.equal(item.composition.accompagnement.libelle, 'Grande Frite'); // pas "Moyenne Frite"
|
||||
// Boisson sans maxiNom : garde son nom de base meme en Maxi (le Maxi ne l agrandit pas).
|
||||
// Boisson sans maxiNom : garde son nom de base meme en Maxi (cas bouteille).
|
||||
assert.equal(item.composition.boisson.libelle, 'Coca');
|
||||
});
|
||||
|
||||
test('buildMenuCartItem Maxi: la boisson AVEC variante (50cl) prend son nom agrandi', () => {
|
||||
// Apres le seed 0006, une boisson fontaine porte maxiNom (ex. "Coca Cola 50cl") :
|
||||
// en Maxi, le libelle et la taille refletent la grande boisson (meme regle que
|
||||
// l'accompagnement). Aucune logique borne specifique : maxiNom suffit.
|
||||
const byIdDrinkVariant = { ...byId(), 14: { id: 14, nom: 'Coca Cola', prix: 0, image: 'c.png', type: 'produit', maxiNom: 'Coca Cola 50cl' } };
|
||||
const m = buildComposerSteps(detail(), byIdDrinkVariant);
|
||||
const item = buildMenuCartItem(menu, m, { size: 'M', selections: { 1: 14, 16: 22, 31: 47 } });
|
||||
assert.equal(item.composition.boisson.libelle, 'Coca Cola 50cl');
|
||||
assert.equal(item.composition.boisson.taille, 'G');
|
||||
});
|
||||
|
||||
test('buildMenuCartItem Normal: l accompagnement garde "Moyenne Frite" (pas de variante)', () => {
|
||||
const m = buildComposerSteps(detail(), byId());
|
||||
const item = buildMenuCartItem(menu, m, { size: 'N', selections: { 1: 14, 16: 22, 31: 47 } });
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue