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).
127 lines
5.9 KiB
JavaScript
127 lines
5.9 KiB
JavaScript
/*
|
|
* Tests de checkout.js (P5 L4), node:test. checkout.js + state.js + data.js n'ont
|
|
* aucun acces DOM au chargement -> import statique. Cible : traduction PURE
|
|
* panier->contrat /api/orders (mapServiceMode, buildSelections, buildOrderItem,
|
|
* buildOrderPayload) + submitOrder (fetch + localStorage mockes).
|
|
*/
|
|
import { test, beforeEach } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
mapServiceMode, buildSelections, buildOrderItem, buildOrderPayload, submitOrder,
|
|
} from '../../src/public/borne/assets/js/checkout.js';
|
|
|
|
/* --- mapServiceMode ------------------------------------------------------ */
|
|
|
|
test('mapServiceMode: borne -> contrat (dine_in / takeaway), inconnu -> null', () => {
|
|
assert.equal(mapServiceMode('sur-place'), 'dine_in');
|
|
assert.equal(mapServiceMode('a-emporter'), 'takeaway');
|
|
assert.equal(mapServiceMode(null), null);
|
|
});
|
|
|
|
/* --- buildSelections ----------------------------------------------------- */
|
|
|
|
const slots = () => ([
|
|
{ id: 1, option_product_ids: [14, 15] }, // boisson
|
|
{ id: 16, option_product_ids: [22, 23] }, // accompagnement
|
|
{ id: 31, option_product_ids: [47] }, // sauce
|
|
]);
|
|
|
|
test('buildSelections: mappe les produits choisis a leur slot', () => {
|
|
const comp = { accompagnement: { id: 22 }, boisson: { id: 14 }, sauce: { id: 47 } };
|
|
assert.deepEqual(buildSelections(comp, slots()), [
|
|
{ menu_slot_id: 16, product_id: 22 },
|
|
{ menu_slot_id: 1, product_id: 14 },
|
|
{ menu_slot_id: 31, product_id: 47 },
|
|
]);
|
|
});
|
|
|
|
test('buildSelections: produit hors slots ignore ; composition vide -> []', () => {
|
|
assert.deepEqual(buildSelections({ boisson: { id: 999 } }, slots()), []);
|
|
assert.deepEqual(buildSelections(undefined, slots()), []);
|
|
});
|
|
|
|
/* --- buildOrderItem ------------------------------------------------------ */
|
|
|
|
test('buildOrderItem: produit simple', () => {
|
|
assert.deepEqual(buildOrderItem({ id: 14, type: 'produit', quantite: 3 }, {}), {
|
|
type: 'product', product_id: 14, quantity: 3,
|
|
});
|
|
});
|
|
|
|
test('buildOrderItem: menu normal vs maxi (format + selections)', () => {
|
|
const menuItem = { id: 1, type: 'menu', quantite: 1, supplement_cents: 0,
|
|
composition: { accompagnement: { id: 22 }, boisson: { id: 14 } } };
|
|
const normal = buildOrderItem(menuItem, { 1: slots() });
|
|
assert.equal(normal.format, 'normal');
|
|
assert.equal(normal.menu_id, 1);
|
|
assert.equal(normal.selections.length, 2);
|
|
|
|
const maxi = buildOrderItem({ ...menuItem, supplement_cents: 150 }, { 1: slots() });
|
|
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', () => {
|
|
const cart = [{ id: 14, type: 'produit', quantite: 1 }];
|
|
const din = buildOrderPayload(cart, 'sur-place', '261', {}, 'key-1');
|
|
assert.equal(din.service_mode, 'dine_in');
|
|
assert.equal(din.service_tag, '261');
|
|
assert.equal(din.idempotency_key, 'key-1');
|
|
assert.equal(din.items.length, 1);
|
|
|
|
const take = buildOrderPayload(cart, 'a-emporter', '', {}, 'key-2');
|
|
assert.equal(take.service_mode, 'takeaway');
|
|
assert.equal('service_tag' in take, false);
|
|
});
|
|
|
|
/* --- submitOrder (mocks) ------------------------------------------------- */
|
|
|
|
function stubEnv(cart, mode) {
|
|
const store = { wakdo_cart: JSON.stringify(cart), wakdo_mode: mode };
|
|
global.localStorage = {
|
|
getItem: (k) => (k in store ? store[k] : null),
|
|
setItem: (k, v) => { store[k] = String(v); },
|
|
removeItem: (k) => { delete store[k]; },
|
|
};
|
|
}
|
|
|
|
test('submitOrder: re-fetch slots, POST create puis pay, renvoie order_number', async () => {
|
|
stubEnv([{ id: 1, type: 'menu', quantite: 1, supplement_cents: 0, composition: { boisson: { id: 14 }, accompagnement: { id: 22 } } }], 'sur-place');
|
|
const calls = [];
|
|
global.fetch = async (url, opts) => {
|
|
calls.push({ url, method: opts?.method, body: opts?.body ? JSON.parse(opts.body) : null });
|
|
if (url === '/api/menus/1') return { ok: true, json: async () => ({ data: { slots: slots() } }) };
|
|
if (url === '/api/orders') return { ok: true, json: async () => ({ data: { order_number: 'K12', total_ttc_cents: 800, status: 'pending_payment' } }) };
|
|
if (url === '/api/orders/K12/pay') return { ok: true, json: async () => ({ data: { order_number: 'K12', total_ttc_cents: 800, status: 'paid' } }) };
|
|
throw new Error(`URL inattendue: ${url}`);
|
|
};
|
|
|
|
const res = await submitOrder({ serviceTag: '261' });
|
|
assert.equal(res.order_number, 'K12');
|
|
assert.equal(res.total_ttc_cents, 800);
|
|
|
|
const create = calls.find(c => c.url === '/api/orders');
|
|
assert.equal(create.method, 'POST');
|
|
assert.equal(create.body.service_mode, 'dine_in');
|
|
assert.equal(create.body.service_tag, '261');
|
|
assert.equal(create.body.items[0].type, 'menu');
|
|
assert.equal(create.body.items[0].selections.length, 2);
|
|
assert.ok(calls.some(c => c.url === '/api/orders/K12/pay' && c.method === 'POST'));
|
|
});
|
|
|
|
test('submitOrder: panier vide -> jette EMPTY_CART', async () => {
|
|
stubEnv([], 'a-emporter');
|
|
await assert.rejects(() => submitOrder(), /EMPTY_CART/);
|
|
});
|