chore: build
Backend - Build & Lint / build (push) Successful in 32m38s
Frontend Admin - EAS Build / build (push) Successful in 1h34m28s
Frontend Client - EAS Build / build (push) Successful in 1h30m42s
Frontend Web - Build & Lint / build (push) Canceled after 12m2s

This commit is contained in:
Xor290
2026-09-13 16:11:30 +02:00
parent f2f537a194
commit d7d7496a66
12 changed files with 631 additions and 52 deletions
+183
View File
@@ -210,3 +210,186 @@ func TestAddToBasket_FreeGiftRejectedWhenStockInsufficientForBonus(t *testing.T)
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)
}
}