diff --git a/backend/gestion/db/db_parrain.go b/backend/gestion/db/db_parrain.go new file mode 100644 index 00000000..98663bf9 --- /dev/null +++ b/backend/gestion/db/db_parrain.go @@ -0,0 +1,69 @@ +package db + +import ( + "fmt" + "gestion/models" +) + +func (d *Database) SetClientParrain(clientUsername, parrainUsername string) error { + result := d.GDB.Model(&models.Client{}). + Where("username = ? AND (parrain IS NULL OR parrain = '')", clientUsername). + Update("parrain", parrainUsername) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return fmt.Errorf("client introuvable ou parrain déjà défini") + } + return nil +} + +func (d *Database) GetClientParrain(clientUsername string) (string, error) { + var parrain string + err := d.GDB.Table("clients"). + Select("parrain"). + Where("username = ?", clientUsername). + Scan(&parrain).Error + return parrain, err +} + +func (d *Database) GetClientsByParrain(parrainUsername string) ([]models.Client, error) { + var clients []models.Client + err := d.GDB. + Where("parrain = ?", parrainUsername). + Find(&clients).Error + return clients, err +} + +func (d *Database) GetParrainStats() ([]map[string]any, error) { + var rows []struct { + Parrain string `gorm:"column:parrain"` + FilleulCount int `gorm:"column:filleul_count"` + ReferralBalance float64 `gorm:"column:referral_balance"` + } + + err := d.GDB.Raw(` + SELECT + c.parrain, + COUNT(c.username) AS filleul_count, + p.referral_balance + FROM clients c + JOIN clients p ON p.username = c.parrain + WHERE c.parrain IS NOT NULL AND c.parrain <> '' + GROUP BY c.parrain, p.referral_balance + ORDER BY filleul_count DESC + `).Scan(&rows).Error + if err != nil { + return nil, err + } + + result := make([]map[string]any, 0, len(rows)) + for _, r := range rows { + result = append(result, map[string]any{ + "parrain": r.Parrain, + "filleul_count": r.FilleulCount, + "referral_balance": r.ReferralBalance, + }) + } + return result, nil +} diff --git a/backend/gestion/handlers/parrain.go b/backend/gestion/handlers/parrain.go new file mode 100644 index 00000000..69d6192f --- /dev/null +++ b/backend/gestion/handlers/parrain.go @@ -0,0 +1,166 @@ +package handlers + +import ( + "gestion/db" + "gestion/utils" + "log" + "net/http" + + "github.com/gin-gonic/gin" +) + +// SetClientParrainAdmin — POST /api/v2/admin/protected/client/:username/parrain/set (admin) +// Assigne un parrain à un client. Le parrain reçoit settings.ReferralAmount sur son solde. +func SetClientParrainAdmin(c *gin.Context) { + database := c.MustGet("database").(*db.Database) + targetUsername := c.Param("username") + + var req struct { + Parrain string `json:"parrain" binding:"required"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Champ parrain requis"}) + return + } + + if req.Parrain == targetUsername { + c.JSON(http.StatusBadRequest, gin.H{"error": "Un client ne peut pas être son propre parrain"}) + return + } + + // Vérifier que le parrain existe + parrain, err := database.GetClientByUsername(req.Parrain) + if err != nil || parrain == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Parrain introuvable"}) + return + } + + // Vérifier que le client n'a pas déjà un parrain + existing, err := database.GetClientParrain(targetUsername) + if err != nil { + utils.ServerErr(c, "Erreur vérification parrain", err) + return + } + if existing != "" { + c.JSON(http.StatusConflict, gin.H{"error": "Ce client a déjà un parrain", "parrain": existing}) + return + } + + if err := database.SetClientParrain(targetUsername, req.Parrain); err != nil { + utils.ServerErr(c, "Erreur enregistrement parrain", err) + return + } + + settings, _ := database.GetSettings() + if settings.ReferralEnabled && settings.ReferralAmount > 0 { + if err := database.CreditClientReferral(req.Parrain, settings.ReferralAmount); err != nil { + log.Printf("⚠️ [PARRAIN] Impossible de créditer %s: %v", req.Parrain, err) + } else { + log.Printf("✅ [PARRAIN] %s parrainé par %s → +%.2f€ crédité", targetUsername, req.Parrain, settings.ReferralAmount) + } + } + + c.JSON(http.StatusOK, gin.H{ + "message": "Parrain enregistré", + "client": targetUsername, + "parrain": req.Parrain, + "amount_credited": settings.ReferralAmount, + }) +} + +// GetMyParrainInfo — GET /api/v1/parrain (client authentifié) +// Retourne le parrain du client + ses filleuls + son solde parrainage. +func GetMyParrainInfo(c *gin.Context) { + database := c.MustGet("database").(*db.Database) + + username, exists := c.Get("username") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Utilisateur non authentifié"}) + return + } + clientUsername := username.(string) + + settings, _ := database.GetSettings() + + parrain, err := database.GetClientParrain(clientUsername) + if err != nil { + utils.ServerErr(c, "Erreur récupération parrain", err) + return + } + + filleuls, err := database.GetClientsByParrain(clientUsername) + if err != nil { + utils.ServerErr(c, "Erreur récupération filleuls", err) + return + } + + balance, err := database.GetClientReferralBalance(clientUsername) + if err != nil { + balance = 0 + } + + filleulNames := make([]string, 0, len(filleuls)) + for _, f := range filleuls { + filleulNames = append(filleulNames, f.Username) + } + + c.JSON(http.StatusOK, gin.H{ + "parrain": parrain, + "filleuls": filleulNames, + "filleul_count": len(filleulNames), + "referral_balance": balance, + "referral_enabled": settings.ReferralEnabled, + "referral_amount": settings.ReferralAmount, + }) +} + +// GetClientParrainAdmin — GET /api/v2/admin/protected/client/:username/parrain (admin) +// Retourne les infos parrainage d'un client spécifique. +func GetClientParrainAdmin(c *gin.Context) { + database := c.MustGet("database").(*db.Database) + targetUsername := c.Param("username") + + parrain, err := database.GetClientParrain(targetUsername) + if err != nil { + utils.ServerErr(c, "Erreur récupération parrain", err) + return + } + + filleuls, err := database.GetClientsByParrain(targetUsername) + if err != nil { + utils.ServerErr(c, "Erreur récupération filleuls", err) + return + } + + balance, _ := database.GetClientReferralBalance(targetUsername) + + filleulNames := make([]string, 0, len(filleuls)) + for _, f := range filleuls { + filleulNames = append(filleulNames, f.Username) + } + + c.JSON(http.StatusOK, gin.H{ + "username": targetUsername, + "parrain": parrain, + "filleuls": filleulNames, + "filleul_count": len(filleulNames), + "referral_balance": balance, + }) +} + +// GetParrainStatsAdmin — GET /api/v2/admin/protected/parrain/stats (admin) +// Retourne la liste de tous les parrains avec leur nombre de filleuls et leur solde. +func GetParrainStatsAdmin(c *gin.Context) { + database := c.MustGet("database").(*db.Database) + + stats, err := database.GetParrainStats() + if err != nil { + utils.ServerErr(c, "Erreur récupération stats parrainage", err) + return + } + + c.JSON(http.StatusOK, gin.H{ + "parrains": stats, + "total": len(stats), + }) +} diff --git a/backend/gestion/handlers/redis_services.go b/backend/gestion/handlers/redis_services.go index 10042071..99db8e82 100644 --- a/backend/gestion/handlers/redis_services.go +++ b/backend/gestion/handlers/redis_services.go @@ -975,6 +975,86 @@ func ResetClientPenaltiesAdmin(c *gin.Context) { }) } +// AddClientPointsAdmin ajoute des points à un client dans un pool donné (Admin/Cabine) +// POST /api/v2/admin/protected/client/:username/points/add +// Body: {"pool_key": "pool_0", "points": 10} +func AddClientPointsAdmin(c *gin.Context) { + userRole := c.GetString("role") + if userRole != "admin" && userRole != "cabine" { + c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) + return + } + + username := c.Param("username") + if username == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Username requis"}) + return + } + + var req struct { + PoolKey string `json:"pool_key" binding:"required"` + Points int `json:"points" binding:"required"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"}) + return + } + + if req.Points <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "Le nombre de points doit être positif"}) + return + } + + database := c.MustGet("database").(*db.Database) + + // Vérifier que le pool_key existe dans les settings + settings, err := database.GetSettings() + if err != nil { + utils.ServerErr(c, "Erreur récupération paramètres", err) + return + } + poolExists := false + for _, pool := range settings.PointsPools { + if pool.Key == req.PoolKey { + poolExists = true + break + } + } + if !poolExists { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Pool de points invalide", + "pools_valides": func() []string { + keys := make([]string, 0, len(settings.PointsPools)) + for _, p := range settings.PointsPools { + keys = append(keys, p.Key) + } + return keys + }(), + }) + return + } + + if err := database.AddClientPointsByCategory(username, req.Points, req.PoolKey); err != nil { + if strings.Contains(err.Error(), "non trouvé") { + c.JSON(http.StatusNotFound, gin.H{"error": "Client non trouvé"}) + } else { + utils.ServerErr(c, "Erreur ajout de points", err) + } + return + } + + log.Printf("✅ [ADD_POINTS] %d points (pool=%s) ajoutés à %s par %s", + req.Points, req.PoolKey, username, c.GetString("username")) + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "Points ajoutés avec succès", + "username": username, + "pool_key": req.PoolKey, + "points": req.Points, + }) +} + // ============================================ // STATISTIQUES TEMPS RÉEL // ============================================ diff --git a/backend/gestion/models/client.go b/backend/gestion/models/client.go index 67eed2d0..6f302fc2 100644 --- a/backend/gestion/models/client.go +++ b/backend/gestion/models/client.go @@ -17,8 +17,8 @@ type Client struct { LastPenaltyReason string `gorm:"column:last_penalty_reason" json:"last_penalty_reason"` MustChangePassword bool `gorm:"column:must_change_password;default:false" json:"must_change_password"` ReferralBalance float64 `gorm:"column:referral_balance;default:0" json:"referral_balance"` - // points_extra est un JSONB géré manuellement (type non supporté nativement par GORM) PointsExtra map[string]int `gorm:"-" json:"points_extra"` + Parrain string `gorm:"column:parrain" json:"parrain"` CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` } diff --git a/backend/gestion/routes/routes.go b/backend/gestion/routes/routes.go index d5457e8e..8759eabc 100644 --- a/backend/gestion/routes/routes.go +++ b/backend/gestion/routes/routes.go @@ -109,6 +109,7 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services // 🎁 PARRAINAGE CLIENT cartGroupV1.GET("/referral/balance", handlers.GetMyReferralBalance) + cartGroupV1.GET("/parrain", handlers.GetMyParrainInfo) // 💸 STATUT PAIEMENT CRYPTO cartGroupV1.GET("/commands/:id/payment-status", handlers.GetCommandPaymentStatus) @@ -247,7 +248,9 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services // ============================================ adminGroupV2.POST("/penalty", handlers.ApplyClientPenalty) // Appliquer pénalité adminGroupV2.GET("/client/:username/penalties", handlers.GetClientPenaltiesAdmin) // Voir pénalités client - adminGroupV2.POST("/client/:username/penalties/reset", handlers.ResetClientPenaltiesAdmin) // Reset pénalités + adminGroupV2.POST("/client/:username/penalties/reset", handlers.ResetClientPenaltiesAdmin) // Reset amende + adminGroupV2.POST("/client/:username/point/reset", handlers.ResetClientPointAdmin) // Reset points → 0 + adminGroupV2.POST("/client/:username/points/add", handlers.AddClientPointsAdmin) // Ajouter points par pool adminGroupV2.GET("/penalties/all", handlers.GetAllClientsWithPenalties) // Liste clients avec pénalités adminGroupV2.GET("/penalties/stats", handlers.GetPenaltiesStats) @@ -260,6 +263,9 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services // 🎁 PARRAINAGE ADMIN adminGroupV2.GET("/client/:username/referral", handlers.GetClientReferralAdmin) adminGroupV2.POST("/client/:username/referral/credit", handlers.CreditClientReferralAdmin) + adminGroupV2.POST("/client/:username/parrain/set", handlers.SetClientParrainAdmin) + adminGroupV2.GET("/client/:username/parrain", handlers.GetClientParrainAdmin) + adminGroupV2.GET("/parrain/stats", handlers.GetParrainStatsAdmin) // ============================================ // ⭐⭐ ALERTES POLICE - GESTION ADMIN