396 lines
14 KiB
Go
396 lines
14 KiB
Go
package tests
|
|
|
|
import (
|
|
"gestion/db"
|
|
"gestion/models"
|
|
"testing"
|
|
)
|
|
|
|
// ── Persistance des settings (save→reload) ──────────────────────────────────
|
|
|
|
func TestUpdateSettings_FreeGiftsRoundTrip(t *testing.T) {
|
|
resetSettingsAfterTest(t)
|
|
|
|
s := db.DefaultSettings()
|
|
s.FreeGiftsEnabled = true
|
|
s.FreeGifts = []models.CategoryFreeGiftConfig{
|
|
{
|
|
Category: "test",
|
|
AllProducts: false,
|
|
Products: []models.FreeGiftProductQuantity{
|
|
{ProductID: 111, Tiers: []models.FreeGiftTier{
|
|
{BuyQuantity: 10, FreeQuantity: 1},
|
|
{BuyQuantity: 20, FreeQuantity: 3},
|
|
}},
|
|
},
|
|
},
|
|
}
|
|
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.FreeGiftsEnabled {
|
|
t.Fatal("free_gifts_enabled devrait être true après reload")
|
|
}
|
|
if len(loaded.FreeGifts) != 1 {
|
|
t.Fatalf("free_gifts: got=%d want=1: %+v", len(loaded.FreeGifts), loaded.FreeGifts)
|
|
}
|
|
gift := loaded.FreeGifts[0]
|
|
if gift.Category != "test" || len(gift.Products) != 1 {
|
|
t.Fatalf("free gift mal persistée: got=%+v", gift)
|
|
}
|
|
if len(gift.Products[0].Tiers) != 2 || gift.Products[0].Tiers[1].BuyQuantity != 20 || gift.Products[0].Tiers[1].FreeQuantity != 3 {
|
|
t.Errorf("tiers mal persistés: got=%+v", gift.Products[0].Tiers)
|
|
}
|
|
|
|
// Désactivation : doit persister à false, pas de résurrection (même
|
|
// classe de bug que TestUpdateSettings_DisablingPointsRewardPersistsAsNil).
|
|
s.FreeGiftsEnabled = 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.FreeGiftsEnabled {
|
|
t.Error("free_gifts_enabled devrait rester false après désactivation")
|
|
}
|
|
}
|
|
|
|
// ── Résolution de la quantité offerte (logique pure) ────────────────────────
|
|
|
|
func TestResolveFreeGift_AllProductsAtOrAboveThreshold(t *testing.T) {
|
|
settings := &models.AppSettings{
|
|
FreeGiftsEnabled: true,
|
|
FreeGifts: []models.CategoryFreeGiftConfig{
|
|
{Category: "fleurs", AllProducts: true, Tiers: []models.FreeGiftTier{
|
|
{BuyQuantity: 10, FreeQuantity: 1},
|
|
}},
|
|
},
|
|
}
|
|
if got := db.ResolveFreeGift(settings, 42, "fleurs", 10); got != 1 {
|
|
t.Errorf("quantité offerte: got=%.2f want=1", got)
|
|
}
|
|
if got := db.ResolveFreeGift(settings, 42, "fleurs", 15); got != 1 {
|
|
t.Errorf("au-dessus du seuil, le cadeau reste dû: got=%.2f want=1", got)
|
|
}
|
|
if got := db.ResolveFreeGift(settings, 42, "fleurs", 9); got != 0 {
|
|
t.Errorf("sous le seuil, aucun cadeau: got=%.2f want=0", got)
|
|
}
|
|
}
|
|
|
|
func TestResolveFreeGift_DisabledReturnsZero(t *testing.T) {
|
|
settings := &models.AppSettings{
|
|
FreeGiftsEnabled: false,
|
|
FreeGifts: []models.CategoryFreeGiftConfig{
|
|
{Category: "fleurs", AllProducts: true, Tiers: []models.FreeGiftTier{
|
|
{BuyQuantity: 10, FreeQuantity: 1},
|
|
}},
|
|
},
|
|
}
|
|
if got := db.ResolveFreeGift(settings, 42, "fleurs", 10); got != 0 {
|
|
t.Errorf("offres désactivées: aucun cadeau attendu: got=%.2f", got)
|
|
}
|
|
}
|
|
|
|
func TestResolveFreeGift_PerProductHighestTierApplies(t *testing.T) {
|
|
settings := &models.AppSettings{
|
|
FreeGiftsEnabled: true,
|
|
FreeGifts: []models.CategoryFreeGiftConfig{
|
|
{
|
|
Category: "fleurs",
|
|
AllProducts: false,
|
|
Products: []models.FreeGiftProductQuantity{
|
|
{ProductID: 111, Tiers: []models.FreeGiftTier{
|
|
{BuyQuantity: 10, FreeQuantity: 1},
|
|
{BuyQuantity: 20, FreeQuantity: 3},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
if got := db.ResolveFreeGift(settings, 111, "fleurs", 10); got != 1 {
|
|
t.Errorf("seuil 10g: got=%.2f want=1", got)
|
|
}
|
|
// 25g dépasse les deux seuils : le plus élevé (20g→3g) doit être retenu,
|
|
// pas le premier de la liste (10g→1g).
|
|
if got := db.ResolveFreeGift(settings, 111, "fleurs", 25); got != 3 {
|
|
t.Errorf("seuil le plus élevé atteint (20g→3g): got=%.2f want=3", got)
|
|
}
|
|
// Produit non listé dans cette config : aucun cadeau.
|
|
if got := db.ResolveFreeGift(settings, 222, "fleurs", 25); got != 0 {
|
|
t.Errorf("produit non couvert: got=%.2f want=0", got)
|
|
}
|
|
}
|
|
|
|
// ── Intégration AddToBasket : la quantité livrée inclut le cadeau, au même prix ──
|
|
|
|
func TestAddToBasket_AppliesFreeGiftQuantityAtSamePrice(t *testing.T) {
|
|
cleanupStockTestData(t)
|
|
resetSettingsAfterTest(t)
|
|
username := newTestClient(t, "freegift_basket_applies")
|
|
productID := newTestProduct(t, "FreeGiftBasketApplies", 50)
|
|
// newTestProduct crée un palier quantity=1 à 10.00€ dans la catégorie "test".
|
|
|
|
s := db.DefaultSettings()
|
|
s.FreeGiftsEnabled = true
|
|
s.FreeGifts = []models.CategoryFreeGiftConfig{
|
|
{Category: "test", AllProducts: true, Tiers: []models.FreeGiftTier{
|
|
{BuyQuantity: 10, FreeQuantity: 1},
|
|
}},
|
|
}
|
|
if err := testDB.UpdateSettings(s); err != nil {
|
|
t.Fatalf("UpdateSettings: %v", err)
|
|
}
|
|
|
|
basket, err := testDB.AddToBasket(username, productID, 10)
|
|
if err != nil {
|
|
t.Fatalf("AddToBasket: %v", err)
|
|
}
|
|
if basket.Quantity != 11 {
|
|
t.Errorf("quantité livrée attendue = 10 + 1 offert = 11: got=%.2f", basket.Quantity)
|
|
}
|
|
if basket.Price != 10.0 {
|
|
t.Errorf("le prix ne doit pas changer (facturé sur les 10g demandés): got=%.2f want=10.00", basket.Price)
|
|
}
|
|
}
|
|
|
|
func TestAddToBasket_NoFreeGiftBelowThreshold(t *testing.T) {
|
|
cleanupStockTestData(t)
|
|
resetSettingsAfterTest(t)
|
|
username := newTestClient(t, "freegift_basket_below")
|
|
productID := newTestProduct(t, "FreeGiftBasketBelow", 50)
|
|
|
|
s := db.DefaultSettings()
|
|
s.FreeGiftsEnabled = true
|
|
s.FreeGifts = []models.CategoryFreeGiftConfig{
|
|
{Category: "test", AllProducts: true, Tiers: []models.FreeGiftTier{
|
|
{BuyQuantity: 10, FreeQuantity: 1},
|
|
}},
|
|
}
|
|
if err := testDB.UpdateSettings(s); err != nil {
|
|
t.Fatalf("UpdateSettings: %v", err)
|
|
}
|
|
|
|
basket, err := testDB.AddToBasket(username, productID, 5)
|
|
if err != nil {
|
|
t.Fatalf("AddToBasket: %v", err)
|
|
}
|
|
if basket.Quantity != 5 {
|
|
t.Errorf("sous le seuil, aucune quantité offerte: got=%.2f want=5", basket.Quantity)
|
|
}
|
|
}
|
|
|
|
// La quantité réellement décomptée du stock doit inclure le cadeau : un stock
|
|
// suffisant pour la quantité demandée mais pas pour demandée+offerte doit
|
|
// faire échouer l'ajout, pas livrer un cadeau partiel.
|
|
func TestAddToBasket_FreeGiftRejectedWhenStockInsufficientForBonus(t *testing.T) {
|
|
cleanupStockTestData(t)
|
|
resetSettingsAfterTest(t)
|
|
username := newTestClient(t, "freegift_basket_stock")
|
|
productID := newTestProduct(t, "FreeGiftBasketStock", 10) // stock = 10, pile la quantité demandée
|
|
|
|
s := db.DefaultSettings()
|
|
s.FreeGiftsEnabled = true
|
|
s.FreeGifts = []models.CategoryFreeGiftConfig{
|
|
{Category: "test", AllProducts: true, Tiers: []models.FreeGiftTier{
|
|
{BuyQuantity: 10, FreeQuantity: 1},
|
|
}},
|
|
}
|
|
if err := testDB.UpdateSettings(s); err != nil {
|
|
t.Fatalf("UpdateSettings: %v", err)
|
|
}
|
|
|
|
if _, err := testDB.AddToBasket(username, productID, 10); err == nil {
|
|
t.Fatal("stock=10 ne doit pas suffire pour livrer 10g + 1g offert")
|
|
}
|
|
}
|
|
|
|
// ── Intégration checkout : le bonus offert est bien décompté du stock ──────
|
|
//
|
|
// AddToBasket stocke déjà quantity = demandée + offerte (voir tests
|
|
// ci-dessus) ; CreateCommandWithAddress ne relit ni ne recalcule cette
|
|
// quantité — elle est copiée telle quelle dans command_items.quantite et
|
|
// utilisée telle quelle pour décrémenter products.stock (db_commands.go).
|
|
// Ces tests vérifient ce chemin de bout en bout, pas juste AddToBasket isolé.
|
|
|
|
func TestCheckout_FreeGiftBonusQuantityDecrementsStock(t *testing.T) {
|
|
cleanupStockTestData(t)
|
|
resetSettingsAfterTest(t)
|
|
username := newTestClient(t, "freegift_checkout_stock")
|
|
productID := newTestProduct(t, "FreeGiftCheckoutStock", 50)
|
|
// newTestProduct crée un palier quantity=1 à 10.00€ dans la catégorie "test".
|
|
|
|
s := db.DefaultSettings()
|
|
s.FreeGiftsEnabled = true
|
|
s.FreeGifts = []models.CategoryFreeGiftConfig{
|
|
{Category: "test", AllProducts: true, Tiers: []models.FreeGiftTier{
|
|
{BuyQuantity: 10, FreeQuantity: 1},
|
|
}},
|
|
}
|
|
if err := testDB.UpdateSettings(s); err != nil {
|
|
t.Fatalf("UpdateSettings: %v", err)
|
|
}
|
|
|
|
if _, err := testDB.AddToBasket(username, productID, 10); err != nil {
|
|
t.Fatalf("AddToBasket: %v", err)
|
|
}
|
|
|
|
cmd, err := testDB.CreateCommandWithAddress(username, "1 rue de test")
|
|
if err != nil {
|
|
t.Fatalf("CreateCommandWithAddress: %v", err)
|
|
}
|
|
|
|
// 50 initial - (10 demandés + 1 offert) = 39, pas 40.
|
|
if got := productStock(t, productID); got != 39 {
|
|
t.Errorf("stock après checkout avec cadeau: got=%.2f want=39 (50 - 11)", got)
|
|
}
|
|
|
|
var item struct {
|
|
Quantite float64 `gorm:"column:quantite"`
|
|
Prix float64 `gorm:"column:prix"`
|
|
}
|
|
if err := testDB.GDB.Raw(
|
|
`SELECT quantite, prix FROM command_items WHERE command_id = ? AND product_id = ?`,
|
|
cmd.ID, productID,
|
|
).Scan(&item).Error; err != nil {
|
|
t.Fatalf("lecture command_items: %v", err)
|
|
}
|
|
if item.Quantite != 11 {
|
|
t.Errorf("command_items.quantite doit inclure le cadeau: got=%.2f want=11", item.Quantite)
|
|
}
|
|
if item.Prix != 10.0 {
|
|
t.Errorf("command_items.prix ne doit pas changer (facturé sur les 10g demandés): got=%.2f want=10.00", item.Prix)
|
|
}
|
|
}
|
|
|
|
func TestCheckout_FreeGiftRollsBackWhenStockInsufficientForBonus(t *testing.T) {
|
|
cleanupStockTestData(t)
|
|
resetSettingsAfterTest(t)
|
|
username := newTestClient(t, "freegift_checkout_rollback")
|
|
productID := newTestProduct(t, "FreeGiftCheckoutRollback", 50)
|
|
|
|
s := db.DefaultSettings()
|
|
s.FreeGiftsEnabled = true
|
|
s.FreeGifts = []models.CategoryFreeGiftConfig{
|
|
{Category: "test", AllProducts: true, Tiers: []models.FreeGiftTier{
|
|
{BuyQuantity: 10, FreeQuantity: 1},
|
|
}},
|
|
}
|
|
if err := testDB.UpdateSettings(s); err != nil {
|
|
t.Fatalf("UpdateSettings: %v", err)
|
|
}
|
|
if _, err := testDB.AddToBasket(username, productID, 10); err != nil {
|
|
t.Fatalf("AddToBasket: %v", err)
|
|
}
|
|
|
|
// Le stock chute sous 11 (10 demandés + 1 offert) après l'ajout au panier,
|
|
// simulant une vente concurrente qui vide le stock entre AddToBasket et
|
|
// checkout — le checkout doit échouer et ne rien décrémenter.
|
|
if err := testDB.GDB.Exec(`UPDATE products SET stock = 10 WHERE id = ?`, productID).Error; err != nil {
|
|
t.Fatalf("réduction stock: %v", err)
|
|
}
|
|
|
|
if _, err := testDB.CreateCommandWithAddress(username, "1 rue de test"); err == nil {
|
|
t.Fatal("checkout attendu en échec: stock=10 insuffisant pour 10 demandés + 1 offert")
|
|
}
|
|
if got := productStock(t, productID); got != 10 {
|
|
t.Errorf("stock ne doit pas bouger si le checkout échoue: got=%.2f want=10", got)
|
|
}
|
|
}
|
|
|
|
// ── Intégration annulation : le remboursement inclut le bonus offert ───────
|
|
|
|
func TestCancelCommandAtomic_RefundsFreeGiftBonusQuantity(t *testing.T) {
|
|
cleanupStockTestData(t)
|
|
resetSettingsAfterTest(t)
|
|
username := newTestClient(t, "freegift_cancel_refund")
|
|
productID := newTestProduct(t, "FreeGiftCancelRefund", 50)
|
|
|
|
s := db.DefaultSettings()
|
|
s.FreeGiftsEnabled = true
|
|
s.FreeGifts = []models.CategoryFreeGiftConfig{
|
|
{Category: "test", AllProducts: true, Tiers: []models.FreeGiftTier{
|
|
{BuyQuantity: 10, FreeQuantity: 1},
|
|
}},
|
|
}
|
|
if err := testDB.UpdateSettings(s); err != nil {
|
|
t.Fatalf("UpdateSettings: %v", err)
|
|
}
|
|
if _, err := testDB.AddToBasket(username, productID, 10); err != nil {
|
|
t.Fatalf("AddToBasket: %v", err)
|
|
}
|
|
|
|
cmd, err := testDB.CreateCommandWithAddress(username, "1 rue de test")
|
|
if err != nil {
|
|
t.Fatalf("CreateCommandWithAddress: %v", err)
|
|
}
|
|
if got := productStock(t, productID); got != 39 {
|
|
t.Fatalf("précondition stock post-checkout: got=%.2f want=39", got)
|
|
}
|
|
|
|
if _, err := testDB.CancelCommandAtomic(cmd.ID, username, "test", false); err != nil {
|
|
t.Fatalf("CancelCommandAtomic: %v", err)
|
|
}
|
|
|
|
// 39 + 11 (10 demandés + 1 offert) = 50, retour exact au stock initial.
|
|
if got := productStock(t, productID); got != 50 {
|
|
t.Errorf("stock après annulation (bonus offert inclus dans le remboursement): got=%.2f want=50", got)
|
|
}
|
|
|
|
// Rejeu : ne doit rembourser qu'une fois.
|
|
if _, err := testDB.CancelCommandAtomic(cmd.ID, username, "test", false); err == nil {
|
|
t.Fatal("le second appel sur une commande déjà annulée doit échouer, pas rembourser une seconde fois")
|
|
}
|
|
if got := productStock(t, productID); got != 50 {
|
|
t.Errorf("stock après double annulation: got=%.2f want=50 (un seul remboursement)", got)
|
|
}
|
|
}
|
|
|
|
// ── Cumul avec les promotions de prix ───────────────────────────────────────
|
|
//
|
|
// Une offre "achetez X, Y offert" et une promotion de réduction (%) sur le
|
|
// même produit doivent pouvoir s'appliquer ensemble : la promotion réduit le
|
|
// prix facturé sur la quantité demandée, le cadeau ajoute de la quantité
|
|
// livrée sans toucher au prix — les deux mécanismes sont indépendants dans
|
|
// AddToBasket (voir db_basket.go) mais rien ne garantissait jusqu'ici qu'ils
|
|
// ne s'écrasent pas mutuellement une fois combinés.
|
|
func TestAddToBasket_FreeGiftAndPromotionBothApplyTogether(t *testing.T) {
|
|
cleanupStockTestData(t)
|
|
resetSettingsAfterTest(t)
|
|
username := newTestClient(t, "freegift_promo_combo")
|
|
productID := newTestProduct(t, "FreeGiftPromoCombo", 50)
|
|
// 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: 10, DiscountPercent: 20},
|
|
}
|
|
s.FreeGiftsEnabled = true
|
|
s.FreeGifts = []models.CategoryFreeGiftConfig{
|
|
{Category: "test", AllProducts: true, Tiers: []models.FreeGiftTier{
|
|
{BuyQuantity: 10, FreeQuantity: 1},
|
|
}},
|
|
}
|
|
if err := testDB.UpdateSettings(s); err != nil {
|
|
t.Fatalf("UpdateSettings: %v", err)
|
|
}
|
|
|
|
basket, err := testDB.AddToBasket(username, productID, 10)
|
|
if err != nil {
|
|
t.Fatalf("AddToBasket: %v", err)
|
|
}
|
|
if basket.Quantity != 11 {
|
|
t.Errorf("le cadeau doit s'appliquer malgré la promo active: got quantity=%.2f want=11", basket.Quantity)
|
|
}
|
|
if basket.Price != 8.0 {
|
|
t.Errorf("la promo doit s'appliquer malgré le cadeau actif: got price=%.2f want=8.00 (10€ - 20%%)", basket.Price)
|
|
}
|
|
}
|