chore: build
This commit is contained in:
@@ -650,3 +650,84 @@ func (d *Database) CanUserAccessCommand(
|
|||||||
|
|
||||||
return exists, err
|
return exists, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetClientPointsAndRewards retourne les points cumulés et les récompenses réclamées pour un client.
|
||||||
|
func (d *Database) GetClientPointsAndRewards(username string) (pointsExtra map[string]int, pointsRedeemed map[string]int, err error) {
|
||||||
|
var row struct {
|
||||||
|
PointsExtraJSON []byte `gorm:"column:points_extra"`
|
||||||
|
PointsRedeemedJSON []byte `gorm:"column:points_redeemed"`
|
||||||
|
}
|
||||||
|
if err = d.GDB.Raw(`
|
||||||
|
SELECT COALESCE(points_extra, '{}'::jsonb) as points_extra,
|
||||||
|
COALESCE(points_redeemed, '{}'::jsonb) as points_redeemed
|
||||||
|
FROM clients WHERE username = ?`, username).Scan(&row).Error; err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("erreur lecture points client: %w", err)
|
||||||
|
}
|
||||||
|
pointsExtra = map[string]int{}
|
||||||
|
pointsRedeemed = map[string]int{}
|
||||||
|
if len(row.PointsExtraJSON) > 0 {
|
||||||
|
json.Unmarshal(row.PointsExtraJSON, &pointsExtra)
|
||||||
|
}
|
||||||
|
if len(row.PointsRedeemedJSON) > 0 {
|
||||||
|
json.Unmarshal(row.PointsRedeemedJSON, &pointsRedeemed)
|
||||||
|
}
|
||||||
|
return pointsExtra, pointsRedeemed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClaimPoolReward réclame une récompense pour un pool donné si le client a assez de points.
|
||||||
|
// Retourne le nombre de récompenses disponibles restantes après la réclamation.
|
||||||
|
func (d *Database) ClaimPoolReward(username, poolKey string, threshold int) (remainingAvailable int, err error) {
|
||||||
|
var points, redeemed int
|
||||||
|
|
||||||
|
err = d.GDB.Transaction(func(tx *gorm.DB) error {
|
||||||
|
var row struct {
|
||||||
|
Points int `gorm:"column:pts"`
|
||||||
|
Redeemed int `gorm:"column:redeemed"`
|
||||||
|
}
|
||||||
|
if err := tx.Raw(`
|
||||||
|
SELECT
|
||||||
|
COALESCE((points_extra->>?)::int, 0) as pts,
|
||||||
|
COALESCE((points_redeemed->>?)::int, 0) as redeemed
|
||||||
|
FROM clients WHERE username = ? FOR UPDATE`,
|
||||||
|
poolKey, poolKey, username).Scan(&row).Error; err != nil {
|
||||||
|
return fmt.Errorf("erreur lecture: %w", err)
|
||||||
|
}
|
||||||
|
points = row.Points
|
||||||
|
redeemed = row.Redeemed
|
||||||
|
|
||||||
|
earned := points / threshold
|
||||||
|
available := earned - redeemed
|
||||||
|
if available <= 0 {
|
||||||
|
return fmt.Errorf("pas de récompense disponible pour ce pool")
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Exec(`
|
||||||
|
UPDATE clients
|
||||||
|
SET points_redeemed = jsonb_set(
|
||||||
|
COALESCE(points_redeemed, '{}'::jsonb),
|
||||||
|
ARRAY[?],
|
||||||
|
to_jsonb(COALESCE((points_redeemed->>?)::int, 0) + 1)
|
||||||
|
), updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE username = ?`,
|
||||||
|
poolKey, poolKey, username).Error
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
earned := points / threshold
|
||||||
|
remainingAvailable = earned - (redeemed + 1)
|
||||||
|
return remainingAvailable, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetClientRedeemed remet à zéro les récompenses réclamées (admin).
|
||||||
|
func (d *Database) ResetClientRedeemed(username, poolKey string) error {
|
||||||
|
if poolKey != "" {
|
||||||
|
return d.GDB.Exec(`
|
||||||
|
UPDATE clients SET points_redeemed = points_redeemed - ?, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE username = ?`, poolKey, username).Error
|
||||||
|
}
|
||||||
|
return d.GDB.Exec(`
|
||||||
|
UPDATE clients SET points_redeemed = '{}'::jsonb, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE username = ?`, username).Error
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gestion/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *Database) AddContact(contact *models.Contact) error {
|
||||||
|
err := d.GDB.Create(contact).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) GetContact(id uint) (*models.Contact, error) {
|
||||||
|
var contact models.Contact
|
||||||
|
if err := d.GDB.First(&contact, id).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &contact, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) UpdateContact(contact *models.Contact) error {
|
||||||
|
err := d.GDB.Save(contact).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) DeleteContact(id uint) error {
|
||||||
|
err := d.GDB.Delete(&models.Contact{}, id).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -236,11 +236,24 @@ func InitDB() *Database {
|
|||||||
log.Fatalf("❌ Erreur migration clients.points_extra: %v", err)
|
log.Fatalf("❌ Erreur migration clients.points_extra: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Migration: récompenses réclamées par pool (nb de fois que la récompense a été obtenue)
|
||||||
|
if _, err = database.Exec(`ALTER TABLE clients ADD COLUMN IF NOT EXISTS points_redeemed JSONB NOT NULL DEFAULT '{}'::jsonb`); err != nil {
|
||||||
|
log.Fatalf("❌ Erreur migration clients.points_redeemed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Migration: flag "à venir" sur les produits
|
// Migration: flag "à venir" sur les produits
|
||||||
if _, err = database.Exec(`ALTER TABLE products ADD COLUMN IF NOT EXISTS coming_soon BOOLEAN NOT NULL DEFAULT FALSE`); err != nil {
|
if _, err = database.Exec(`ALTER TABLE products ADD COLUMN IF NOT EXISTS coming_soon BOOLEAN NOT NULL DEFAULT FALSE`); err != nil {
|
||||||
log.Fatalf("❌ Erreur migration products.coming_soon: %v", err)
|
log.Fatalf("❌ Erreur migration products.coming_soon: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Migration: table contacts (SAV Telegram)
|
||||||
|
if _, err = database.Exec(`CREATE TABLE IF NOT EXISTS contacts (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL
|
||||||
|
)`); err != nil {
|
||||||
|
log.Fatalf("❌ Erreur migration contacts: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Lancer le nettoyage périodique des tokens expirés
|
// Lancer le nettoyage périodique des tokens expirés
|
||||||
go database.cleanExpiredTokensPeriodically()
|
go database.cleanExpiredTokensPeriodically()
|
||||||
|
|
||||||
@@ -469,6 +482,14 @@ func (db *Database) createTables() error {
|
|||||||
`CREATE INDEX IF NOT EXISTS idx_issues_command ON delivery_issues(command_id);`,
|
`CREATE INDEX IF NOT EXISTS idx_issues_command ON delivery_issues(command_id);`,
|
||||||
`CREATE INDEX IF NOT EXISTS idx_issues_status ON delivery_issues(status);`,
|
`CREATE INDEX IF NOT EXISTS idx_issues_status ON delivery_issues(status);`,
|
||||||
`CREATE INDEX IF NOT EXISTS idx_issues_reported_by ON delivery_issues(reported_by);`,
|
`CREATE INDEX IF NOT EXISTS idx_issues_reported_by ON delivery_issues(reported_by);`,
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// TABLE contacts
|
||||||
|
// ============================
|
||||||
|
`CREATE TABLE IF NOT EXISTS contacts (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL
|
||||||
|
);`,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, query := range queries {
|
for _, query := range queries {
|
||||||
|
|||||||
@@ -24,9 +24,13 @@ func (d *Database) NotifyClient(username string, commandID int, notifType, messa
|
|||||||
Redis.LPush(RedisCtx, notifKey, notifJSON)
|
Redis.LPush(RedisCtx, notifKey, notifJSON)
|
||||||
Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour)
|
Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour)
|
||||||
|
|
||||||
if services.TelegramBot != nil && services.TelegramBot.IsConfigured() && services.TelegramBot.IsNotificationsEnabled() {
|
if services.LBTelegram != nil && services.LBTelegram.IsConfigured() && services.TelegramBot != nil && services.TelegramBot.IsNotificationsEnabled() {
|
||||||
if chatID, ok, err := d.GetClientTelegramChatID(username); err == nil && ok {
|
if chatID, ok, err := d.GetClientTelegramChatID(username); err == nil && ok {
|
||||||
go services.TelegramBot.SendMessage(chatID, fmt.Sprintf("🔔 <b>Notification</b>\n\n%s", message))
|
go func(id int64, msg string) {
|
||||||
|
if err := services.LBTelegram.SendNotification(id, fmt.Sprintf("🔔 <b>Notification</b>\n\n%s", msg)); err != nil {
|
||||||
|
log.Printf("⚠️ [LB] notification client échouée pour %s: %v", username, err)
|
||||||
|
}
|
||||||
|
}(chatID, message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,9 +54,13 @@ func (d *Database) NotifyLivreur(username string, commandID int, notifType, mess
|
|||||||
Redis.LPush(RedisCtx, notifKey, notifJSON)
|
Redis.LPush(RedisCtx, notifKey, notifJSON)
|
||||||
Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour)
|
Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour)
|
||||||
|
|
||||||
if services.TelegramBot != nil && services.TelegramBot.IsConfigured() && services.TelegramBot.IsNotificationsEnabled() {
|
if services.LBTelegram != nil && services.LBTelegram.IsConfigured() && services.TelegramBot != nil && services.TelegramBot.IsNotificationsEnabled() {
|
||||||
if chatID, ok, err := d.GetUserTelegramChatID(username); err == nil && ok {
|
if chatID, ok, err := d.GetUserTelegramChatID(username); err == nil && ok {
|
||||||
go services.TelegramBot.SendMessage(chatID, fmt.Sprintf("🔔 <b>Notification</b>\n\n%s", message))
|
go func(id int64, msg string) {
|
||||||
|
if err := services.LBTelegram.SendNotification(id, fmt.Sprintf("🔔 <b>Notification</b>\n\n%s", msg)); err != nil {
|
||||||
|
log.Printf("⚠️ [LB] notification livreur échouée pour %s: %v", username, err)
|
||||||
|
}
|
||||||
|
}(chatID, message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,11 +95,15 @@ func (d *Database) NotifyAllAdminCabine(commandID int, clientUsername, deliveryA
|
|||||||
Redis.LPush(RedisCtx, notifKey, notifJSON)
|
Redis.LPush(RedisCtx, notifKey, notifJSON)
|
||||||
Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour)
|
Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour)
|
||||||
|
|
||||||
if services.TelegramBot != nil && services.TelegramBot.IsConfigured() && services.TelegramBot.IsNotificationsEnabled() {
|
if services.LBTelegram != nil && services.LBTelegram.IsConfigured() && services.TelegramBot != nil && services.TelegramBot.IsNotificationsEnabled() {
|
||||||
if chatID, ok, err := d.GetUserTelegramChatID(u.Username); err == nil && ok {
|
if chatID, ok, err := d.GetUserTelegramChatID(u.Username); err == nil && ok {
|
||||||
capturedChatID := chatID
|
capturedChatID := chatID
|
||||||
capturedMsg := msg
|
capturedMsg := msg
|
||||||
go services.TelegramBot.SendMessage(capturedChatID, fmt.Sprintf("🔔 <b>Nouvelle commande</b>\n\n%s", capturedMsg))
|
go func(id int64, m string) {
|
||||||
|
if err := services.LBTelegram.SendNotification(id, fmt.Sprintf("🔔 <b>Nouvelle commande</b>\n\n%s", m)); err != nil {
|
||||||
|
log.Printf("⚠️ [LB] notification admin/cabine échouée: %v", err)
|
||||||
|
}
|
||||||
|
}(capturedChatID, capturedMsg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
count++
|
count++
|
||||||
@@ -126,11 +138,15 @@ func (d *Database) NotifyAllAdminCabineAlert(alertID int, livreurUsername, alert
|
|||||||
Redis.LPush(RedisCtx, notifKey, notifJSON)
|
Redis.LPush(RedisCtx, notifKey, notifJSON)
|
||||||
Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour)
|
Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour)
|
||||||
|
|
||||||
if services.TelegramBot != nil && services.TelegramBot.IsConfigured() && services.TelegramBot.IsNotificationsEnabled() {
|
if services.LBTelegram != nil && services.LBTelegram.IsConfigured() && services.TelegramBot != nil && services.TelegramBot.IsNotificationsEnabled() {
|
||||||
if chatID, ok, err := d.GetUserTelegramChatID(u.Username); err == nil && ok {
|
if chatID, ok, err := d.GetUserTelegramChatID(u.Username); err == nil && ok {
|
||||||
capturedChatID := chatID
|
capturedChatID := chatID
|
||||||
capturedBody := body
|
capturedBody := body
|
||||||
go services.TelegramBot.SendMessage(capturedChatID, fmt.Sprintf("🚨 <b>Alerte livreur</b>\n\n%s", capturedBody))
|
go func(id int64, m string) {
|
||||||
|
if err := services.LBTelegram.SendNotification(id, fmt.Sprintf("🚨 <b>Alerte livreur</b>\n\n%s", m)); err != nil {
|
||||||
|
log.Printf("⚠️ [LB] notification alerte échouée: %v", err)
|
||||||
|
}
|
||||||
|
}(capturedChatID, capturedBody)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
count++
|
count++
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ func DefaultSettings() models.AppSettings {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
ShopName: "Milieu-Nantais",
|
ShopName: "Milieu-Nantais",
|
||||||
|
ContactTelegram: "MLN44LA",
|
||||||
DeliveryMode: models.DeliveryModeConfig{
|
DeliveryMode: models.DeliveryModeConfig{
|
||||||
Mode: "single",
|
Mode: "single",
|
||||||
CategoryRoutes: []models.CategoryRoute{},
|
CategoryRoutes: []models.CategoryRoute{},
|
||||||
@@ -111,6 +112,11 @@ func (d *Database) GetSettings() (models.AppSettings, error) {
|
|||||||
if err := json.Unmarshal([]byte(row.Value), &pools); err == nil {
|
if err := json.Unmarshal([]byte(row.Value), &pools); err == nil {
|
||||||
settings.PointsPools = pools
|
settings.PointsPools = pools
|
||||||
}
|
}
|
||||||
|
case "points_reward":
|
||||||
|
var reward models.PointsReward
|
||||||
|
if err := json.Unmarshal([]byte(row.Value), &reward); err == nil {
|
||||||
|
settings.PointsReward = &reward
|
||||||
|
}
|
||||||
case "referral_enabled":
|
case "referral_enabled":
|
||||||
settings.ReferralEnabled = row.Value == "true"
|
settings.ReferralEnabled = row.Value == "true"
|
||||||
case "referral_amount":
|
case "referral_amount":
|
||||||
@@ -140,6 +146,8 @@ func (d *Database) GetSettings() (models.AppSettings, error) {
|
|||||||
if err := json.Unmarshal([]byte(row.Value), &zones); err == nil {
|
if err := json.Unmarshal([]byte(row.Value), &zones); err == nil {
|
||||||
settings.PostalZones = zones
|
settings.PostalZones = zones
|
||||||
}
|
}
|
||||||
|
case "contact_telegram":
|
||||||
|
settings.ContactTelegram = row.Value
|
||||||
case "telegram_bot_token":
|
case "telegram_bot_token":
|
||||||
settings.TelegramBotToken = row.Value
|
settings.TelegramBotToken = row.Value
|
||||||
case "telegram_bot_username":
|
case "telegram_bot_username":
|
||||||
@@ -186,6 +194,11 @@ func (d *Database) UpdateSettings(s models.AppSettings) error {
|
|||||||
return fmt.Errorf("erreur sérialisation pools: %w", err)
|
return fmt.Errorf("erreur sérialisation pools: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rewardJSON, err := json.Marshal(s.PointsReward)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("erreur sérialisation points_reward: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if s.NowPaymentsCurrencies == nil {
|
if s.NowPaymentsCurrencies == nil {
|
||||||
s.NowPaymentsCurrencies = []string{}
|
s.NowPaymentsCurrencies = []string{}
|
||||||
}
|
}
|
||||||
@@ -215,11 +228,15 @@ func (d *Database) UpdateSettings(s models.AppSettings) error {
|
|||||||
return fmt.Errorf("erreur sérialisation delivery_mode: %w", err)
|
return fmt.Errorf("erreur sérialisation delivery_mode: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.ContactTelegram == "" {
|
||||||
|
s.ContactTelegram = "MLN44LA"
|
||||||
|
}
|
||||||
pairs := [][2]string{
|
pairs := [][2]string{
|
||||||
{"penalties_enabled", boolStr(s.PenaltiesEnabled)},
|
{"penalties_enabled", boolStr(s.PenaltiesEnabled)},
|
||||||
{"show_amende_score", boolStr(s.ShowAmendeScore)},
|
{"show_amende_score", boolStr(s.ShowAmendeScore)},
|
||||||
{"points_enabled", boolStr(s.PointsEnabled)},
|
{"points_enabled", boolStr(s.PointsEnabled)},
|
||||||
{"points_pools", string(poolsJSON)},
|
{"points_pools", string(poolsJSON)},
|
||||||
|
{"points_reward", string(rewardJSON)},
|
||||||
{"referral_enabled", boolStr(s.ReferralEnabled)},
|
{"referral_enabled", boolStr(s.ReferralEnabled)},
|
||||||
{"referral_amount", strconv.FormatFloat(s.ReferralAmount, 'f', 2, 64)},
|
{"referral_amount", strconv.FormatFloat(s.ReferralAmount, 'f', 2, 64)},
|
||||||
{"crypto_payment_enabled", boolStr(s.CryptoPaymentEnabled)},
|
{"crypto_payment_enabled", boolStr(s.CryptoPaymentEnabled)},
|
||||||
@@ -235,6 +252,7 @@ func (d *Database) UpdateSettings(s models.AppSettings) error {
|
|||||||
{"telegram_2fa_enabled", boolStr(s.Telegram2FAEnabled)},
|
{"telegram_2fa_enabled", boolStr(s.Telegram2FAEnabled)},
|
||||||
{"delivery_mode", string(deliveryModeJSON)},
|
{"delivery_mode", string(deliveryModeJSON)},
|
||||||
{"shop_name", s.ShopName},
|
{"shop_name", s.ShopName},
|
||||||
|
{"contact_telegram", s.ContactTelegram},
|
||||||
}
|
}
|
||||||
|
|
||||||
upsert := `INSERT INTO app_settings (key, value) VALUES (?, ?)
|
upsert := `INSERT INTO app_settings (key, value) VALUES (?, ?)
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gestion/db"
|
||||||
|
"gestion/models"
|
||||||
|
"gestion/utils"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
username := c.GetString("username")
|
||||||
|
if username == "" {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
database := c.MustGet("database").(*db.Database)
|
||||||
|
|
||||||
|
settings, err := database.GetSettings()
|
||||||
|
if err != nil {
|
||||||
|
utils.ServerErr(c, "Erreur lecture paramètres", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !settings.PointsEnabled || len(settings.PointsPools) == 0 {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"enabled": false, "pools": []gin.H{}, "reward": nil})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pointsExtra, pointsRedeemed, err := database.GetClientPointsAndRewards(username)
|
||||||
|
if err != nil {
|
||||||
|
utils.ServerErr(c, "Erreur lecture points", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reward := settings.PointsReward
|
||||||
|
|
||||||
|
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 []models.RewardCategoryConfig `json:"eligible_configs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
pools := make([]PoolInfo, 0, len(settings.PointsPools))
|
||||||
|
for _, pool := range settings.PointsPools {
|
||||||
|
pts := pointsExtra[pool.Key]
|
||||||
|
redeemed := pointsRedeemed[pool.Key]
|
||||||
|
|
||||||
|
var earned, available int
|
||||||
|
if reward != nil && reward.Threshold > 0 {
|
||||||
|
earned = pts / reward.Threshold
|
||||||
|
available = earned - redeemed
|
||||||
|
if available < 0 {
|
||||||
|
available = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtrer les category_configs aux seules catégories du pool
|
||||||
|
poolCats := make(map[string]bool, len(pool.Categories))
|
||||||
|
for _, c := range pool.Categories {
|
||||||
|
poolCats[c] = true
|
||||||
|
}
|
||||||
|
eligibleConfigs := make([]models.RewardCategoryConfig, 0)
|
||||||
|
if reward != nil {
|
||||||
|
for _, cfg := range reward.CategoryConfigs {
|
||||||
|
if poolCats[cfg.Category] {
|
||||||
|
eligibleConfigs = append(eligibleConfigs, cfg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pools = append(pools, PoolInfo{
|
||||||
|
Key: pool.Key,
|
||||||
|
Name: pool.Name,
|
||||||
|
Points: pts,
|
||||||
|
RewardsEarned: earned,
|
||||||
|
RewardsClaimed: redeemed,
|
||||||
|
RewardsAvailable: available,
|
||||||
|
EligibleConfigs: eligibleConfigs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retourner la récompense sans category_configs (les configs sont par pool)
|
||||||
|
var rewardMeta gin.H
|
||||||
|
if reward != nil {
|
||||||
|
rewardMeta = gin.H{
|
||||||
|
"threshold": reward.Threshold,
|
||||||
|
"type": reward.Type,
|
||||||
|
"description": reward.Description,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"enabled": true, "pools": pools, "reward": rewardMeta})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClaimMyReward réclame une récompense sur un pool donné si le client a atteint le seuil.
|
||||||
|
func ClaimMyReward(c *gin.Context) {
|
||||||
|
username := c.GetString("username")
|
||||||
|
if username == "" {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
PoolKey string `json:"pool_key" binding:"required"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "pool_key requis"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
database := c.MustGet("database").(*db.Database)
|
||||||
|
|
||||||
|
settings, err := database.GetSettings()
|
||||||
|
if err != nil {
|
||||||
|
utils.ServerErr(c, "Erreur lecture paramètres", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !settings.PointsEnabled {
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{"error": "Système de points désactivé"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reward := settings.PointsReward
|
||||||
|
if reward == nil || reward.Threshold <= 0 {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Aucune récompense configurée"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier que le pool existe
|
||||||
|
poolExists := false
|
||||||
|
for _, p := range settings.PointsPools {
|
||||||
|
if p.Key == req.PoolKey {
|
||||||
|
poolExists = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !poolExists {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Pool introuvable"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
remaining, err := database.ClaimPoolReward(username, req.PoolKey, reward.Threshold)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "pas de récompense disponible") {
|
||||||
|
c.JSON(http.StatusConflict, gin.H{"error": "Pas assez de points pour réclamer une récompense"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.ServerErr(c, "Erreur réclamation récompense", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"description": reward.Description,
|
||||||
|
"remaining_rewards": remaining,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminResetClientRedeemed remet à zéro les récompenses réclamées d'un client (admin).
|
||||||
|
func AdminResetClientRedeemed(c *gin.Context) {
|
||||||
|
username := c.Param("username")
|
||||||
|
poolKey := c.Query("pool_key")
|
||||||
|
|
||||||
|
database := c.MustGet("database").(*db.Database)
|
||||||
|
if err := database.ResetClientRedeemed(username, poolKey); err != nil {
|
||||||
|
utils.ServerErr(c, "Erreur reset récompenses", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
@@ -47,6 +47,7 @@ func GetPublicSettings(c *gin.Context) {
|
|||||||
"telegram_notifications_enabled": settings.TelegramNotificationsEnabled,
|
"telegram_notifications_enabled": settings.TelegramNotificationsEnabled,
|
||||||
"shop_name": settings.ShopName,
|
"shop_name": settings.ShopName,
|
||||||
"two_fa_enabled": settings.Telegram2FAEnabled,
|
"two_fa_enabled": settings.Telegram2FAEnabled,
|
||||||
|
"contact_telegram": settings.ContactTelegram,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"gestion/services"
|
"gestion/services"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -91,10 +92,20 @@ func handleLinkAccount(c *gin.Context, token string, chatID int64) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("✅ [TELEGRAM_LINK] Compte %s (%s) lié au chat_id %d", username, role, chatID)
|
log.Printf("✅ [TELEGRAM_LINK] Compte %s (%s) lié au chat_id %d", username, role, chatID)
|
||||||
|
|
||||||
|
if services.LBTelegram != nil && services.LBTelegram.IsConfigured() {
|
||||||
|
if err := services.LBTelegram.EnrollUser(chatID, username, role); err != nil {
|
||||||
|
log.Printf("⚠️ [LB] enrollment échoué pour %s: %v", username, err)
|
||||||
|
// fallback: confirmation directe via le bot principal
|
||||||
if services.TelegramBot != nil {
|
if services.TelegramBot != nil {
|
||||||
services.TelegramBot.SendMessage(chatID,
|
services.TelegramBot.SendMessage(chatID,
|
||||||
"✅ <b>Compte lié avec succès !</b>\n\nVous recevrez désormais vos notifications ici.")
|
"✅ <b>Compte lié avec succès !</b>\n\nVous recevrez désormais vos notifications ici.")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} else if services.TelegramBot != nil {
|
||||||
|
services.TelegramBot.SendMessage(chatID,
|
||||||
|
"✅ <b>Compte lié avec succès !</b>\n\nVous recevrez désormais vos notifications ici.")
|
||||||
|
}
|
||||||
|
|
||||||
c.Status(http.StatusOK)
|
c.Status(http.StatusOK)
|
||||||
}
|
}
|
||||||
@@ -118,9 +129,14 @@ func GenerateClientLinkToken(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
botUsername := services.TelegramBot.BotUsername
|
||||||
|
if services.LBTelegram != nil && services.LBTelegram.IsConfigured() && services.LBTelegram.Bot1Username != "" {
|
||||||
|
botUsername = services.LBTelegram.Bot1Username
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"token": token,
|
"token": token,
|
||||||
"link_url": "https://t.me/" + services.TelegramBot.BotUsername + "?start=" + token,
|
"link_url": "https://t.me/" + botUsername + "?start=" + token,
|
||||||
"message": "/start " + token,
|
"message": "/start " + token,
|
||||||
"expires_in": 600,
|
"expires_in": 600,
|
||||||
})
|
})
|
||||||
@@ -145,9 +161,14 @@ func GenerateLivreurLinkToken(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
botUsername := services.TelegramBot.BotUsername
|
||||||
|
if services.LBTelegram != nil && services.LBTelegram.IsConfigured() && services.LBTelegram.Bot1Username != "" {
|
||||||
|
botUsername = services.LBTelegram.Bot1Username
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"token": token,
|
"token": token,
|
||||||
"link_url": "https://t.me/" + services.TelegramBot.BotUsername + "?start=" + token,
|
"link_url": "https://t.me/" + botUsername + "?start=" + token,
|
||||||
"message": "/start " + token,
|
"message": "/start " + token,
|
||||||
"expires_in": 600,
|
"expires_in": 600,
|
||||||
})
|
})
|
||||||
@@ -180,9 +201,14 @@ func GenerateAdminLinkToken(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
botUsername := services.TelegramBot.BotUsername
|
||||||
|
if services.LBTelegram != nil && services.LBTelegram.IsConfigured() && services.LBTelegram.Bot1Username != "" {
|
||||||
|
botUsername = services.LBTelegram.Bot1Username
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"token": token,
|
"token": token,
|
||||||
"link_url": "https://t.me/" + services.TelegramBot.BotUsername + "?start=" + token,
|
"link_url": "https://t.me/" + botUsername + "?start=" + token,
|
||||||
"message": "/start " + token,
|
"message": "/start " + token,
|
||||||
"expires_in": 600,
|
"expires_in": 600,
|
||||||
})
|
})
|
||||||
@@ -297,3 +323,62 @@ func UnlinkAdminTelegram(c *gin.Context) {
|
|||||||
log.Printf("✅ [TELEGRAM_UNLINK] Compte admin %s délié", username)
|
log.Printf("✅ [TELEGRAM_UNLINK] Compte admin %s délié", username)
|
||||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// LIAISON INTERNE (appelée par LBTelegram)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
// POST /api/internal/telegram/link
|
||||||
|
// Appelée par LBTelegram quand Bot1 reçoit /start TOKEN.
|
||||||
|
// Valide le token, enregistre le chat_id, déclenche l'enrollment.
|
||||||
|
func InternalTelegramLink(c *gin.Context) {
|
||||||
|
secret := c.GetHeader("X-Internal-Secret")
|
||||||
|
expected := os.Getenv("BACKEND_LINK_SECRET")
|
||||||
|
if expected == "" || secret != expected {
|
||||||
|
c.Status(http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
ChatID int64 `json:"chat_id" binding:"required"`
|
||||||
|
Token string `json:"token" binding:"required"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
database := c.MustGet("database").(*db.Database)
|
||||||
|
|
||||||
|
username, role, err := db.ValidateAndConsumeLinkToken(req.Token)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("⚠️ [TELEGRAM_LINK_INTERNAL] Token invalide: %v", err)
|
||||||
|
c.Status(http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var saveErr error
|
||||||
|
switch role {
|
||||||
|
case "client":
|
||||||
|
saveErr = database.SaveClientTelegramChatID(username, req.ChatID)
|
||||||
|
default:
|
||||||
|
saveErr = database.SaveUserTelegramChatID(username, req.ChatID)
|
||||||
|
}
|
||||||
|
if saveErr != nil {
|
||||||
|
log.Printf("❌ [TELEGRAM_LINK_INTERNAL] Erreur sauvegarde pour %s: %v", username, saveErr)
|
||||||
|
c.Status(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("✅ [TELEGRAM_LINK_INTERNAL] Compte %s (%s) lié via Bot1 (chat_id %d)", username, role, req.ChatID)
|
||||||
|
|
||||||
|
if services.LBTelegram != nil && services.LBTelegram.IsConfigured() {
|
||||||
|
if err := services.LBTelegram.EnrollUser(req.ChatID, username, role); err != nil {
|
||||||
|
log.Printf("⚠️ [LB] enrollment échoué pour %s: %v", username, err)
|
||||||
|
c.Status(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusOK)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
// ============================================
|
|
||||||
// main.go - VERSION SIMPLIFIÉE AVEC CLEANUP AUTO
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -42,6 +38,13 @@ func main() {
|
|||||||
geoService := services.NewGeoService(db.Redis, db.RedisCtx)
|
geoService := services.NewGeoService(db.Redis, db.RedisCtx)
|
||||||
log.Println("✅ Service de géolocalisation initialisé")
|
log.Println("✅ Service de géolocalisation initialisé")
|
||||||
|
|
||||||
|
lbService := services.NewLBTelegramService()
|
||||||
|
if lbService.IsConfigured() {
|
||||||
|
log.Println("✅ Service LBTelegram initialisé")
|
||||||
|
} else {
|
||||||
|
log.Println("ℹ️ Service LBTelegram désactivé (LBTELEGRAM_URL non défini)")
|
||||||
|
}
|
||||||
|
|
||||||
telegramService := services.NewTelegramService()
|
telegramService := services.NewTelegramService()
|
||||||
if telegramService.IsConfigured() {
|
if telegramService.IsConfigured() {
|
||||||
log.Println("✅ Service Telegram initialisé")
|
log.Println("✅ Service Telegram initialisé")
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ type Client struct {
|
|||||||
MustChangePassword bool `gorm:"column:must_change_password;default:false" json:"must_change_password"`
|
MustChangePassword bool `gorm:"column:must_change_password;default:false" json:"must_change_password"`
|
||||||
ReferralBalance float64 `gorm:"column:referral_balance;default:0" json:"referral_balance"`
|
ReferralBalance float64 `gorm:"column:referral_balance;default:0" json:"referral_balance"`
|
||||||
PointsExtra map[string]int `gorm:"-" json:"points_extra"`
|
PointsExtra map[string]int `gorm:"-" json:"points_extra"`
|
||||||
|
PointsRedeemed map[string]int `gorm:"-" json:"points_redeemed"`
|
||||||
Parrain string `gorm:"column:parrain" json:"parrain"`
|
Parrain string `gorm:"column:parrain" json:"parrain"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
type Contact struct {
|
||||||
|
ID int `json:"id" gorm:"primaryKey"`
|
||||||
|
Name string `json:"name" gorm:"not null"`
|
||||||
|
}
|
||||||
@@ -14,6 +14,22 @@ type PointsTier struct {
|
|||||||
Points int `json:"points"`
|
Points int `json:"points"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RewardCategoryConfig définit les produits éligibles dans une catégorie pour une récompense
|
||||||
|
type RewardCategoryConfig struct {
|
||||||
|
Category string `json:"category"` // nom 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
|
||||||
|
Amount float64 `json:"amount"` // valeur monétaire de la récompense pour cette catégorie (ex: 30.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PointsReward représente la récompense débloquée à partir d'un seuil de points cumulés
|
||||||
|
type PointsReward struct {
|
||||||
|
Threshold int `json:"threshold"` // points cumulés nécessaires (ex: 20)
|
||||||
|
Type string `json:"type"` // "free_product" | "half_price_product" | "custom"
|
||||||
|
Description string `json:"description"` // description libre affichée au client
|
||||||
|
CategoryConfigs []RewardCategoryConfig `json:"category_configs"` // catégories + produits éligibles
|
||||||
|
}
|
||||||
|
|
||||||
// DaySchedule représente les horaires de livraison pour un jour de la semaine
|
// DaySchedule représente les horaires de livraison pour un jour de la semaine
|
||||||
type DaySchedule struct {
|
type DaySchedule struct {
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
@@ -66,6 +82,7 @@ type AppSettings struct {
|
|||||||
PenaltyTiers []PenaltyTier `json:"penalty_tiers"` // barème des amendes (liste configurable)
|
PenaltyTiers []PenaltyTier `json:"penalty_tiers"` // barème des amendes (liste configurable)
|
||||||
PointsEnabled bool `json:"points_enabled"` // afficher/activer le système de points
|
PointsEnabled bool `json:"points_enabled"` // afficher/activer le système de points
|
||||||
PointsPools []PointsPool `json:"points_pools"` // types de points personnalisés
|
PointsPools []PointsPool `json:"points_pools"` // types de points personnalisés
|
||||||
|
PointsReward *PointsReward `json:"points_reward"` // récompense globale par palier de points
|
||||||
ReferralEnabled bool `json:"referral_enabled"` // activer/désactiver le système de parrainage
|
ReferralEnabled bool `json:"referral_enabled"` // activer/désactiver le système de parrainage
|
||||||
ReferralAmount float64 `json:"referral_amount"` // montant crédité par parrainage
|
ReferralAmount float64 `json:"referral_amount"` // montant crédité par parrainage
|
||||||
CryptoPaymentEnabled bool `json:"crypto_payment_enabled"` // activer/désactiver le paiement crypto
|
CryptoPaymentEnabled bool `json:"crypto_payment_enabled"` // activer/désactiver le paiement crypto
|
||||||
@@ -81,4 +98,5 @@ type AppSettings struct {
|
|||||||
DeliveryMode DeliveryModeConfig `json:"delivery_mode"` // mode d'assignation des livreurs
|
DeliveryMode DeliveryModeConfig `json:"delivery_mode"` // mode d'assignation des livreurs
|
||||||
ShopName string `json:"shop_name"` // nom affiché dans la sidebar du site client
|
ShopName string `json:"shop_name"` // nom affiché dans la sidebar du site client
|
||||||
Telegram2FAEnabled bool `json:"telegram_2fa_enabled"` // activer/désactiver l'authentification à deux facteurs
|
Telegram2FAEnabled bool `json:"telegram_2fa_enabled"` // activer/désactiver l'authentification à deux facteurs
|
||||||
|
ContactTelegram string `json:"contact_telegram"` // numéro de téléphone Telegram du contact
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
// ============================================
|
|
||||||
// routes/routes.go - VERSION CORRIGÉE COMPLÈTE
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
package routes
|
package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -116,6 +112,10 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
|
|||||||
cartGroupV1.GET("/referral/balance", handlers.GetMyReferralBalance)
|
cartGroupV1.GET("/referral/balance", handlers.GetMyReferralBalance)
|
||||||
cartGroupV1.GET("/parrain", handlers.GetMyParrainInfo)
|
cartGroupV1.GET("/parrain", handlers.GetMyParrainInfo)
|
||||||
|
|
||||||
|
// 🏆 POINTS & RÉCOMPENSES CLIENT
|
||||||
|
cartGroupV1.GET("/points/rewards", handlers.GetMyPointsRewards)
|
||||||
|
cartGroupV1.POST("/points/claim", handlers.ClaimMyReward)
|
||||||
|
|
||||||
// 💸 STATUT PAIEMENT CRYPTO
|
// 💸 STATUT PAIEMENT CRYPTO
|
||||||
cartGroupV1.GET("/commands/:id/payment-status", handlers.GetCommandPaymentStatus)
|
cartGroupV1.GET("/commands/:id/payment-status", handlers.GetCommandPaymentStatus)
|
||||||
}
|
}
|
||||||
@@ -130,6 +130,11 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
|
|||||||
// ============================================
|
// ============================================
|
||||||
router.POST("/webhook/telegram", handlers.TelegramWebhook)
|
router.POST("/webhook/telegram", handlers.TelegramWebhook)
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 🔗 LIAISON INTERNE TELEGRAM (appelée par LBTelegram)
|
||||||
|
// ============================================
|
||||||
|
router.POST("/api/internal/telegram/link", handlers.InternalTelegramLink)
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// 📋 PATTERN v2: ADMIN API
|
// 📋 PATTERN v2: ADMIN API
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -266,6 +271,7 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
|
|||||||
adminGroupV2.POST("/client/:username/point/reset", handlers.ResetClientPointAdmin) // Reset points → 0
|
adminGroupV2.POST("/client/:username/point/reset", handlers.ResetClientPointAdmin) // Reset points → 0
|
||||||
adminGroupV2.POST("/client/:username/points/add", handlers.AddClientPointsAdmin) // Ajouter points par pool
|
adminGroupV2.POST("/client/:username/points/add", handlers.AddClientPointsAdmin) // Ajouter points par pool
|
||||||
adminGroupV2.POST("/client/:username/points/subtract", handlers.SubtractClientPointsAdmin) // Enlever points par pool
|
adminGroupV2.POST("/client/:username/points/subtract", handlers.SubtractClientPointsAdmin) // Enlever points par pool
|
||||||
|
adminGroupV2.POST("/client/:username/rewards/reset", handlers.AdminResetClientRedeemed) // Reset récompenses réclamées
|
||||||
adminGroupV2.GET("/penalties/all", handlers.GetAllClientsWithPenalties) // Liste clients avec pénalités
|
adminGroupV2.GET("/penalties/all", handlers.GetAllClientsWithPenalties) // Liste clients avec pénalités
|
||||||
adminGroupV2.GET("/penalties/stats", handlers.GetPenaltiesStats)
|
adminGroupV2.GET("/penalties/stats", handlers.GetPenaltiesStats)
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,3 @@
|
|||||||
// ============================================
|
|
||||||
// services/address_correction.go
|
|
||||||
// Correction automatique des adresses mal orthographiées
|
|
||||||
// Stratégie : Nominatim fuzzy → suggestions structurées → fallback
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var LBTelegram *LBTelegramService
|
||||||
|
|
||||||
|
type LBTelegramService struct {
|
||||||
|
gatewayURL string
|
||||||
|
Bot1Username string
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLBTelegramService() *LBTelegramService {
|
||||||
|
url := os.Getenv("LBTELEGRAM_URL")
|
||||||
|
if url == "" {
|
||||||
|
url = "http://lbtelegram:8081"
|
||||||
|
}
|
||||||
|
svc := &LBTelegramService{
|
||||||
|
gatewayURL: url,
|
||||||
|
Bot1Username: os.Getenv("LBTELEGRAM_BOT1_USERNAME"),
|
||||||
|
client: &http.Client{Timeout: 10 * time.Second},
|
||||||
|
}
|
||||||
|
LBTelegram = svc
|
||||||
|
return svc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LBTelegramService) IsConfigured() bool {
|
||||||
|
return os.Getenv("LBTELEGRAM_URL") != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnrollUser enrôle un utilisateur auprès de LBTelegram après liaison du compte.
|
||||||
|
// LBTelegram envoie lui-même le message de confirmation (chaîne Bot1→Bot2→Bot3).
|
||||||
|
func (s *LBTelegramService) EnrollUser(chatID int64, username, role string) error {
|
||||||
|
payload := map[string]interface{}{
|
||||||
|
"user_id": chatID,
|
||||||
|
"username": username,
|
||||||
|
"role": role,
|
||||||
|
"chat_id": chatID,
|
||||||
|
}
|
||||||
|
|
||||||
|
body, _ := json.Marshal(payload)
|
||||||
|
resp, err := s.client.Post(s.gatewayURL+"/enrollment/begin", "application/json", bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("enrollment: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
b, _ := io.ReadAll(resp.Body)
|
||||||
|
return fmt.Errorf("enrollment HTTP %d: %s", resp.StatusCode, string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("✅ [LB] Enrollment OK pour %s (%s)", username, role)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendNotification envoie un message via la gateway LBTelegram.
|
||||||
|
// Le bot est choisi automatiquement selon la stratégie configurée (failover/roundrobin/leastconn).
|
||||||
|
func (s *LBTelegramService) SendNotification(userID int64, message string) error {
|
||||||
|
payload := map[string]interface{}{
|
||||||
|
"user_id": userID,
|
||||||
|
"message": message,
|
||||||
|
}
|
||||||
|
|
||||||
|
body, _ := json.Marshal(payload)
|
||||||
|
resp, err := s.client.Post(s.gatewayURL+"/notify", "application/json", bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("notify: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
b, _ := io.ReadAll(resp.Body)
|
||||||
|
return fmt.Errorf("notify HTTP %d: %s", resp.StatusCode, string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user