package tests import ( "gestion/db" "gestion/models" "testing" "time" ) // ── Traçage du montant économisé (AddToBasket → command_items) ───────────── // // command_items.promo_discount / baskets.promo_discount capturent le montant // (€) économisé par une promotion de prix au moment de AddToBasket, pour // permettre des statistiques historiques fiables même si la configuration de // promotion change ensuite (voir db_basket.go, commentaire sur promoDiscount). func TestAddToBasket_TracksPromoDiscountAmount(t *testing.T) { cleanupStockTestData(t) resetSettingsAfterTest(t) username := newTestClient(t, "promo_discount_track") productID := newTestProduct(t, "PromoDiscountTrack", 20) // newTestProduct crée un palier quantity=1 à 10.00€ dans la catégorie "test". s := db.DefaultSettings() s.PromotionsEnabled = true s.Promotions = []models.CategoryPromotionConfig{ {Category: "test", AllProducts: true, Quantity: 1, DiscountPercent: 20}, } if err := testDB.UpdateSettings(s); err != nil { t.Fatalf("UpdateSettings: %v", err) } basket, err := testDB.AddToBasket(username, productID, 1) if err != nil { t.Fatalf("AddToBasket: %v", err) } if basket.Price != 8.0 { t.Fatalf("précondition prix promo: got=%.2f want=8.00", basket.Price) } if basket.PromoDiscount != 2.0 { t.Errorf("promo_discount doit être l'écart catalogue/promo: got=%.2f want=2.00 (10€-8€)", basket.PromoDiscount) } } func TestAddToBasket_NoPromoDiscountWithoutPromotion(t *testing.T) { cleanupStockTestData(t) resetSettingsAfterTest(t) username := newTestClient(t, "promo_discount_none") productID := newTestProduct(t, "PromoDiscountNone", 20) basket, err := testDB.AddToBasket(username, productID, 1) if err != nil { t.Fatalf("AddToBasket: %v", err) } if basket.PromoDiscount != 0 { t.Errorf("sans promo, promo_discount doit rester à 0: got=%.2f", basket.PromoDiscount) } } // Deux ajouts successifs du même produit (même ligne panier, is_reward=false) // fusionnent quantité et prix (voir AddToBasket) — promo_discount doit être // cumulé de la même façon, pas remplacé par le dernier ajout. func TestAddToBasket_MergePromoDiscountAccumulatesAcrossAdds(t *testing.T) { cleanupStockTestData(t) resetSettingsAfterTest(t) username := newTestClient(t, "promo_discount_merge") productID := newTestProduct(t, "PromoDiscountMerge", 20) s := db.DefaultSettings() s.PromotionsEnabled = true s.Promotions = []models.CategoryPromotionConfig{ {Category: "test", AllProducts: true, Quantity: 1, DiscountPercent: 20}, } if err := testDB.UpdateSettings(s); err != nil { t.Fatalf("UpdateSettings: %v", err) } if _, err := testDB.AddToBasket(username, productID, 1); err != nil { t.Fatalf("AddToBasket (1er ajout): %v", err) } basket, err := testDB.AddToBasket(username, productID, 1) if err != nil { t.Fatalf("AddToBasket (2e ajout): %v", err) } if basket.PromoDiscount != 4.0 { t.Errorf("le cumul des deux ajouts doit sommer les remises: got=%.2f want=4.00 (2×2€)", basket.PromoDiscount) } } func TestCheckout_PromoDiscountCopiedToCommandItems(t *testing.T) { cleanupStockTestData(t) resetSettingsAfterTest(t) username := newTestClient(t, "promo_discount_checkout") productID := newTestProduct(t, "PromoDiscountCheckout", 20) s := db.DefaultSettings() s.PromotionsEnabled = true s.Promotions = []models.CategoryPromotionConfig{ {Category: "test", AllProducts: true, Quantity: 1, DiscountPercent: 20}, } if err := testDB.UpdateSettings(s); err != nil { t.Fatalf("UpdateSettings: %v", err) } if _, err := testDB.AddToBasket(username, productID, 1); err != nil { t.Fatalf("AddToBasket: %v", err) } cmd, err := testDB.CreateCommandWithAddress(username, "1 rue de test") if err != nil { t.Fatalf("CreateCommandWithAddress: %v", err) } var discount float64 if err := testDB.GDB.Raw( `SELECT promo_discount FROM command_items WHERE command_id = ? AND product_id = ?`, cmd.ID, productID, ).Scan(&discount).Error; err != nil { t.Fatalf("lecture command_items: %v", err) } if discount != 2.0 { t.Errorf("promo_discount doit être copié tel quel au checkout: got=%.2f want=2.00", discount) } } // ── Stats admin : total économisé et nombre de commandes concernées ──────── func approveTestCommand(t *testing.T, commandID int) { t.Helper() if err := testDB.GDB.Exec(`UPDATE commandes SET status = 'approved' WHERE id = ?`, commandID).Error; err != nil { t.Fatalf("passage en approved: %v", err) } } func TestTotalPromoDiscount_SumsOnlyApprovedOrders(t *testing.T) { cleanupStockTestData(t) resetSettingsAfterTest(t) username := newTestClient(t, "stats_promo_discount") productID := newTestProduct(t, "StatsPromoDiscount", 20) s := db.DefaultSettings() s.PromotionsEnabled = true s.Promotions = []models.CategoryPromotionConfig{ {Category: "test", AllProducts: true, Quantity: 1, DiscountPercent: 20}, } if err := testDB.UpdateSettings(s); err != nil { t.Fatalf("UpdateSettings: %v", err) } // Commande 1 : avec promo, approuvée → comptée. if _, err := testDB.AddToBasket(username, productID, 1); err != nil { t.Fatalf("AddToBasket (cmd1): %v", err) } cmd1, err := testDB.CreateCommandWithAddress(username, "1 rue de test") if err != nil { t.Fatalf("CreateCommandWithAddress (cmd1): %v", err) } approveTestCommand(t, cmd1.ID) // Commande 2 : avec promo, restée "pending" (statut par défaut du // checkout) → NE DOIT PAS être comptée. if _, err := testDB.AddToBasket(username, productID, 1); err != nil { t.Fatalf("AddToBasket (cmd2): %v", err) } if _, err := testDB.CreateCommandWithAddress(username, "1 rue de test"); err != nil { t.Fatalf("CreateCommandWithAddress (cmd2): %v", err) } total, err := testDB.TotalPromoDiscount(time.Time{}) if err != nil { t.Fatalf("TotalPromoDiscount: %v", err) } if total != 2.0 { t.Errorf("seule la commande approuvée doit compter: got=%.2f want=2.00", total) } count, err := testDB.PromoOrdersCount(time.Time{}) if err != nil { t.Fatalf("PromoOrdersCount: %v", err) } if count != 1 { t.Errorf("une seule commande approuvée avec promo: got=%d want=1", count) } } func TestTotalPromoDiscount_RespectsResetFilter(t *testing.T) { cleanupStockTestData(t) resetSettingsAfterTest(t) username := newTestClient(t, "stats_promo_reset") productID := newTestProduct(t, "StatsPromoReset", 20) s := db.DefaultSettings() s.PromotionsEnabled = true s.Promotions = []models.CategoryPromotionConfig{ {Category: "test", AllProducts: true, Quantity: 1, DiscountPercent: 20}, } if err := testDB.UpdateSettings(s); err != nil { t.Fatalf("UpdateSettings: %v", err) } if _, err := testDB.AddToBasket(username, productID, 1); err != nil { t.Fatalf("AddToBasket: %v", err) } cmd, err := testDB.CreateCommandWithAddress(username, "1 rue de test") if err != nil { t.Fatalf("CreateCommandWithAddress: %v", err) } approveTestCommand(t, cmd.ID) // Un reset postérieur à la création de la commande doit l'exclure — sert // aussi à vérifier que la jointure command_items/commandes qualifie bien // created_at par l'alias (les deux tables ont une colonne created_at, // donc une clause non qualifiée provoquerait une erreur Postgres // "ambiguous column" plutôt qu'un mauvais résultat). future := time.Now().Add(time.Hour) total, err := testDB.TotalPromoDiscount(future) if err != nil { t.Fatalf("TotalPromoDiscount: %v", err) } if total != 0 { t.Errorf("commande antérieure au reset: doit être exclue: got=%.2f want=0", total) } count, err := testDB.PromoOrdersCount(future) if err != nil { t.Fatalf("PromoOrdersCount: %v", err) } if count != 0 { t.Errorf("commande antérieure au reset: doit être exclue: got=%d want=0", count) } } // Une commande avec plusieurs lignes en promo ne doit compter qu'une fois // dans PromoOrdersCount (COUNT DISTINCT command_id), mais le montant total // doit sommer toutes les lignes. func TestPromoOrdersCount_CountsOrderOnceDespiteMultipleDiscountedItems(t *testing.T) { cleanupStockTestData(t) resetSettingsAfterTest(t) username := newTestClient(t, "stats_promo_multi") productA := newTestProduct(t, "StatsPromoMultiA", 20) productB := newTestProduct(t, "StatsPromoMultiB", 20) s := db.DefaultSettings() s.PromotionsEnabled = true s.Promotions = []models.CategoryPromotionConfig{ {Category: "test", AllProducts: true, Quantity: 1, DiscountPercent: 20}, } if err := testDB.UpdateSettings(s); err != nil { t.Fatalf("UpdateSettings: %v", err) } if _, err := testDB.AddToBasket(username, productA, 1); err != nil { t.Fatalf("AddToBasket A: %v", err) } if _, err := testDB.AddToBasket(username, productB, 1); err != nil { t.Fatalf("AddToBasket B: %v", err) } cmd, err := testDB.CreateCommandWithAddress(username, "1 rue de test") if err != nil { t.Fatalf("CreateCommandWithAddress: %v", err) } approveTestCommand(t, cmd.ID) count, err := testDB.PromoOrdersCount(time.Time{}) if err != nil { t.Fatalf("PromoOrdersCount: %v", err) } if count != 1 { t.Errorf("une commande avec 2 lignes en promo doit compter une seule fois: got=%d want=1", count) } total, err := testDB.TotalPromoDiscount(time.Time{}) if err != nil { t.Fatalf("TotalPromoDiscount: %v", err) } if total != 4.0 { t.Errorf("le montant total doit sommer les deux lignes: got=%.2f want=4.00 (2×2€)", total) } } func TestPromoOrdersCount_IgnoresOrdersWithoutDiscount(t *testing.T) { cleanupStockTestData(t) resetSettingsAfterTest(t) username := newTestClient(t, "stats_promo_zero") productID := newTestProduct(t, "StatsPromoZero", 20) // Aucune promotion configurée : promo_discount reste à 0 pour cette commande. if _, err := testDB.AddToBasket(username, productID, 1); err != nil { t.Fatalf("AddToBasket: %v", err) } cmd, err := testDB.CreateCommandWithAddress(username, "1 rue de test") if err != nil { t.Fatalf("CreateCommandWithAddress: %v", err) } approveTestCommand(t, cmd.ID) count, err := testDB.PromoOrdersCount(time.Time{}) if err != nil { t.Fatalf("PromoOrdersCount: %v", err) } if count != 0 { t.Errorf("aucune commande sans promo ne doit être comptée: got=%d want=0", count) } total, err := testDB.TotalPromoDiscount(time.Time{}) if err != nil { t.Fatalf("TotalPromoDiscount: %v", err) } if total != 0 { t.Errorf("aucun montant économisé sans promo: got=%.2f want=0", total) } }