906 lines
29 KiB
Go
906 lines
29 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"`
|
|
}
|
|
|
|
func (d *Database) fetchBasketItems(username string) ([]basketItem, float64, error) {
|
|
var items []basketItem
|
|
if err := d.GDB.Table("baskets").Select("product_id, quantity, price").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,
|
|
}
|
|
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,
|
|
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
|
|
}
|