chore: build
Backend - Build & Lint / build (push) Canceled after 25s

This commit is contained in:
Xor290
2026-08-30 15:13:38 +02:00
parent 96e7d33739
commit a35031b2a1
3 changed files with 111 additions and 29 deletions
+34 -20
View File
@@ -64,14 +64,18 @@ func resolveCategoryRewardCandidates(database *db.Database, reward *models.Point
Category: cfg.Category, Type: rewardType, ProductID: p.ID, Name: p.Name, Quantity: cfg.Quantity, Category: cfg.Category, Type: rewardType, ProductID: p.ID, Name: p.Name, Quantity: cfg.Quantity,
}) })
} }
} else if len(cfg.ProductIDs) > 0 { } else if len(cfg.Products) > 0 {
names, err := database.GetProductNamesByIDs(cfg.ProductIDs) ids := make([]int, len(cfg.Products))
for i, pq := range cfg.Products {
ids[i] = pq.ProductID
}
names, err := database.GetProductNamesByIDs(ids)
if err != nil { if err != nil {
return nil, fmt.Errorf("noms produits catégorie %q: %w", cfg.Category, err) return nil, fmt.Errorf("noms produits catégorie %q: %w", cfg.Category, err)
} }
for _, pid := range cfg.ProductIDs { for _, pq := range cfg.Products {
candidates = append(candidates, categoryRewardCandidate{ candidates = append(candidates, categoryRewardCandidate{
Category: cfg.Category, Type: rewardType, ProductID: pid, Name: names[pid], Quantity: cfg.Quantity, Category: cfg.Category, Type: rewardType, ProductID: pq.ProductID, Name: names[pq.ProductID], Quantity: pq.Quantity,
}) })
} }
} }
@@ -126,13 +130,18 @@ func GetMyPointsRewards(c *gin.Context) {
reward := settings.PointsReward reward := settings.PointsReward
type ConfigProductResponse struct {
ProductID int `json:"product_id"`
ProductName string `json:"product_name"`
Quantity float64 `json:"quantity"`
}
type EligibleConfigResponse struct { type EligibleConfigResponse struct {
Category string `json:"category"` Category string `json:"category"`
Type string `json:"type"` Type string `json:"type"`
AllProducts bool `json:"all_products"` AllProducts bool `json:"all_products"`
ProductIDs []int `json:"product_ids"` Products []ConfigProductResponse `json:"products"`
ProductNames []string `json:"product_names"` Quantity float64 `json:"quantity"`
Quantity float64 `json:"quantity"`
} }
type RewardItemResponse struct { type RewardItemResponse struct {
@@ -181,22 +190,27 @@ func GetMyPointsRewards(c *gin.Context) {
if !poolCats[cfg.Category] { if !poolCats[cfg.Category] {
continue continue
} }
names := make([]string, 0, len(cfg.ProductIDs)) products := make([]ConfigProductResponse, 0, len(cfg.Products))
for _, pid := range cfg.ProductIDs { for _, pq := range cfg.Products {
name := ""
for _, cand := range candidates { for _, cand := range candidates {
if cand.ProductID == pid && cand.Category == cfg.Category { if cand.ProductID == pq.ProductID && cand.Category == cfg.Category {
names = append(names, cand.Name) name = cand.Name
break break
} }
} }
products = append(products, ConfigProductResponse{
ProductID: pq.ProductID,
ProductName: name,
Quantity: pq.Quantity,
})
} }
eligibleConfigs = append(eligibleConfigs, EligibleConfigResponse{ eligibleConfigs = append(eligibleConfigs, EligibleConfigResponse{
Category: cfg.Category, Category: cfg.Category,
Type: normalizeRewardCategoryType(cfg.Type), Type: normalizeRewardCategoryType(cfg.Type),
AllProducts: cfg.AllProducts, AllProducts: cfg.AllProducts,
ProductIDs: cfg.ProductIDs, Products: products,
ProductNames: names, Quantity: cfg.Quantity,
Quantity: cfg.Quantity,
}) })
} }
} }
+18 -5
View File
@@ -14,17 +14,30 @@ type PointsTier struct {
Points int `json:"points"` Points int `json:"points"`
} }
// RewardProductQuantity associe un produit à sa propre quantité offerte / à
// -50%, pour le cas où une catégorie n'est pas configurée en "tous les
// produits" — ex: produit A à 2g offerts, produit B à 1g offert, tous deux
// dans la même catégorie et le même type de récompense.
type RewardProductQuantity struct {
ProductID int `json:"product_id"`
Quantity float64 `json:"quantity"`
}
// RewardCategoryConfig définit les produits éligibles dans une catégorie pour une récompense, // RewardCategoryConfig définit les produits éligibles dans une catégorie pour une récompense,
// le type de récompense appliqué pour cette catégorie précise, et la quantité // le type de récompense appliqué pour cette catégorie précise, et la quantité
// concernée (ex: 1g offert, ou 2g à -50%) — la quantité correspond au palier // concernée (ex: 1g offert, ou 2g à -50%) — la quantité correspond au palier
// de prix catalogue du produit (voir GetActiveProductPrice), pas une valeur // de prix catalogue du produit (voir GetActiveProductPrice), pas une valeur
// libre : ex. "30€ offert = 1g" si le produit a un palier quantity=1 à 30€. // libre : ex. "30€ offert = 1g" si le produit a un palier quantity=1 à 30€.
//
// Si AllProducts = true, Quantity s'applique uniformément à tous les produits
// de la catégorie. Si AllProducts = false, chaque produit sélectionné dans
// Products a sa propre quantité (Quantity au niveau catégorie est alors ignoré).
type RewardCategoryConfig struct { type RewardCategoryConfig struct {
Category string `json:"category"` // nom de la catégorie Category string `json:"category"` // nom de la catégorie
Type string `json:"type"` // "free_product" (défaut) | "half_price_product" Type string `json:"type"` // "free_product" (défaut) | "half_price_product"
AllProducts bool `json:"all_products"` // true = tous les produits de la catégorie AllProducts bool `json:"all_products"` // true = tous les produits de la catégorie
ProductIDs []int `json:"product_ids"` // IDs des produits éligibles si AllProducts = false Quantity float64 `json:"quantity"` // quantité uniforme si AllProducts = true
Quantity float64 `json:"quantity"` // quantité offerte / à -50% pour ce type dans cette catégorie Products []RewardProductQuantity `json:"products"` // produits + quantité individuelle si AllProducts = false
} }
// RewardItem représente un produit résolu à ajouter au panier lors d'un // RewardItem représente un produit résolu à ajouter au panier lors d'un
+59 -4
View File
@@ -189,7 +189,7 @@ func TestClaimMyReward_HTTPFlow_RejectsWhenBelowThreshold(t *testing.T) {
} }
} }
// Si un produit configuré par l'admin (via ProductIDs explicite) pointe vers // Si un produit configuré par l'admin (via Products explicite) pointe vers
// un produit supprimé/inexistant, la réclamation entière doit échouer — la // 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é // récompense ne doit pas être consommée sans qu'aucun produit ne soit livré
// au client (ClaimPoolReward + AddRewardsToBasket sont dans la même // au client (ClaimPoolReward + AddRewardsToBasket sont dans la même
@@ -202,7 +202,7 @@ func TestClaimMyReward_HTTPFlow_FailsAtomicallyWhenProductMissing(t *testing.T)
configureRewardSettings(t, &models.PointsReward{ configureRewardSettings(t, &models.PointsReward{
Threshold: 20, Threshold: 20,
CategoryConfigs: []models.RewardCategoryConfig{ CategoryConfigs: []models.RewardCategoryConfig{
{Category: "test", Type: "free_product", ProductIDs: []int{999999999}, Quantity: 1}, {Category: "test", Type: "free_product", Products: []models.RewardProductQuantity{{ProductID: 999999999, Quantity: 1}}},
}, },
}) })
setClientPoolPoints(t, username, "pool_0", 20) setClientPoolPoints(t, username, "pool_0", 20)
@@ -241,8 +241,8 @@ func TestClaimMyReward_HTTPFlow_CategoryWithBothTypesSimultaneously(t *testing.T
Threshold: 20, Threshold: 20,
Description: "Un produit offert + un produit à -50%", Description: "Un produit offert + un produit à -50%",
CategoryConfigs: []models.RewardCategoryConfig{ CategoryConfigs: []models.RewardCategoryConfig{
{Category: "test", Type: "free_product", ProductIDs: []int{freeProductID}, Quantity: 1}, {Category: "test", Type: "free_product", Products: []models.RewardProductQuantity{{ProductID: freeProductID, Quantity: 1}}},
{Category: "test", Type: "half_price_product", ProductIDs: []int{halfProductID}, Quantity: 1}, {Category: "test", Type: "half_price_product", Products: []models.RewardProductQuantity{{ProductID: halfProductID, Quantity: 1}}},
}, },
}) })
setClientPoolPoints(t, username, "pool_0", 20) setClientPoolPoints(t, username, "pool_0", 20)
@@ -279,3 +279,58 @@ func TestClaimMyReward_HTTPFlow_CategoryWithBothTypesSimultaneously(t *testing.T
t.Errorf("produit de la config half_price_product: prix attendu = 50%% de 10.00€ = 5.00€: got=%.2f", halfRow.Price) t.Errorf("produit de la config half_price_product: prix attendu = 50%% de 10.00€ = 5.00€: got=%.2f", halfRow.Price)
} }
} }
// Quand une catégorie n'est pas configurée en "tous les produits", chaque
// produit sélectionné a sa propre quantité (ex: produit A à 2g offerts,
// produit B à 1g offert, tous deux dans la même catégorie et le même type).
func TestClaimMyReward_HTTPFlow_PerProductQuantityWithinSameCategoryAndType(t *testing.T) {
cleanupStockTestData(t)
username := newTestClient(t, "reward_http_per_product_qty")
productA := newTestProduct(t, "RewardHTTPPerProductA", 5)
productB := newTestProduct(t, "RewardHTTPPerProductB", 5)
// newTestProduct crée les deux produits dans la catégorie "test".
configureRewardSettings(t, &models.PointsReward{
Threshold: 20,
Description: "Produit A 2g offert, produit B 1g offert",
CategoryConfigs: []models.RewardCategoryConfig{
{Category: "test", Type: "free_product", Products: []models.RewardProductQuantity{
{ProductID: productA, Quantity: 2},
{ProductID: productB, 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 rowA, rowB *rewardBasketRow
for i := range rows {
switch rows[i].ProductID {
case productA:
rowA = &rows[i]
case productB:
rowB = &rows[i]
}
}
if rowA == nil || rowB == nil {
t.Fatalf("les deux produits attendus doivent être présents: %+v", rows)
}
if rowA.Quantity != 2 {
t.Errorf("produit A: quantité attendue=2, got=%.2f", rowA.Quantity)
}
if rowB.Quantity != 1 {
t.Errorf("produit B: quantité attendue=1, got=%.2f", rowB.Quantity)
}
}