chore: build
This commit is contained in:
@@ -78,9 +78,14 @@ func (d *Database) CancelCommandAtomic(commandID int, username, reason string, f
|
||||
|
||||
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 {
|
||||
SET stock = stock + agg.total_qty, updated_at = CURRENT_TIMESTAMP
|
||||
FROM (
|
||||
SELECT product_id, SUM(quantite) AS total_qty
|
||||
FROM command_items
|
||||
WHERE command_id = ?
|
||||
GROUP BY product_id
|
||||
) agg
|
||||
WHERE agg.product_id = p.id`, commandID).Error; err != nil {
|
||||
return fmt.Errorf("erreur remboursement stock: %w", err)
|
||||
}
|
||||
log.Printf("✅ [CancelAtomic] Stock remboursé")
|
||||
@@ -145,20 +150,18 @@ 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)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -192,13 +195,18 @@ func (d *Database) DeleteCommandAtomic(commandID int, deletedBy, role string) er
|
||||
log.Printf("📋 [DeleteAtomic] Trouvée - status=%s, client=%s", cmdResult.Status, cmdResult.Username)
|
||||
|
||||
// ✅ Ne restitue le stock QUE si pas déjà fait
|
||||
stockAlreadyRestored := cmdResult.Status == "cancelled" || cmdResult.Status == "approved"
|
||||
stockAlreadyRestored := cmdResult.Status == "cancelled" || cmdResult.Status == "approved" || cmdResult.Status == "livre"
|
||||
if !stockAlreadyRestored {
|
||||
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 {
|
||||
SET stock = stock + agg.total_qty, updated_at = CURRENT_TIMESTAMP
|
||||
FROM (
|
||||
SELECT product_id, SUM(quantite) AS total_qty
|
||||
FROM command_items
|
||||
WHERE command_id = ?
|
||||
GROUP BY product_id
|
||||
) agg
|
||||
WHERE agg.product_id = p.id`, commandID).Error; err != nil {
|
||||
log.Printf("⚠️ [DeleteAtomic] Erreur remboursement: %v", err)
|
||||
} else {
|
||||
log.Printf("✅ [DeleteAtomic] Stock remboursé (statut: %s)", cmdResult.Status)
|
||||
@@ -316,12 +324,89 @@ 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 + agg.total_qty, updated_at = CURRENT_TIMESTAMP
|
||||
FROM (
|
||||
SELECT product_id, SUM(quantite) AS total_qty
|
||||
FROM command_items
|
||||
WHERE command_id = ?
|
||||
GROUP BY product_id
|
||||
) agg
|
||||
WHERE agg.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
|
||||
})
|
||||
}
|
||||
|
||||
// CancelDeliveryByLivreurAtomic annule une commande côté livreur et restaure le stock
|
||||
// de manière atomique (verrou FOR UPDATE + transition conditionnée à l'ancien statut).
|
||||
// Idempotent : si la commande est déjà annulée, ne touche pas au stock et renvoie
|
||||
// alreadyCancelled=true — évite un remboursement en double en cas de double appel
|
||||
// (double-tap, retry réseau, ou commande déjà annulée par un autre canal).
|
||||
func (d *Database) CancelDeliveryByLivreurAtomic(commandID int) (alreadyCancelled bool, prevStatus string, err error) {
|
||||
err = d.GDB.Transaction(func(tx *gorm.DB) error {
|
||||
if e := tx.Raw(`SELECT status FROM commandes WHERE id = ? FOR UPDATE`, commandID).Scan(&prevStatus).Error; e != nil {
|
||||
return e
|
||||
}
|
||||
if prevStatus == "" {
|
||||
return fmt.Errorf("commande non trouvée")
|
||||
}
|
||||
if prevStatus == "cancelled" {
|
||||
alreadyCancelled = true
|
||||
return nil
|
||||
}
|
||||
|
||||
result := tx.Exec(`
|
||||
UPDATE commandes SET status = 'cancelled', updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ? AND status = ?`, commandID, prevStatus)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("commande déjà modifiée par une autre requête")
|
||||
}
|
||||
|
||||
if e := tx.Exec(`
|
||||
UPDATE products p
|
||||
SET stock = stock + agg.total_qty, updated_at = CURRENT_TIMESTAMP
|
||||
FROM (
|
||||
SELECT product_id, SUM(quantite) AS total_qty
|
||||
FROM command_items
|
||||
WHERE command_id = ?
|
||||
GROUP BY product_id
|
||||
) agg
|
||||
WHERE agg.product_id = p.id`, commandID).Error; e != nil {
|
||||
return fmt.Errorf("erreur remboursement stock: %w", e)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user