chore: fix bug
Backend - Build & Lint / build (push) Has been cancelled

This commit is contained in:
Nuxgrid
2026-07-11 11:45:13 +02:00
parent f531ddcda8
commit f674b6ef80
29 changed files with 3621 additions and 313 deletions
+42 -12
View File
@@ -145,20 +145,23 @@ func (d *Database) CancelCommandAtomic(commandID int, username, reason string, f
return penalty, nil
}
// CheckCommandETAExistsAndValid vérifie si une ETA RÉELLE existe (> 0 minutes, non expirée)
// CheckCommandETAExistsAndValid vérifie si une ETA RÉELLE existe (> 0 minutes, non expirée).
// La clé command:eta:{id} est un hash (HSet) — un Redis.Get dessus renvoie
// toujours une erreur WRONGTYPE, ce qui faisait échouer cette vérification à
// chaque appel (aucune annulation n'était jamais détectée comme tardive via
// ce chemin, seul le statut en_route/arrived était pris en compte).
func (d *Database) CheckCommandETAExistsAndValid(commandID int) bool {
etaKey := fmt.Sprintf("command:eta:%d", commandID)
etaMinutesStr, err := Redis.Get(RedisCtx, etaKey).Result()
if err != nil {
etaData, err := Redis.HGetAll(RedisCtx, etaKey).Result()
if err != nil || len(etaData) == 0 {
log.Printf("⚠️ [CheckETA] Pas d'ETA trouvée pour cmd %d", commandID)
return false
}
var etaMinutes int
_, err = fmt.Sscanf(etaMinutesStr, "%d", &etaMinutes)
if err != nil || etaMinutes <= 0 {
log.Printf("⚠️ [CheckETA] ETA invalide pour cmd %d: %s", commandID, etaMinutesStr)
if _, err := fmt.Sscanf(etaData["eta_minutes"], "%d", &etaMinutes); err != nil || etaMinutes <= 0 {
log.Printf("⚠️ [CheckETA] ETA invalide pour cmd %d: %s", commandID, etaData["eta_minutes"])
return false
}
@@ -316,13 +319,40 @@ func (d *Database) AddClientPenalty(username string, points int) error {
return nil
}
func (d *Database) RestoreCommandStock(commandID int) error {
// CancelCommandByAdminAtomic transitionne une commande vers 'cancelled' depuis le
// panel admin/cabine de façon atomique (verrou FOR UPDATE sur la commande) : le
// remboursement de stock et le changement de statut se font dans la même
// transaction, conditionnés à une lecture du statut précédent faite sous verrou.
// Corrige un double remboursement possible sur double-tap/appel concurrent —
// l'ancien code (RestoreCommandStock + UpdateCommandStatus appelés séparément
// par le handler) lisait le statut puis restaurait le stock hors transaction,
// laissant une fenêtre où deux requêtes concurrentes lisaient toutes les deux
// "pas encore annulée" et remboursaient chacune le stock.
func (d *Database) CancelCommandByAdminAtomic(commandID int) error {
return d.GDB.Transaction(func(tx *gorm.DB) error {
return tx.Exec(`
UPDATE products p
SET stock = stock + ci.quantite, updated_at = CURRENT_TIMESTAMP
FROM command_items ci
WHERE ci.command_id = ? AND ci.product_id = p.id`, commandID).Error
var prevStatus string
if err := tx.Raw(`SELECT status FROM commandes WHERE id = ? FOR UPDATE`, commandID).Scan(&prevStatus).Error; err != nil {
return err
}
if prevStatus == "" {
return fmt.Errorf("commande non trouvée")
}
noRestoreStatuses := []string{"cancelled", "approved", "livre"}
if !slices.Contains(noRestoreStatuses, prevStatus) {
if err := tx.Exec(`
UPDATE products p
SET stock = stock + ci.quantite, updated_at = CURRENT_TIMESTAMP
FROM command_items ci
WHERE ci.command_id = ? AND ci.product_id = p.id`, commandID).Error; err != nil {
return fmt.Errorf("erreur remboursement stock: %w", err)
}
}
if err := tx.Exec(`UPDATE commandes SET status = 'cancelled', updated_at = CURRENT_TIMESTAMP WHERE id = ?`, commandID).Error; err != nil {
return fmt.Errorf("erreur mise à jour statut: %w", err)
}
return nil
})
}