chore: build
Backend - Build & Lint / build (push) Successful in 33m53s
Frontend Web - Build & Lint / build (push) Successful in 15m41s

This commit is contained in:
Xor290
2026-08-25 21:26:02 +02:00
parent 5ee979fcc3
commit d8073bdc46
4 changed files with 196 additions and 167 deletions
+80 -37
View File
@@ -25,9 +25,9 @@ func claimRewardContext(username string, body []byte) (*gin.Context, *httptest.R
}
// configureRewardSettings applique la récompense donnée, avec pool_0 mappé
// sur la catégorie "test" — nécessaire pour que eligibleRewardProducts
// sur la catégorie "test" — nécessaire pour que resolveCategoryRewardCandidates
// (qui croise pool.Categories et reward.CategoryConfigs) considère les
// reward_items comme éligibles.
// produits de la catégorie comme éligibles.
func configureRewardSettings(t *testing.T, reward *models.PointsReward) {
t.Helper()
settings := db.DefaultSettings()
@@ -39,17 +39,20 @@ func configureRewardSettings(t *testing.T, reward *models.PointsReward) {
}
// Flux complet réel : POST /points/claim avec un seuil atteint doit ajouter
// le produit récompense configuré au panier et décompter la récompense.
// le produit récompense configuré au panier et décompter la récompense. Le
// produit éligible et sa quantité sont désormais définis directement dans le
// bloc catégorie (RewardCategoryConfig), plus de liste "reward_items" à part.
func TestClaimMyReward_HTTPFlow_AddsRewardToBasketAndDecrementsAvailable(t *testing.T) {
cleanupStockTestData(t)
username := newTestClient(t, "reward_http_flow")
rewardProductID := newTestProduct(t, "RewardHTTPFlow", 5)
configureRewardSettings(t, &models.PointsReward{
Threshold: 20,
Description: "Un produit offert",
CategoryConfigs: []models.RewardCategoryConfig{{Category: "test", Type: "free_product", AllProducts: true}},
RewardItems: []models.RewardItem{{ProductID: rewardProductID, Quantity: 1, Price: 12}},
Threshold: 20,
Description: "Un produit offert",
CategoryConfigs: []models.RewardCategoryConfig{
{Category: "test", Type: "free_product", AllProducts: true, Quantity: 1},
},
})
setClientPoolPoints(t, username, "pool_0", 20)
@@ -85,9 +88,9 @@ func TestClaimMyReward_HTTPFlow_AddsRewardToBasketAndDecrementsAvailable(t *test
}
}
// Catégorie configurée en "half_price_product" : le produit récompense doit
// être ajouté au panier à 50% du prix catalogue actif (pas 0€, pas le prix
// indicatif RewardItem.Price saisi par l'admin).
// Catégorie configurée en "half_price_product" avec quantité=1 : le produit
// récompense doit être ajouté au panier à 50% du prix catalogue actif pour
// cette quantité (palier ≤ 1), pas 0€.
func TestClaimMyReward_HTTPFlow_HalfPriceCategoryChargesFiftyPercentOfCatalogPrice(t *testing.T) {
cleanupStockTestData(t)
username := newTestClient(t, "reward_http_halfprice")
@@ -95,10 +98,11 @@ func TestClaimMyReward_HTTPFlow_HalfPriceCategoryChargesFiftyPercentOfCatalogPri
// newTestProduct crée un prix actif de 10.00€ pour quantity=1 (voir tests/main_test.go).
configureRewardSettings(t, &models.PointsReward{
Threshold: 20,
Description: "Un produit à moitié prix",
CategoryConfigs: []models.RewardCategoryConfig{{Category: "test", Type: "half_price_product", AllProducts: true}},
RewardItems: []models.RewardItem{{ProductID: rewardProductID, Quantity: 1, Price: 999}}, // Price indicatif, doit être ignoré
Threshold: 20,
Description: "Un produit à moitié prix",
CategoryConfigs: []models.RewardCategoryConfig{
{Category: "test", Type: "half_price_product", AllProducts: true, Quantity: 1},
},
})
setClientPoolPoints(t, username, "pool_0", 20)
@@ -119,15 +123,60 @@ func TestClaimMyReward_HTTPFlow_HalfPriceCategoryChargesFiftyPercentOfCatalogPri
}
}
// La quantité configurée dans le bloc catégorie détermine le palier de prix
// utilisé pour le calcul du -50% (ex: 30€ le palier quantity=1 → 15€ facturé),
// pas un prix indicatif saisi ailleurs.
func TestClaimMyReward_HTTPFlow_HalfPriceUsesConfiguredQuantityForPriceTier(t *testing.T) {
cleanupStockTestData(t)
username := newTestClient(t, "reward_http_halfprice_qty")
rewardProductID := newTestProduct(t, "RewardHTTPHalfPriceQty", 20)
// Ajoute un palier quantity=3 à 30€ (en plus du palier quantity=1 à 10€ créé par newTestProduct).
if err := testDB.GDB.Exec(
`INSERT INTO product_prices (product_id, quantity, price, active_price) VALUES (?, 3, 30.00, true)`,
rewardProductID,
).Error; err != nil {
t.Fatalf("création palier de prix supplémentaire: %v", err)
}
configureRewardSettings(t, &models.PointsReward{
Threshold: 20,
Description: "Un produit à moitié prix, quantité 3",
CategoryConfigs: []models.RewardCategoryConfig{
{Category: "test", Type: "half_price_product", AllProducts: true, Quantity: 3},
},
})
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) != 1 || rows[0].ProductID != rewardProductID {
t.Fatalf("le produit récompense doit être dans le panier: %+v", rows)
}
if rows[0].Quantity != 3 {
t.Errorf("la quantité en panier doit être celle configurée pour la catégorie: got=%.2f want=3", rows[0].Quantity)
}
if rows[0].Price != 15.0 {
t.Errorf("palier quantity=3 à 30€ : prix attendu = 50%% = 15.00€: got=%.2f", rows[0].Price)
}
}
func TestClaimMyReward_HTTPFlow_RejectsWhenBelowThreshold(t *testing.T) {
cleanupStockTestData(t)
username := newTestClient(t, "reward_http_below")
rewardProductID := newTestProduct(t, "RewardHTTPBelow", 5)
newTestProduct(t, "RewardHTTPBelow", 5)
configureRewardSettings(t, &models.PointsReward{
Threshold: 20,
CategoryConfigs: []models.RewardCategoryConfig{{Category: "test", Type: "free_product", AllProducts: true}},
RewardItems: []models.RewardItem{{ProductID: rewardProductID, Quantity: 1, Price: 12}},
Threshold: 20,
CategoryConfigs: []models.RewardCategoryConfig{
{Category: "test", Type: "free_product", AllProducts: true, Quantity: 1},
},
})
setClientPoolPoints(t, username, "pool_0", 5)
@@ -140,22 +189,21 @@ func TestClaimMyReward_HTTPFlow_RejectsWhenBelowThreshold(t *testing.T) {
}
}
// Si un item récompense configuré par l'admin pointe vers un produit
// supprimé/inexistant, la réclamation entière doit échouer — la récompense
// ne doit pas être consommée sans qu'aucun produit ne soit livré au client
// (ClaimPoolReward + AddRewardsToBasket sont maintenant dans la même
// transaction via ClaimPoolRewardAndAddToBasket).
// Si un produit configuré par l'admin (via ProductIDs explicite) pointe vers
// un produit supprimé/inexistant, la réclamation entière doit échouer — la
// récompense ne doit pas être consommée sans qu'aucun produit ne soit livré
// au client (ClaimPoolReward + AddRewardsToBasket sont dans la même
// transaction via ClaimPoolRewardAndAddToBasket ; la contrainte de clé
// étrangère sur baskets.product_id fait échouer l'insertion).
func TestClaimMyReward_HTTPFlow_FailsAtomicallyWhenProductMissing(t *testing.T) {
cleanupStockTestData(t)
username := newTestClient(t, "reward_http_missing_product")
configureRewardSettings(t, &models.PointsReward{
Threshold: 20,
// ProductIDs explicite (pas AllProducts) : le produit n'existe pas en
// base, donc il n'apparaîtrait jamais dans productCategories et ne
// serait jamais éligible via une correspondance AllProducts.
CategoryConfigs: []models.RewardCategoryConfig{{Category: "test", Type: "free_product", ProductIDs: []int{999999999}}},
RewardItems: []models.RewardItem{{ProductID: 999999999, Quantity: 1, Price: 12}}, // produit inexistant
CategoryConfigs: []models.RewardCategoryConfig{
{Category: "test", Type: "free_product", ProductIDs: []int{999999999}, Quantity: 1},
},
})
setClientPoolPoints(t, username, "pool_0", 20)
@@ -178,10 +226,9 @@ func TestClaimMyReward_HTTPFlow_FailsAtomicallyWhenProductMissing(t *testing.T)
// 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
// avec sa propre sélection de produits et sa propre quantité. Un seul claim
// doit alors ajouter les deux produits au panier, chacun tarifé selon son
// propre type.
// propre type et sa propre quantité.
func TestClaimMyReward_HTTPFlow_CategoryWithBothTypesSimultaneously(t *testing.T) {
cleanupStockTestData(t)
username := newTestClient(t, "reward_http_dual_type")
@@ -194,12 +241,8 @@ func TestClaimMyReward_HTTPFlow_CategoryWithBothTypesSimultaneously(t *testing.T
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},
{Category: "test", Type: "free_product", ProductIDs: []int{freeProductID}, Quantity: 1},
{Category: "test", Type: "half_price_product", ProductIDs: []int{halfProductID}, Quantity: 1},
},
})
setClientPoolPoints(t, username, "pool_0", 20)