diff --git a/backend/gestion/db/db_product.go b/backend/gestion/db/db_product.go index cca77256..093ff385 100644 --- a/backend/gestion/db/db_product.go +++ b/backend/gestion/db/db_product.go @@ -143,6 +143,27 @@ func (d *Database) GetProductNamesByIDs(ids []int) (map[int]string, error) { return result, nil } +// GetProductCategoriesByIDs retourne un map id→category pour une liste d'IDs. +func (d *Database) GetProductCategoriesByIDs(ids []int) (map[int]string, error) { + result := make(map[int]string, len(ids)) + if len(ids) == 0 { + return result, nil + } + rows, err := d.GDB.Raw(`SELECT id, category FROM products WHERE id IN ?`, ids).Rows() + if err != nil { + return result, err + } + defer rows.Close() + for rows.Next() { + var id int + var category string + if err := rows.Scan(&id, &category); err == nil { + result[id] = category + } + } + return result, nil +} + func (d *Database) GetAllProducts() ([]models.Product, error) { log.Println("📦 [GetAllProducts] START") diff --git a/backend/gestion/handlers/points.go b/backend/gestion/handlers/points.go index ff15ca13..d48978f6 100644 --- a/backend/gestion/handlers/points.go +++ b/backend/gestion/handlers/points.go @@ -11,6 +11,34 @@ import ( "github.com/gin-gonic/gin" ) +// eligibleRewardProductIDs détermine, pour un pool donné, quels product_id de +// reward.RewardItems sont éligibles : sa catégorie (via CategoryConfigs) doit +// faire partie des catégories du pool, soit par whitelist explicite (ProductIDs) +// soit par correspondance de catégorie produit (AllProducts). +func eligibleRewardProductIDs(reward *models.PointsReward, poolCategories map[string]bool, productCategories map[int]string) map[int]bool { + eligible := make(map[int]bool) + if reward == nil { + return eligible + } + for _, cfg := range reward.CategoryConfigs { + if !poolCategories[cfg.Category] { + continue + } + if cfg.AllProducts { + for pid, cat := range productCategories { + if cat == cfg.Category { + eligible[pid] = true + } + } + } else { + for _, pid := range cfg.ProductIDs { + eligible[pid] = true + } + } + } + return eligible +} + // GetMyPointsRewards retourne les points et les récompenses disponibles du client connecté. // La récompense est globale : son seuil s'applique indépendamment à chaque pool. func GetMyPointsRewards(c *gin.Context) { @@ -48,14 +76,22 @@ func GetMyPointsRewards(c *gin.Context) { ProductNames []string `json:"product_names"` } + type RewardItemResponse struct { + ProductID int `json:"product_id"` + ProductName string `json:"product_name"` + Quantity float64 `json:"quantity"` + Price float64 `json:"price"` + } + type PoolInfo struct { - Key string `json:"key"` - Name string `json:"name"` - Points int `json:"points"` - RewardsEarned int `json:"rewards_earned"` - RewardsClaimed int `json:"rewards_claimed"` - RewardsAvailable int `json:"rewards_available"` - EligibleConfigs []EligibleConfigResponse `json:"eligible_configs"` + Key string `json:"key"` + Name string `json:"name"` + Points int `json:"points"` + RewardsEarned int `json:"rewards_earned"` + RewardsClaimed int `json:"rewards_claimed"` + RewardsAvailable int `json:"rewards_available"` + EligibleConfigs []EligibleConfigResponse `json:"eligible_configs"` + EligibleRewardItems []RewardItemResponse `json:"eligible_reward_items"` } // Collecter tous les product_ids nécessaires en un seul passage @@ -73,6 +109,7 @@ func GetMyPointsRewards(c *gin.Context) { } } productNames, _ := database.GetProductNamesByIDs(allProductIDs) + productCategories, _ := database.GetProductCategoriesByIDs(allProductIDs) pools := make([]PoolInfo, 0, len(settings.PointsPools)) for _, pool := range settings.PointsPools { @@ -111,24 +148,35 @@ func GetMyPointsRewards(c *gin.Context) { } } + eligibleProductIDs := eligibleRewardProductIDs(reward, poolCats, productCategories) + eligibleRewardItems := make([]RewardItemResponse, 0) + if reward != nil { + for _, item := range reward.RewardItems { + if !eligibleProductIDs[item.ProductID] { + continue + } + eligibleRewardItems = append(eligibleRewardItems, RewardItemResponse{ + ProductID: item.ProductID, + ProductName: productNames[item.ProductID], + Quantity: item.Quantity, + Price: item.Price, + }) + } + } + pools = append(pools, PoolInfo{ - Key: pool.Key, - Name: pool.Name, - Points: pts, - RewardsEarned: earned, - RewardsClaimed: redeemed, - RewardsAvailable: available, - EligibleConfigs: eligibleConfigs, + Key: pool.Key, + Name: pool.Name, + Points: pts, + RewardsEarned: earned, + RewardsClaimed: redeemed, + RewardsAvailable: available, + EligibleConfigs: eligibleConfigs, + EligibleRewardItems: eligibleRewardItems, }) } // Construire la liste des produits récompense avec leurs noms - type RewardItemResponse struct { - ProductID int `json:"product_id"` - ProductName string `json:"product_name"` - Quantity float64 `json:"quantity"` - Price float64 `json:"price"` - } var rewardMeta gin.H if reward != nil { rewardItems := make([]RewardItemResponse, 0, len(reward.RewardItems)) @@ -191,27 +239,62 @@ func ClaimMyReward(c *gin.Context) { return } - // Vérifier que le pool existe - poolExists := false - for _, p := range settings.PointsPools { - if p.Key == req.PoolKey { - poolExists = true + // Vérifier que le pool existe et récupérer ses catégories + var selectedPool *models.PointsPool + for i := range settings.PointsPools { + if settings.PointsPools[i].Key == req.PoolKey { + selectedPool = &settings.PointsPools[i] break } } - if !poolExists { + if selectedPool == nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Pool introuvable"}) return } - itemsToAdd := reward.RewardItems - if req.ProductID > 0 && len(reward.RewardItems) > 1 { - for _, item := range reward.RewardItems { + // Un produit récompense n'est éligible pour ce pool que si sa catégorie + // fait partie des catégories du pool (via CategoryConfigs) — sans ce + // filtre, un client pourrait réclamer n'importe quel produit récompense + // (toutes catégories confondues) avec les points d'un pool quelconque. + poolCategories := make(map[string]bool, len(selectedPool.Categories)) + for _, cat := range selectedPool.Categories { + poolCategories[cat] = true + } + + rewardProductIDs := make([]int, 0, len(reward.RewardItems)) + for _, item := range reward.RewardItems { + if item.ProductID > 0 { + rewardProductIDs = append(rewardProductIDs, item.ProductID) + } + } + productCategories, err := database.GetProductCategoriesByIDs(rewardProductIDs) + if err != nil { + utils.ServerErr(c, "Erreur lecture catégories produits", err) + return + } + + eligibleProductIDs := eligibleRewardProductIDs(reward, poolCategories, productCategories) + + eligibleItems := make([]models.RewardItem, 0, len(reward.RewardItems)) + for _, item := range reward.RewardItems { + if eligibleProductIDs[item.ProductID] { + eligibleItems = append(eligibleItems, item) + } + } + + itemsToAdd := eligibleItems + if req.ProductID > 0 { + itemsToAdd = nil + for _, item := range eligibleItems { if item.ProductID == req.ProductID { itemsToAdd = []models.RewardItem{item} break } } + if itemsToAdd == nil { + c.JSON(http.StatusForbidden, gin.H{"error": "Ce produit n'est pas éligible pour cette récompense"}) + return + } } remaining, added, err := database.ClaimPoolRewardAndAddToBasket(username, req.PoolKey, reward.Threshold, itemsToAdd) diff --git a/frontend-prep/src/api/api.ts b/frontend-prep/src/api/api.ts index bf2165ea..5f0b7d22 100644 --- a/frontend-prep/src/api/api.ts +++ b/frontend-prep/src/api/api.ts @@ -2172,6 +2172,13 @@ export type RewardCategoryConfig = { amount: number; }; +export type RewardItemConfig = { + product_id: number; + product_name: string; + quantity: number; + price: number; +}; + export type PointsPoolInfo = { key: string; name: string; @@ -2180,13 +2187,7 @@ export type PointsPoolInfo = { rewards_claimed: number; rewards_available: number; eligible_configs: RewardCategoryConfig[]; -}; - -export type RewardItemConfig = { - product_id: number; - product_name: string; - quantity: number; - price: number; + eligible_reward_items: RewardItemConfig[]; }; export type PointsRewardConfig = { diff --git a/frontend-prep/src/pages/User/ConsultationHistorique.tsx b/frontend-prep/src/pages/User/ConsultationHistorique.tsx index 5c3515d6..cce6b954 100644 --- a/frontend-prep/src/pages/User/ConsultationHistorique.tsx +++ b/frontend-prep/src/pages/User/ConsultationHistorique.tsx @@ -180,7 +180,7 @@ function ConsultationHistorique() { // Ouvre la modal de choix si plusieurs produits sont éligibles, // sinon réclame directement (1 seul produit configuré ou aucun item). const openRewardModal = (pool: PointsPoolInfo) => { - const items = pointsRewards?.reward?.reward_items ?? []; + const items = pool.eligible_reward_items ?? []; if (items.length > 1) { setSelectedProductId(items[0]?.product_id ?? null); setRewardModalPool(pool); @@ -716,7 +716,7 @@ function ConsultationHistorique() {

- {(pointsRewards.reward.reward_items ?? []).map( + {(rewardModalPool.eligible_reward_items ?? []).map( (item) => { const isSelected = selectedProductId === item.product_id; diff --git a/mobile/src/api/api.ts b/mobile/src/api/api.ts index 9d1c6e70..5cf050b6 100644 --- a/mobile/src/api/api.ts +++ b/mobile/src/api/api.ts @@ -971,6 +971,13 @@ export type RewardCategoryConfig = { amount: number; }; +export type RewardItemConfig = { + product_id: number; + product_name: string; + quantity: number; + price: number; +}; + export type PointsPoolInfo = { key: string; name: string; @@ -979,13 +986,7 @@ export type PointsPoolInfo = { rewards_claimed: number; rewards_available: number; eligible_configs: RewardCategoryConfig[]; -}; - -export type RewardItemConfig = { - product_id: number; - product_name: string; - quantity: number; - price: number; + eligible_reward_items: RewardItemConfig[]; }; export type PointsRewardConfig = { diff --git a/mobile/src/screens/client/OrderHistoryScreen.tsx b/mobile/src/screens/client/OrderHistoryScreen.tsx index d1444a38..a163ccf0 100644 --- a/mobile/src/screens/client/OrderHistoryScreen.tsx +++ b/mobile/src/screens/client/OrderHistoryScreen.tsx @@ -124,7 +124,8 @@ export default function OrderHistoryScreen() { }, []); const handleClaim = (poolKey: string) => { - const items = pointsRewards?.reward?.reward_items ?? []; + const pool = pointsRewards?.pools.find((p) => p.key === poolKey); + const items = pool?.eligible_reward_items ?? []; if (items.length > 1) { setClaimFeedback(null); setRewardPickerPool(poolKey); @@ -1096,7 +1097,9 @@ export default function OrderHistoryScreen() { {( - pointsRewards?.reward?.reward_items ?? [] + pointsRewards?.pools.find( + (p) => p.key === rewardPickerPool, + )?.eligible_reward_items ?? [] ).map((item: RewardItemConfig, idx: number) => (