282 lines
10 KiB
Go
282 lines
10 KiB
Go
package tests
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"gestion/db"
|
|
"gestion/handlers"
|
|
"gestion/models"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func claimRewardContext(username string, body []byte) (*gin.Context, *httptest.ResponseRecorder) {
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/points/claim", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(rec)
|
|
c.Request = req
|
|
c.Set("database", testDB)
|
|
c.Set("username", username)
|
|
return c, rec
|
|
}
|
|
|
|
// configureRewardSettings applique la récompense donnée, avec pool_0 mappé
|
|
// sur la catégorie "test" — nécessaire pour que resolveCategoryRewardCandidates
|
|
// (qui croise pool.Categories et reward.CategoryConfigs) considère les
|
|
// produits de la catégorie comme éligibles.
|
|
func configureRewardSettings(t *testing.T, reward *models.PointsReward) {
|
|
t.Helper()
|
|
settings := db.DefaultSettings()
|
|
settings.PointsPools[0].Categories = []string{"test"}
|
|
settings.PointsReward = reward
|
|
if err := testDB.UpdateSettings(settings); err != nil {
|
|
t.Fatalf("UpdateSettings: %v", err)
|
|
}
|
|
}
|
|
|
|
// 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 é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, Quantity: 1},
|
|
},
|
|
})
|
|
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())
|
|
}
|
|
|
|
var resp struct {
|
|
Success bool `json:"success"`
|
|
RemainingRewards int `json:"remaining_rewards"`
|
|
ProductAdded bool `json:"product_added"`
|
|
}
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("décodage réponse: %v body=%s", err, rec.Body.String())
|
|
}
|
|
if !resp.Success || !resp.ProductAdded {
|
|
t.Fatalf("réclamation devrait réussir avec produit ajouté: %+v", resp)
|
|
}
|
|
if resp.RemainingRewards != 0 {
|
|
t.Errorf("remaining_rewards: got=%d want=0", resp.RemainingRewards)
|
|
}
|
|
|
|
rows := basketRewardItems(t, username)
|
|
if len(rows) != 1 || rows[0].ProductID != rewardProductID {
|
|
t.Errorf("le produit récompense doit être dans le panier: %+v", rows)
|
|
}
|
|
if rows[0].Price != 0 {
|
|
t.Errorf("catégorie free_product: le prix en panier doit être 0: got=%.2f", rows[0].Price)
|
|
}
|
|
}
|
|
|
|
// 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")
|
|
rewardProductID := newTestProduct(t, "RewardHTTPHalfPrice", 5)
|
|
// 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, Quantity: 1},
|
|
},
|
|
})
|
|
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].Price != 5.0 {
|
|
t.Errorf("catégorie half_price_product: prix attendu = 50%% de 10.00€ = 5.00€: got=%.2f", rows[0].Price)
|
|
}
|
|
}
|
|
|
|
// 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")
|
|
newTestProduct(t, "RewardHTTPBelow", 5)
|
|
|
|
configureRewardSettings(t, &models.PointsReward{
|
|
Threshold: 20,
|
|
CategoryConfigs: []models.RewardCategoryConfig{
|
|
{Category: "test", Type: "free_product", AllProducts: true, Quantity: 1},
|
|
},
|
|
})
|
|
setClientPoolPoints(t, username, "pool_0", 5)
|
|
|
|
body, _ := json.Marshal(map[string]string{"pool_key": "pool_0"})
|
|
c, rec := claimRewardContext(username, body)
|
|
handlers.ClaimMyReward(c)
|
|
|
|
if rec.Code != http.StatusConflict {
|
|
t.Fatalf("status HTTP: got=%d want=%d body=%s", rec.Code, http.StatusConflict, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
// 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,
|
|
CategoryConfigs: []models.RewardCategoryConfig{
|
|
{Category: "test", Type: "free_product", ProductIDs: []int{999999999}, Quantity: 1},
|
|
},
|
|
})
|
|
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("la réclamation ne doit pas réussir avec un produit récompense invalide, body=%s", rec.Body.String())
|
|
}
|
|
|
|
_, redeemed, err := testDB.GetClientPointsAndRewards(username)
|
|
if err != nil {
|
|
t.Fatalf("GetClientPointsAndRewards: %v", err)
|
|
}
|
|
if redeemed["pool_0"] != 0 {
|
|
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 et sa propre quantité. Un seul claim
|
|
// doit alors ajouter les deux produits au panier, chacun tarifé selon son
|
|
// propre type et sa propre quantité.
|
|
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}, Quantity: 1},
|
|
{Category: "test", Type: "half_price_product", ProductIDs: []int{halfProductID}, Quantity: 1},
|
|
},
|
|
})
|
|
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)
|
|
}
|
|
}
|