Files
projet_gestion_commande/backend/gestion/db/db_commands.go
T
2026-06-14 17:50:35 +02:00

922 lines
30 KiB
Go

package db
import (
"fmt"
"gestion/models"
"log"
"slices"
"strings"
"time"
"gorm.io/gorm"
)
func sanitizeString(s string) string {
sanitized := strings.Map(func(r rune) rune {
if r < 32 || r == 127 {
return -1
}
return r
}, s)
if len(sanitized) > 1000 {
sanitized = sanitized[:1000]
}
return strings.TrimSpace(sanitized)
}
func validateAddress(address string) error {
address = strings.TrimSpace(address)
if address == "" {
return fmt.Errorf("adresse vide non autorisée")
}
if len(address) > 500 {
return fmt.Errorf("adresse trop longue (max 500 caractères)")
}
if strings.ContainsAny(address, "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0B\x0C\x0E\x0F") {
return fmt.Errorf("adresse contient des caractères de contrôle interdits")
}
return nil
}
type basketItem struct {
ProductID int `gorm:"column:product_id"`
Quantity float64 `gorm:"column:quantity"`
Price float64 `gorm:"column:price"`
IsReward bool `gorm:"column:is_reward"`
RewardPoolKey string `gorm:"column:reward_pool_key"`
}
func (d *Database) fetchBasketItems(username string) ([]basketItem, float64, error) {
var items []basketItem
if err := d.GDB.Table("baskets").Select("product_id, quantity, price, is_reward, reward_pool_key").Where("username = ?", username).Scan(&items).Error; err != nil {
return nil, 0, fmt.Errorf("erreur récupération panier: %w", err)
}
total := 0.0
for _, item := range items {
total += item.Price
}
return items, total, nil
}
// validateCommandStatus vérifie si le statut est valide
func validateCommandStatus(status string) error {
validStatuses := map[string]bool{
"pending": true,
"assigned": true,
"en_route": true,
"arrived": true,
"livre": true,
"approved": true,
"cancelled": true,
"disabled": true,
}
if !validStatuses[status] {
return fmt.Errorf("statut invalide: %s", status)
}
return nil
}
func (d *Database) CreateCommand(username string) (*models.Command, error) {
adresse := "Adresse non spécifiée"
var clientCheck models.Client
if err := d.GDB.Select("username").Where("username = ?", username).First(&clientCheck).Error; err == nil && clientCheck.Username != "" {
adresse = clientCheck.Username
}
basketItems, totalPrix, err := d.fetchBasketItems(username)
if err != nil {
return nil, err
}
if len(basketItems) == 0 {
return nil, fmt.Errorf("le panier est vide")
}
var cmdResult struct {
ID int `gorm:"column:id"`
ClientOrderID int `gorm:"column:client_order_id"`
CreatedAt time.Time `gorm:"column:created_at"`
UpdatedAt time.Time `gorm:"column:updated_at"`
}
err = d.GDB.Raw(`
INSERT INTO commandes (username, status, adresse, total_prix, client_order_id, created_at, updated_at)
VALUES (?, ?, ?, ?, (SELECT COALESCE(MAX(client_order_id), 0) + 1 FROM commandes WHERE username = ?), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
RETURNING id, client_order_id, created_at, updated_at`,
username, "pending", adresse, totalPrix, username).Scan(&cmdResult).Error
if err != nil {
return nil, fmt.Errorf("erreur lors de la création de la commande: %w", err)
}
commandID := cmdResult.ID
for _, item := range basketItems {
productName, err := d.GetProductNameByID(item.ProductID)
if err != nil {
productName = "Produit inconnu"
}
cmdItem := models.CommandItem{
CommandID: commandID,
Produit: productName,
ProductID: item.ProductID,
Quantity: item.Quantity,
Price: item.Price,
IsReward: item.IsReward,
RewardPoolKey: item.RewardPoolKey,
}
if err := d.GDB.Create(&cmdItem).Error; err != nil {
return nil, fmt.Errorf("erreur lors de l'insertion des items: %w", err)
}
}
if err := d.GDB.Exec(`DELETE FROM baskets WHERE username = ?`, username).Error; err != nil {
return nil, fmt.Errorf("erreur lors du vidage du panier: %w", err)
}
command := &models.Command{
ID: commandID,
ClientOrderID: cmdResult.ClientOrderID,
Status: "pending",
Total: totalPrix,
}
return command, nil
}
func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (*models.Command, error) {
if err := validateUsername(username); err != nil {
return nil, err
}
if err := validateAddress(deliveryAddress); err != nil {
return nil, err
}
client, err := d.GetClientByUsername(username)
if err != nil {
log.Printf("⚠️ Client non trouvé: %v", err)
}
clientNom := ""
clientPrenom := ""
clientTelephone := ""
if client != nil {
clientNom = sanitizeString(client.Nom)
clientPrenom = sanitizeString(client.Prenom)
clientTelephone = sanitizeString(client.Telephone)
}
basketItems, totalPrix, err := d.fetchBasketItems(username)
if err != nil {
log.Printf("❌ Erreur query basket: %v", err)
return nil, err
}
if len(basketItems) == 0 {
return nil, fmt.Errorf("le panier est vide")
}
for _, item := range basketItems {
if item.ProductID <= 0 || item.Quantity <= 0 || item.Price < 0 {
return nil, fmt.Errorf("données panier invalides")
}
}
if totalPrix <= 0 || totalPrix > 100000 {
return nil, fmt.Errorf("montant de commande invalide: %.2f€", totalPrix)
}
var cmdResult struct {
ID int `gorm:"column:id"`
ClientOrderID int `gorm:"column:client_order_id"`
CreatedAt time.Time `gorm:"column:created_at"`
UpdatedAt time.Time `gorm:"column:updated_at"`
}
err = d.GDB.Raw(`
INSERT INTO commandes (username, status, adresse, total_prix, client_order_id, created_at, updated_at)
VALUES (?, ?, ?, ?, (SELECT COALESCE(MAX(client_order_id), 0) + 1 FROM commandes WHERE username = ?), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
RETURNING id, client_order_id, created_at, updated_at`,
username, "pending", deliveryAddress, totalPrix, username).Scan(&cmdResult).Error
if err != nil {
return nil, fmt.Errorf("erreur création commande: %w", err)
}
commandID := cmdResult.ID
for _, item := range basketItems {
productName, err := d.GetProductNameByID(item.ProductID)
if err != nil || productName == "" {
productName = fmt.Sprintf("Produit #%d", item.ProductID)
}
err = d.InsertCommandItemWithClientInfo(
commandID,
productName,
item.ProductID,
item.Quantity,
item.Price,
item.IsReward,
item.RewardPoolKey,
username,
clientNom,
clientPrenom,
clientTelephone,
deliveryAddress,
)
if err != nil {
log.Printf("❌ Erreur INSERT command_items: %v", err)
return nil, fmt.Errorf("erreur insertion items: %w", err)
}
// Stock déjà déduit à l'ajout au panier — ne pas déduire une seconde fois ici.
}
if err := d.GDB.Delete(&models.Panier{}, "username = ?", username).Error; err != nil {
log.Printf("⚠️ Erreur vidage panier: %v", err)
}
sanitizedAddress := sanitizeLogMessage(deliveryAddress)
d.AddCommandLog(commandID, "created",
fmt.Sprintf("Commande créée - Adresse: %s - Total: %.2f€ - Client: %s %s",
sanitizedAddress, totalPrix, sanitizeLogMessage(clientNom), sanitizeLogMessage(clientPrenom)),
username)
command := &models.Command{
ID: commandID,
ClientOrderID: cmdResult.ClientOrderID,
Username: username,
Status: "pending",
Total: totalPrix,
DeliveryAddress: deliveryAddress,
CreatedAt: cmdResult.CreatedAt,
UpdatedAt: cmdResult.UpdatedAt,
}
return command, nil
}
func (d *Database) GetApprovedCommands() ([]models.Command, error) {
var commands []models.Command
err := d.GDB.Where("status = ?", "approved").Order("created_at DESC").Find(&commands).Error
return commands, err
}
func (d *Database) GetAllCommands(status, username string) ([]map[string]any, error) {
if username != "" {
if err := validateUsername(username); err != nil {
return nil, err
}
}
if status != "" {
if err := validateCommandStatus(status); err != nil {
return nil, err
}
}
var rows []struct {
ID int `gorm:"column:id"`
Username string `gorm:"column:username"`
Status string `gorm:"column:status"`
Adresse string `gorm:"column:adresse"`
TotalPrix float64 `gorm:"column:total_prix"`
LivreurAssign *string `gorm:"column:livreur_assign"`
CreatedAt time.Time `gorm:"column:created_at"`
UpdatedAt time.Time `gorm:"column:updated_at"`
ProposedAddress *string `gorm:"column:proposed_address"`
AddressProposalStatus string `gorm:"column:address_proposal_status"`
ClientOrderNumber int `gorm:"column:client_order_number"`
ReferralUsed float64 `gorm:"column:referral_used"`
CancelReason string `gorm:"column:cancel_reason"`
}
gdb := d.GDB.Table("commandes c").
Select(`c.id, c.username, c.status, c.adresse, c.total_prix,
c.livreur_assign, c.created_at, c.updated_at,
c.proposed_address, c.address_proposal_status,
c.client_order_id AS client_order_number,
COALESCE(c.referral_used, 0) AS referral_used,
COALESCE(c.cancel_reason, '') AS cancel_reason`)
if status == "" {
gdb = gdb.Where("c.status IN ?", []string{"pending", "assigned", "en_route", "arrived", "livre"})
} else {
gdb = gdb.Where("c.status = ?", status)
}
if username != "" {
gdb = gdb.Where("c.username = ?", username)
}
if err := gdb.Order("c.created_at DESC").Limit(1000).Scan(&rows).Error; err != nil {
return nil, fmt.Errorf("erreur lors de la récupération des commandes: %w", err)
}
commands := make([]map[string]any, 0, len(rows))
for _, row := range rows {
command := map[string]any{
"id": row.ID,
"username": row.Username,
"status": row.Status,
"adresse": sanitizeString(row.Adresse),
"total_prix": row.TotalPrix,
"created_at": row.CreatedAt,
"updated_at": row.UpdatedAt,
"address_proposal_status": row.AddressProposalStatus,
"client_order_number": row.ClientOrderNumber,
"referral_used": row.ReferralUsed,
"cancel_reason": row.CancelReason,
}
if row.LivreurAssign != nil {
command["livreur_assign"] = *row.LivreurAssign
} else {
command["livreur_assign"] = nil
}
if row.ProposedAddress != nil {
command["proposed_address"] = *row.ProposedAddress
} else {
command["proposed_address"] = nil
}
commands = append(commands, command)
}
return commands, nil
}
func (d *Database) SetCommandReferralUsed(commandID int, amount float64) error {
return d.GDB.Exec(`UPDATE commandes SET referral_used = ? WHERE id = ?`, amount, commandID).Error
}
func (d *Database) GetCommandByID(id int) (map[string]any, error) {
var row struct {
ID int `gorm:"column:id"`
Username string `gorm:"column:username"`
Status string `gorm:"column:status"`
Adresse string `gorm:"column:adresse"`
TotalPrix float64 `gorm:"column:total_prix"`
LivreurAssign *string `gorm:"column:livreur_assign"`
CreatedAt time.Time `gorm:"column:created_at"`
UpdatedAt time.Time `gorm:"column:updated_at"`
ProposedAddress *string `gorm:"column:proposed_address"`
AddressProposalStatus string `gorm:"column:address_proposal_status"`
ReferralUsed float64 `gorm:"column:referral_used"`
ClientOrderNumber int `gorm:"column:client_order_number"`
CancelReason string `gorm:"column:cancel_reason"`
}
if err := d.GDB.Table("commandes c").
Select(`c.id, c.username, c.status, c.adresse, c.total_prix, c.livreur_assign,
c.created_at, c.updated_at, c.proposed_address, c.address_proposal_status,
c.referral_used, c.client_order_id AS client_order_number,
COALESCE(c.cancel_reason, '') AS cancel_reason`).
Where("c.id = ?", id).
First(&row).Error; err != nil {
return nil, fmt.Errorf("erreur lors de la récupération de la commande: %w", err)
}
if row.ID == 0 {
return nil, fmt.Errorf("commande non trouvée")
}
command := map[string]any{
"id": row.ID,
"username": row.Username,
"status": row.Status,
"adresse": row.Adresse,
"total_prix": row.TotalPrix,
"created_at": row.CreatedAt,
"updated_at": row.UpdatedAt,
"address_proposal_status": row.AddressProposalStatus,
"referral_used": row.ReferralUsed,
"client_order_number": row.ClientOrderNumber,
"cancel_reason": row.CancelReason,
}
if row.LivreurAssign != nil {
command["livreur_assign"] = *row.LivreurAssign
} else {
command["livreur_assign"] = nil
}
if row.ProposedAddress != nil {
command["proposed_address"] = *row.ProposedAddress
} else {
command["proposed_address"] = nil
}
return command, nil
}
// GetClientOrderID retourne le client_order_id (numéro perso du client) pour un commandID global.
// Retourne commandID en fallback si introuvable.
func (d *Database) GetClientOrderID(commandID int) int {
var result struct {
ClientOrderID int `gorm:"column:client_order_id"`
}
if err := d.GDB.Model(&models.Command{}).Select("client_order_id").Where("id = ?", commandID).First(&result).Error; err != nil || result.ClientOrderID == 0 {
return commandID
}
return result.ClientOrderID
}
func (d *Database) UpdateCommandAddress(commandID int, deliveryAddress string) error {
if len(deliveryAddress) > 500 {
return fmt.Errorf("adresse trop longue (max 500 caractères)")
}
if strings.TrimSpace(deliveryAddress) == "" {
return fmt.Errorf("adresse vide non autorisée")
}
result := d.GDB.Model(&models.Command{}).Where("id = ?", commandID).Updates(map[string]any{
"adresse": deliveryAddress,
"updated_at": time.Now(),
})
if result.Error != nil {
return fmt.Errorf("erreur lors de la mise à jour de l'adresse: %w", result.Error)
}
if result.RowsAffected == 0 {
return fmt.Errorf("commande non trouvée")
}
log.Printf("✅ Adresse commande %d mise à jour", commandID)
return nil
}
// ProposeAddressChange propose une nouvelle adresse (admin/cabine) en attente de validation client
func (d *Database) ProposeAddressChange(commandID int, proposedAddress, proposedBy string) error {
if err := validateAddress(proposedAddress); err != nil {
return err
}
result := d.GDB.Model(&models.Command{}).Where("id = ?", commandID).Updates(map[string]any{
"proposed_address": proposedAddress,
"address_proposal_status": "pending",
"updated_at": time.Now(),
})
if result.Error != nil {
return fmt.Errorf("erreur proposition adresse: %w", result.Error)
}
if result.RowsAffected == 0 {
return fmt.Errorf("commande non trouvée")
}
d.AddCommandLog(commandID, "address_proposed",
fmt.Sprintf("Nouvelle adresse proposée par %s: %s", proposedBy, proposedAddress),
proposedBy)
log.Printf("✅ Adresse proposée pour commande %d par %s", commandID, proposedBy)
return nil
}
// RespondToAddressProposal accepte ou refuse la proposition d'adresse
func (d *Database) RespondToAddressProposal(commandID int, clientUsername string, accepted bool) error {
var query string
if accepted {
query = `UPDATE commandes
SET adresse = proposed_address, proposed_address = NULL,
address_proposal_status = 'accepted', updated_at = CURRENT_TIMESTAMP
WHERE id = ? AND username = ? AND address_proposal_status = 'pending'`
} else {
query = `UPDATE commandes
SET proposed_address = NULL, address_proposal_status = 'rejected',
updated_at = CURRENT_TIMESTAMP
WHERE id = ? AND username = ? AND address_proposal_status = 'pending'`
}
result := d.GDB.Exec(query, commandID, clientUsername)
if result.Error != nil {
return fmt.Errorf("erreur réponse proposition adresse: %w", result.Error)
}
if result.RowsAffected == 0 {
return fmt.Errorf("aucune proposition en attente pour cette commande")
}
action := "refusée"
if accepted {
action = "acceptée"
}
d.AddCommandLog(commandID, "address_proposal_"+action,
fmt.Sprintf("Proposition d'adresse %s par le client %s", action, clientUsername),
clientUsername)
log.Printf("✅ Proposition adresse %s pour commande %d", action, commandID)
return nil
}
// UpdateCommandStatus met à jour le statut d'une commande
func (d *Database) UpdateCommandStatus(commandID int, status string) error {
if err := validateCommandStatus(status); err != nil {
return err
}
result := d.GDB.Model(&models.Command{}).Where("id = ?", commandID).Updates(map[string]any{
"status": status,
"updated_at": time.Now(),
})
if result.Error != nil {
return fmt.Errorf("erreur lors de la mise à jour du statut: %w", result.Error)
}
if result.RowsAffected == 0 {
return fmt.Errorf("commande non trouvée")
}
return nil
}
func (d *Database) AddCommandLog(commandID int, status, message, author string) error {
sanitizedMessage := sanitizeLogMessage(message)
sanitizedAuthor := sanitizeLogMessage(author)
if err := d.GDB.Create(&models.CommandLog{
CommandID: commandID,
Status: status,
Message: sanitizedMessage,
Author: sanitizedAuthor,
}).Error; err != nil {
log.Printf("⚠️ Avertissement: impossible d'ajouter le log (table command_logs peut-être manquante): %v", err)
return nil
}
return nil
}
// GetCommandLogs récupère tous les logs d'une commande
func (d *Database) GetCommandLogs(commandID int) ([]map[string]any, error) {
var rows []models.CommandLog
if err := d.GDB.Where("command_id = ?", commandID).Order("created_at ASC").Find(&rows).Error; err != nil {
log.Printf("⚠️ Avertissement: impossible de récupérer les logs: %v", err)
return []map[string]any{}, nil
}
logs := make([]map[string]any, 0, len(rows))
for _, row := range rows {
logs = append(logs, map[string]any{
"id": row.ID,
"command_id": row.CommandID,
"status": row.Status,
"message": row.Message,
"author": row.Author,
"created_at": row.CreatedAt,
})
}
return logs, nil
}
func sanitizeLogMessage(message string) string {
sanitized := strings.Map(func(r rune) rune {
if r < 32 || r == 127 {
return -1
}
return r
}, message)
if len(sanitized) > 1000 {
sanitized = sanitized[:1000]
}
return sanitized
}
func (d *Database) ValidateDeliveryAtomic(commandID int, adminUsername string) (int, error) {
var totalPoints int
var cmdUsernameOut string
var livreurAssignOut string
err := d.GDB.Transaction(func(tx *gorm.DB) error {
var cmd struct {
Status string `gorm:"column:status"`
Username string `gorm:"column:username"`
LivreurAssign string `gorm:"column:livreur_assign"`
TotalPrix float64 `gorm:"column:total_prix"`
}
if err := tx.Raw(`
SELECT status, username, COALESCE(livreur_assign, '') as livreur_assign, total_prix
FROM commandes WHERE id = ? FOR UPDATE`, commandID).Scan(&cmd).Error; err != nil {
log.Printf("❌ [ValidateAtomic] Erreur SELECT: %v", err)
return fmt.Errorf("erreur lecture commande: %w", err)
}
if cmd.Username == "" {
log.Printf("❌ [ValidateAtomic] Commande %d non trouvée", commandID)
return fmt.Errorf("commande non trouvée")
}
log.Printf("📋 [ValidateAtomic] Commande trouvée - status=%s, client=%s, livreur=%s",
cmd.Status, cmd.Username, cmd.LivreurAssign)
validStatuses := []string{"assigned", "en_route", "pending", "livre"}
if !slices.Contains(validStatuses, cmd.Status) {
log.Printf("❌ [ValidateAtomic] Statut invalide pour validation: %s", cmd.Status)
return fmt.Errorf("statut invalide pour validation: %s", cmd.Status)
}
if cmd.Status == "approved" {
log.Printf("⚠️ [ValidateAtomic] Commande %d déjà approuvée", commandID)
return fmt.Errorf("commande déjà approuvée")
}
result := tx.Exec(`
UPDATE commandes SET status = 'approved', updated_at = CURRENT_TIMESTAMP
WHERE id = ? AND status = ?`, commandID, cmd.Status)
if result.Error != nil {
log.Printf("❌ [ValidateAtomic] Erreur UPDATE: %v", result.Error)
return fmt.Errorf("erreur mise à jour statut: %w", result.Error)
}
if result.RowsAffected == 0 {
log.Printf("❌ [ValidateAtomic] Commande %d déjà modifiée (race condition évitée)", commandID)
return fmt.Errorf("commande déjà modifiée par une autre requête")
}
if cmd.Username != "" {
log.Printf("🔍 [ValidateAtomic] Calcul points pour client: %s", cmd.Username)
points, _, err := d.CalculateAndAddPointsForCommandTx(tx, commandID, cmd.Username)
if err != nil {
log.Printf("❌ [ValidateAtomic] Erreur calcul/ajout points: %v", err)
return fmt.Errorf("erreur attribution points: %w", err)
}
totalPoints = points
log.Printf("✅ [ValidateAtomic] %d points attribués à %s", totalPoints, cmd.Username)
if err := tx.Exec(`
UPDATE clients SET command = command + 1, updated_at = CURRENT_TIMESTAMP
WHERE username = ?`, cmd.Username).Error; err != nil {
log.Printf("⚠️ [ValidateAtomic] Erreur incrémentation compteur: %v", err)
} else {
log.Printf("✅ [ValidateAtomic] Compteur commandes incrémenté pour %s", cmd.Username)
}
}
if err := tx.Exec(`
INSERT INTO command_logs (command_id, status, message, author, created_at)
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)`,
commandID, "approved",
fmt.Sprintf("Livraison validée par admin %s - %d points attribués", adminUsername, totalPoints),
adminUsername).Error; err != nil {
log.Printf("⚠️ [ValidateAtomic] Erreur ajout log: %v", err)
}
cmdUsernameOut = cmd.Username
livreurAssignOut = cmd.LivreurAssign
return nil
})
if err != nil {
return 0, err
}
log.Printf("🎉 [ValidateAtomic] SUCCÈS - Commande %d validée, %d points attribués",
commandID, totalPoints)
if livreurAssignOut != "" {
log.Printf("📦 [ValidateAtomic] Optimisation queue pour livreur: %s", livreurAssignOut)
go func() {
if err := d.CompleteDeliveryAndProcessNext(livreurAssignOut, commandID); err != nil {
log.Printf("⚠️ [ValidateAtomic] Erreur optimisation queue: %v", err)
}
}()
}
go func() {
commandCacheKey := fmt.Sprintf("command:%d", commandID)
Redis.Del(RedisCtx, commandCacheKey)
if cmdUsernameOut != "" {
clientCacheKey := fmt.Sprintf("client:%s", cmdUsernameOut)
Redis.Del(RedisCtx, clientCacheKey)
clientCommandsCacheKey := fmt.Sprintf("client:%s:commands", cmdUsernameOut)
Redis.Del(RedisCtx, clientCommandsCacheKey)
}
log.Printf("✅ [ValidateAtomic] Caches invalidés pour cmd %d", commandID)
}()
return totalPoints, nil
}
// ApproveDeliveryAtomic - Version atomique pour approbation client
func (d *Database) ApproveDeliveryAtomic(commandID int, username string) (int, string, error) {
log.Printf("🔒 [ApproveAtomic] START - cmd=%d, client=%s", commandID, username)
var totalPoints int
var pointCategory string
var livreurAssignOut string
err := d.GDB.Transaction(func(tx *gorm.DB) error {
var cmd struct {
Status string `gorm:"column:status"`
Username string `gorm:"column:username"`
LivreurAssign string `gorm:"column:livreur_assign"`
}
if err := tx.Raw(`
SELECT status, username, COALESCE(livreur_assign, '') as livreur_assign
FROM commandes WHERE id = ? FOR UPDATE`, commandID).Scan(&cmd).Error; err != nil {
log.Printf("❌ [ApproveAtomic] Erreur SELECT: %v", err)
return fmt.Errorf("erreur lecture commande: %w", err)
}
if cmd.Username == "" {
log.Printf("❌ [ApproveAtomic] Commande %d non trouvée", commandID)
return fmt.Errorf("commande non trouvée")
}
log.Printf("📋 [ApproveAtomic] Commande trouvée - status=%s, owner=%s", cmd.Status, cmd.Username)
if cmd.Username != username {
log.Printf("❌ [ApproveAtomic] Commande n'appartient pas à %s (propriétaire: %s)",
username, cmd.Username)
return fmt.Errorf("cette commande ne vous appartient pas")
}
if cmd.Status != "livre" {
log.Printf("❌ [ApproveAtomic] Statut invalide: %s (attendu: livre)", cmd.Status)
return fmt.Errorf("commande doit être en statut 'livre' (statut actuel: %s)", cmd.Status)
}
result := tx.Exec(`
UPDATE commandes SET status = 'approved', updated_at = CURRENT_TIMESTAMP
WHERE id = ? AND status = 'livre' AND username = ?`, commandID, username)
if result.Error != nil {
log.Printf("❌ [ApproveAtomic] Erreur UPDATE: %v", result.Error)
return fmt.Errorf("erreur mise à jour statut: %w", result.Error)
}
if result.RowsAffected == 0 {
log.Printf("❌ [ApproveAtomic] Commande %d déjà modifiée (race condition évitée)", commandID)
return fmt.Errorf("commande déjà approuvée ou modifiée")
}
log.Printf("✅ [ApproveAtomic] Statut mis à jour: livre → approved")
pts, cat, err := d.CalculateAndAddPointsForCommandTx(tx, commandID, username)
if err != nil {
log.Printf("❌ [ApproveAtomic] Erreur calcul points: %v", err)
return fmt.Errorf("erreur attribution points: %w", err)
}
totalPoints = pts
pointCategory = cat
log.Printf("✅ [ApproveAtomic] %d points [%s] attribués à %s", totalPoints, pointCategory, username)
if err := tx.Exec(`
UPDATE clients SET command = command + 1, updated_at = CURRENT_TIMESTAMP
WHERE username = ?`, username).Error; err != nil {
log.Printf("⚠️ [ApproveAtomic] Erreur incrémentation compteur: %v", err)
}
if err := tx.Exec(`
INSERT INTO command_logs (command_id, status, message, author, created_at)
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)`,
commandID, "approved",
fmt.Sprintf("Livraison confirmée par le client %s - %d points [%s] attribués", username, totalPoints, pointCategory),
username).Error; err != nil {
log.Printf("⚠️ [ApproveAtomic] Erreur ajout log: %v", err)
}
livreurAssignOut = cmd.LivreurAssign
return nil
})
if err != nil {
return 0, "", err
}
log.Printf("🎉 [ApproveAtomic] SUCCÈS - Commande %d approuvée, %d points [%s] attribués",
commandID, totalPoints, pointCategory)
if livreurAssignOut != "" {
log.Printf("📦 [ApproveAtomic] Optimisation queue pour livreur: %s", livreurAssignOut)
go func() {
if err := d.CompleteDeliveryAndProcessNext(livreurAssignOut, commandID); err != nil {
log.Printf("⚠️ [ApproveAtomic] Erreur optimisation queue: %v", err)
}
}()
}
go func() {
commandCacheKey := fmt.Sprintf("command:%d", commandID)
Redis.Del(RedisCtx, commandCacheKey)
clientCacheKey := fmt.Sprintf("client:%s", username)
Redis.Del(RedisCtx, clientCacheKey)
clientCommandsCacheKey := fmt.Sprintf("client:%s:commands", username)
Redis.Del(RedisCtx, clientCommandsCacheKey)
log.Printf("✅ [ApproveAtomic] Caches invalidés")
}()
return totalPoints, pointCategory, nil
}
// ApproveDeliveryAtomicByStaff - Confirmation de réception par admin ou cabine à la place du client
func (d *Database) ApproveDeliveryAtomicByStaff(commandID int, staffUsername string) (int, string, string, error) {
log.Printf("🔒 [ApproveAtomicStaff] START - cmd=%d, staff=%s", commandID, staffUsername)
var totalPoints int
var pointCategory string
var clientUsernameOut string
var livreurAssignOut string
err := d.GDB.Transaction(func(tx *gorm.DB) error {
var cmd struct {
Status string `gorm:"column:status"`
Username string `gorm:"column:username"`
LivreurAssign string `gorm:"column:livreur_assign"`
}
if err := tx.Raw(`
SELECT status, username, COALESCE(livreur_assign, '') as livreur_assign
FROM commandes WHERE id = ? FOR UPDATE`, commandID).Scan(&cmd).Error; err != nil {
return fmt.Errorf("erreur lecture commande: %w", err)
}
if cmd.Username == "" {
return fmt.Errorf("commande non trouvée")
}
if cmd.Status != "livre" {
return fmt.Errorf("commande doit être en statut 'livre' (statut actuel: %s)", cmd.Status)
}
result := tx.Exec(`
UPDATE commandes SET status = 'approved', updated_at = CURRENT_TIMESTAMP
WHERE id = ? AND status = 'livre'`, commandID)
if result.Error != nil {
return fmt.Errorf("erreur mise à jour statut: %w", result.Error)
}
if result.RowsAffected == 0 {
return fmt.Errorf("commande déjà approuvée ou modifiée")
}
pts, cat, err := d.CalculateAndAddPointsForCommandTx(tx, commandID, cmd.Username)
if err != nil {
return fmt.Errorf("erreur attribution points: %w", err)
}
totalPoints = pts
pointCategory = cat
if err := tx.Exec(`
UPDATE clients SET command = command + 1, updated_at = CURRENT_TIMESTAMP
WHERE username = ?`, cmd.Username).Error; err != nil {
log.Printf("⚠️ [ApproveAtomicStaff] Erreur incrémentation compteur: %v", err)
}
if err := tx.Exec(`
INSERT INTO command_logs (command_id, status, message, author, created_at)
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)`,
commandID, "approved",
fmt.Sprintf("Réception confirmée par %s au nom du client %s - %d points attribués", staffUsername, cmd.Username, totalPoints),
staffUsername).Error; err != nil {
log.Printf("⚠️ [ApproveAtomicStaff] Erreur log: %v", err)
}
clientUsernameOut = cmd.Username
livreurAssignOut = cmd.LivreurAssign
return nil
})
if err != nil {
return 0, "", "", err
}
log.Printf("🎉 [ApproveAtomicStaff] SUCCÈS - cmd=%d approuvée par %s, %d points → client %s",
commandID, staffUsername, totalPoints, clientUsernameOut)
if livreurAssignOut != "" {
go func() {
if err := d.CompleteDeliveryAndProcessNext(livreurAssignOut, commandID); err != nil {
log.Printf("⚠️ [ApproveAtomicStaff] Erreur queue: %v", err)
}
}()
}
go func() {
Redis.Del(RedisCtx, fmt.Sprintf("command:%d", commandID))
Redis.Del(RedisCtx, fmt.Sprintf("client:%s", clientUsernameOut))
Redis.Del(RedisCtx, fmt.Sprintf("client:%s:commands", clientUsernameOut))
}()
return totalPoints, pointCategory, clientUsernameOut, nil
}
func (d *Database) SetCommandCancelReason(commandID int, reason string) error {
if len(reason) > 500 {
reason = reason[:500]
}
return d.GDB.Exec(
`UPDATE commandes SET cancel_reason = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`,
reason, commandID,
).Error
}