chore: build
Frontend Admin - EAS Build / build (push) Canceled after 0s
Frontend Client - EAS Build / build (push) Canceled after 0s
Backend - Build & Lint / build (push) Failing after 25m18s
Frontend Web - Build & Lint / build (push) Failing after 9m58s

This commit is contained in:
Xor290
2026-08-06 12:06:05 +02:00
parent c034088bee
commit 22a8d5026c
174 changed files with 30315 additions and 16120 deletions
+104 -19
View File
@@ -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
}