package tests import ( "encoding/json" "gestion/db" "gestion/handlers" "gestion/models" "net/http" "net/http/httptest" "strconv" "testing" "github.com/gin-gonic/gin" ) // configurePromotionSettings applique les settings donnés (avec Promotions) // via testDB.UpdateSettings, comme le ferait l'admin — testDB.UpdateSettings // normalise déjà les slices nil, donc ce helper reste minimal. func configurePromotionSettings(t *testing.T, settings models.AppSettings) { t.Helper() if err := testDB.UpdateSettings(settings); err != nil { t.Fatalf("UpdateSettings: %v", err) } } // ── Persistance des settings (save→reload) ────────────────────────────────── func TestUpdateSettings_PromotionsRoundTrip(t *testing.T) { resetSettingsAfterTest(t) s := db.DefaultSettings() s.PromotionsEnabled = true s.Promotions = []models.CategoryPromotionConfig{ { Category: "test", DiscountPercent: 15.5, AllProducts: false, Products: []models.PromotionProductQuantity{ {ProductID: 111, Quantity: 2}, {ProductID: 222, Quantity: 1}, }, }, } if err := testDB.UpdateSettings(s); err != nil { t.Fatalf("UpdateSettings: %v", err) } loaded, err := testDB.GetSettings() if err != nil { t.Fatalf("GetSettings: %v", err) } if !loaded.PromotionsEnabled { t.Fatal("promotions_enabled devrait être true après reload") } if len(loaded.Promotions) != 1 { t.Fatalf("promotions: got=%d want=1: %+v", len(loaded.Promotions), loaded.Promotions) } promo := loaded.Promotions[0] if promo.Category != "test" || promo.DiscountPercent != 15.5 { t.Errorf("promo mal persistée: got=%+v", promo) } if len(promo.Products) != 2 || promo.Products[0].ProductID != 111 || promo.Products[0].Quantity != 2 { t.Errorf("products mal persistés: got=%+v", promo.Products) } // Désactivation : doit persister à false, pas de résurrection (même // classe de bug que TestUpdateSettings_DisablingPointsRewardPersistsAsNil). s.PromotionsEnabled = false if err := testDB.UpdateSettings(s); err != nil { t.Fatalf("UpdateSettings (désactivation): %v", err) } loaded, err = testDB.GetSettings() if err != nil { t.Fatalf("GetSettings (désactivation): %v", err) } if loaded.PromotionsEnabled { t.Error("promotions_enabled devrait rester false après désactivation") } } // ── Résolution de la réduction (logique pure) ─────────────────────────────── func TestResolvePromotionDiscount_MatchesAllProductsAtConfiguredQuantity(t *testing.T) { settings := &models.AppSettings{ PromotionsEnabled: true, Promotions: []models.CategoryPromotionConfig{ {Category: "fleurs", DiscountPercent: 20, AllProducts: true, Quantity: 5}, }, } discount, ok := db.ResolvePromotionDiscount(settings, 42, "fleurs", 5) if !ok || discount != 20 { t.Errorf("got discount=%.2f ok=%v want=20/true", discount, ok) } // Mauvaise quantité : pas de promo. if _, ok := db.ResolvePromotionDiscount(settings, 42, "fleurs", 3); ok { t.Error("ne devrait pas matcher une quantité différente de celle configurée") } // Mauvaise catégorie : pas de promo. if _, ok := db.ResolvePromotionDiscount(settings, 42, "autre", 5); ok { t.Error("ne devrait pas matcher une catégorie différente") } } func TestResolvePromotionDiscount_DisabledReturnsNoDiscount(t *testing.T) { settings := &models.AppSettings{ PromotionsEnabled: false, Promotions: []models.CategoryPromotionConfig{ {Category: "fleurs", DiscountPercent: 20, AllProducts: true, Quantity: 5}, }, } if _, ok := db.ResolvePromotionDiscount(settings, 42, "fleurs", 5); ok { t.Error("aucune promo ne doit s'appliquer si promotions_enabled = false") } } func TestResolvePromotionDiscount_PerProductSelection(t *testing.T) { settings := &models.AppSettings{ PromotionsEnabled: true, Promotions: []models.CategoryPromotionConfig{ { Category: "fleurs", AllProducts: false, Products: []models.PromotionProductQuantity{ {ProductID: 1, Quantity: 2}, }, DiscountPercent: 10, }, }, } if discount, ok := db.ResolvePromotionDiscount(settings, 1, "fleurs", 2); !ok || discount != 10 { t.Errorf("produit sélectionné à la bonne quantité: got discount=%.2f ok=%v", discount, ok) } if _, ok := db.ResolvePromotionDiscount(settings, 1, "fleurs", 3); ok { t.Error("mauvaise quantité pour ce produit : ne doit pas matcher") } if _, ok := db.ResolvePromotionDiscount(settings, 2, "fleurs", 2); ok { t.Error("produit non sélectionné : ne doit pas matcher") } } // ── AddToBasket applique réellement la réduction au prix facturé ─────────── func TestAddToBasket_AppliesPromotionDiscount(t *testing.T) { cleanupStockTestData(t) resetSettingsAfterTest(t) username := newTestClient(t, "promo_basket_applies") productID := newTestProduct(t, "PromoBasketApplies", 10) // 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", DiscountPercent: 20, AllProducts: true, Quantity: 1}, } configurePromotionSettings(t, s) basket, err := testDB.AddToBasket(username, productID, 1) if err != nil { t.Fatalf("AddToBasket: %v", err) } if basket.Price != 8.0 { t.Errorf("prix attendu = 10€ - 20%% = 8.00€: got=%.2f", basket.Price) } } func TestAddToBasket_NoDiscountWhenPromotionsDisabled(t *testing.T) { cleanupStockTestData(t) resetSettingsAfterTest(t) username := newTestClient(t, "promo_basket_disabled") productID := newTestProduct(t, "PromoBasketDisabled", 10) s := db.DefaultSettings() s.PromotionsEnabled = false s.Promotions = []models.CategoryPromotionConfig{ {Category: "test", DiscountPercent: 20, AllProducts: true, Quantity: 1}, } configurePromotionSettings(t, s) basket, err := testDB.AddToBasket(username, productID, 1) if err != nil { t.Fatalf("AddToBasket: %v", err) } if basket.Price != 10.0 { t.Errorf("promotions désactivées: le prix catalogue plein doit s'appliquer: got=%.2f want=10.00", basket.Price) } } func TestAddToBasket_NoDiscountForDifferentProductSelection(t *testing.T) { cleanupStockTestData(t) resetSettingsAfterTest(t) username := newTestClient(t, "promo_basket_other_product") promotedID := newTestProduct(t, "PromoBasketOtherPromoted", 10) otherID := newTestProduct(t, "PromoBasketOtherPlain", 10) s := db.DefaultSettings() s.PromotionsEnabled = true s.Promotions = []models.CategoryPromotionConfig{ { Category: "test", AllProducts: false, Products: []models.PromotionProductQuantity{{ProductID: promotedID, Quantity: 1}}, DiscountPercent: 50, }, } configurePromotionSettings(t, s) promotedBasket, err := testDB.AddToBasket(username, promotedID, 1) if err != nil { t.Fatalf("AddToBasket (promu): %v", err) } if promotedBasket.Price != 5.0 { t.Errorf("produit promu: prix attendu = 10€ - 50%% = 5.00€: got=%.2f", promotedBasket.Price) } otherBasket, err := testDB.AddToBasket(username, otherID, 1) if err != nil { t.Fatalf("AddToBasket (autre): %v", err) } if otherBasket.Price != 10.0 { t.Errorf("produit non sélectionné dans la promo: prix plein attendu=10.00: got=%.2f", otherBasket.Price) } } // ── Affichage catalogue : le prix promo est annoté sur le palier concerné ── func TestGetProductByID_AnnotatesPromoPriceOnMatchingTier(t *testing.T) { cleanupStockTestData(t) resetSettingsAfterTest(t) productID := newTestProduct(t, "PromoDisplayAnnotated", 10) s := db.DefaultSettings() s.PromotionsEnabled = true s.Promotions = []models.CategoryPromotionConfig{ {Category: "test", DiscountPercent: 25, AllProducts: true, Quantity: 1}, } configurePromotionSettings(t, s) idStr := strconv.Itoa(productID) req := httptest.NewRequest(http.MethodGet, "/api/v1/products/"+idStr, nil) rec := httptest.NewRecorder() c, _ := gin.CreateTestContext(rec) c.Request = req c.Set("database", testDB) c.Params = gin.Params{{Key: "id", Value: idStr}} c.Set("role", "client") handlers.GetProductByID(c) if rec.Code != http.StatusOK { t.Fatalf("status HTTP: got=%d body=%s", rec.Code, rec.Body.String()) } var resp struct { Data models.Product `json:"data"` } if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { t.Fatalf("décodage réponse: %v body=%s", err, rec.Body.String()) } if len(resp.Data.Prices) != 1 { t.Fatalf("attendu 1 palier de prix: got=%+v", resp.Data.Prices) } tier := resp.Data.Prices[0] if tier.PromoPrice == nil { t.Fatal("PromoPrice devrait être renseigné pour ce palier couvert par la promo") } if *tier.PromoPrice != 7.5 { t.Errorf("promo_price attendu = 10€ - 25%% = 7.50€: got=%.2f", *tier.PromoPrice) } if tier.PromoPercent != 25 { t.Errorf("promo_percent attendu=25: got=%.2f", tier.PromoPercent) } }