From dfa432862ae0aabb2d05ca5ec84cb29de3ff2a78 Mon Sep 17 00:00:00 2001
From: Nuxgrid
Date: Sat, 18 Jul 2026 16:41:42 +0200
Subject: [PATCH] chore: fix recompense categorie
---
backend/gestion/db/db_product.go | 21 +++
backend/gestion/handlers/points.go | 141 ++++++++++++++----
frontend-prep/src/api/api.ts | 15 +-
.../src/pages/User/ConsultationHistorique.tsx | 4 +-
mobile/src/api/api.ts | 15 +-
.../src/screens/client/OrderHistoryScreen.tsx | 7 +-
6 files changed, 156 insertions(+), 47 deletions(-)
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) => (