@@ -175,3 +175,64 @@ func TestClaimMyReward_HTTPFlow_FailsAtomicallyWhenProductMissing(t *testing.T)
|
||||
t.Errorf("la récompense ne doit PAS être consommée si le produit est introuvable: got redeemed=%d want=0", redeemed["pool_0"])
|
||||
}
|
||||
}
|
||||
|
||||
// Une même catégorie peut avoir les deux types de récompense actifs en
|
||||
// parallèle (un lot de produits offerts + un lot de produits à -50%), chacun
|
||||
// avec sa propre sélection de produits (voir eligibleRewardProducts, qui
|
||||
// n'impose aucune unicité de catégorie dans CategoryConfigs). Un seul claim
|
||||
// doit alors ajouter les deux produits au panier, chacun tarifé selon son
|
||||
// propre type.
|
||||
func TestClaimMyReward_HTTPFlow_CategoryWithBothTypesSimultaneously(t *testing.T) {
|
||||
cleanupStockTestData(t)
|
||||
username := newTestClient(t, "reward_http_dual_type")
|
||||
freeProductID := newTestProduct(t, "RewardHTTPDualFree", 5)
|
||||
halfProductID := newTestProduct(t, "RewardHTTPDualHalf", 5)
|
||||
// newTestProduct crée les deux produits dans la catégorie "test", avec un
|
||||
// prix actif de 10.00€ pour quantity=1 (voir tests/main_test.go).
|
||||
|
||||
configureRewardSettings(t, &models.PointsReward{
|
||||
Threshold: 20,
|
||||
Description: "Un produit offert + un produit à -50%",
|
||||
CategoryConfigs: []models.RewardCategoryConfig{
|
||||
{Category: "test", Type: "free_product", ProductIDs: []int{freeProductID}},
|
||||
{Category: "test", Type: "half_price_product", ProductIDs: []int{halfProductID}},
|
||||
},
|
||||
RewardItems: []models.RewardItem{
|
||||
{ProductID: freeProductID, Quantity: 1, Price: 12},
|
||||
{ProductID: halfProductID, Quantity: 1, Price: 12},
|
||||
},
|
||||
})
|
||||
setClientPoolPoints(t, username, "pool_0", 20)
|
||||
|
||||
body, _ := json.Marshal(map[string]string{"pool_key": "pool_0"})
|
||||
c, rec := claimRewardContext(username, body)
|
||||
handlers.ClaimMyReward(c)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status HTTP: got=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
rows := basketRewardItems(t, username)
|
||||
if len(rows) != 2 {
|
||||
t.Fatalf("les deux produits récompense doivent être dans le panier: %+v", rows)
|
||||
}
|
||||
|
||||
var freeRow, halfRow *rewardBasketRow
|
||||
for i := range rows {
|
||||
switch rows[i].ProductID {
|
||||
case freeProductID:
|
||||
freeRow = &rows[i]
|
||||
case halfProductID:
|
||||
halfRow = &rows[i]
|
||||
}
|
||||
}
|
||||
if freeRow == nil || halfRow == nil {
|
||||
t.Fatalf("les deux produits attendus doivent être présents: %+v", rows)
|
||||
}
|
||||
if freeRow.Price != 0 {
|
||||
t.Errorf("produit de la config free_product: le prix en panier doit être 0: got=%.2f", freeRow.Price)
|
||||
}
|
||||
if halfRow.Price != 5.0 {
|
||||
t.Errorf("produit de la config half_price_product: prix attendu = 50%% de 10.00€ = 5.00€: got=%.2f", halfRow.Price)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"gestion/db"
|
||||
"gestion/models"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -40,6 +41,41 @@ func basketRewardItems(t *testing.T, username string) []rewardBasketRow {
|
||||
return rows
|
||||
}
|
||||
|
||||
// Désactiver la récompense (PointsReward = nil) puis sauvegarder ne doit pas
|
||||
// la faire réapparaître activée au rechargement — régression : json.Marshal
|
||||
// d'un pointeur nil produit la chaîne "null", et json.Unmarshal d'un null
|
||||
// JSON dans une valeur non-pointeur est un no-op sans erreur, ce qui laissait
|
||||
// settings.PointsReward pointer vers une struct vide mais non-nil.
|
||||
func TestUpdateSettings_DisablingPointsRewardPersistsAsNil(t *testing.T) {
|
||||
settings := db.DefaultSettings()
|
||||
settings.PointsReward = &models.PointsReward{
|
||||
Threshold: 20,
|
||||
Description: "Un produit offert",
|
||||
}
|
||||
if err := testDB.UpdateSettings(settings); err != nil {
|
||||
t.Fatalf("UpdateSettings (activation): %v", err)
|
||||
}
|
||||
loaded, err := testDB.GetSettings()
|
||||
if err != nil {
|
||||
t.Fatalf("GetSettings (activation): %v", err)
|
||||
}
|
||||
if loaded.PointsReward == nil {
|
||||
t.Fatal("la récompense devrait être active après la première sauvegarde")
|
||||
}
|
||||
|
||||
settings.PointsReward = nil
|
||||
if err := testDB.UpdateSettings(settings); 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.PointsReward != nil {
|
||||
t.Errorf("la récompense désactivée ne doit pas réapparaître après sauvegarde: got=%+v", loaded.PointsReward)
|
||||
}
|
||||
}
|
||||
|
||||
// ── ClaimPoolReward : seuil, atomicité, épuisement ──────────────────────────
|
||||
|
||||
func TestClaimPoolReward_BelowThresholdFails(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user