chore: build
This commit is contained in:
@@ -150,8 +150,13 @@ func (d *Database) AddToBasket(username string, productID int, quantity float64)
|
||||
}
|
||||
// Une promotion active pour ce produit/quantité/catégorie s'applique
|
||||
// automatiquement au prix facturé — indépendamment des points de
|
||||
// fidélité (contrairement aux récompenses par palier).
|
||||
// fidélité (contrairement aux récompenses par palier). Le montant
|
||||
// économisé est conservé (promoDiscount) pour les statistiques
|
||||
// admin, indépendamment de la config de promo courante au moment où
|
||||
// ces stats seront consultées.
|
||||
var promoDiscount float64
|
||||
if discounted, ok := d.ApplyPromotionToPrice(productID, productInfo.Category, quantity, priceResult.Price); ok {
|
||||
promoDiscount = priceResult.Price - discounted
|
||||
priceResult.Price = discounted
|
||||
}
|
||||
|
||||
@@ -167,26 +172,27 @@ func (d *Database) AddToBasket(username string, productID int, quantity float64)
|
||||
}
|
||||
|
||||
var existing struct {
|
||||
ID int `gorm:"column:id"`
|
||||
Quantity float64 `gorm:"column:quantity"`
|
||||
Price float64 `gorm:"column:price"`
|
||||
ID int `gorm:"column:id"`
|
||||
Quantity float64 `gorm:"column:quantity"`
|
||||
Price float64 `gorm:"column:price"`
|
||||
PromoDiscount float64 `gorm:"column:promo_discount"`
|
||||
}
|
||||
// Chercher uniquement un item normal (non-récompense) pour ce produit
|
||||
tx.Raw(`SELECT id, quantity, price FROM baskets WHERE username = ? AND product_id = ? AND is_reward = false`,
|
||||
tx.Raw(`SELECT id, quantity, price, promo_discount FROM baskets WHERE username = ? AND product_id = ? AND is_reward = false`,
|
||||
username, productID).Scan(&existing)
|
||||
|
||||
if existing.ID != 0 {
|
||||
return tx.Raw(`
|
||||
UPDATE baskets SET quantity = ?, price = ?, created_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ? AND is_reward = false RETURNING id, username, product_id, quantity, price, is_reward, created_at`,
|
||||
UPDATE baskets SET quantity = ?, price = ?, promo_discount = ?, created_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ? AND is_reward = false RETURNING id, username, product_id, quantity, price, is_reward, promo_discount, created_at`,
|
||||
existing.Quantity+deliveredQuantity, existing.Price+priceResult.Price,
|
||||
existing.ID).Scan(&basket).Error
|
||||
existing.PromoDiscount+promoDiscount, existing.ID).Scan(&basket).Error
|
||||
}
|
||||
return tx.Raw(`
|
||||
INSERT INTO baskets (username, product_id, quantity, price, is_reward, created_at)
|
||||
VALUES (?, ?, ?, ?, false, CURRENT_TIMESTAMP)
|
||||
RETURNING id, username, product_id, quantity, price, is_reward, created_at`,
|
||||
username, productID, deliveredQuantity, priceResult.Price).Scan(&basket).Error
|
||||
INSERT INTO baskets (username, product_id, quantity, price, is_reward, promo_discount, created_at)
|
||||
VALUES (?, ?, ?, ?, false, ?, CURRENT_TIMESTAMP)
|
||||
RETURNING id, username, product_id, quantity, price, is_reward, promo_discount, created_at`,
|
||||
username, productID, deliveredQuantity, priceResult.Price, promoDiscount).Scan(&basket).Error
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -19,6 +19,7 @@ type commandItemFull struct {
|
||||
Prix float64 `gorm:"column:prix"`
|
||||
IsReward bool `gorm:"column:is_reward"`
|
||||
RewardPoolKey string `gorm:"column:reward_pool_key"`
|
||||
PromoDiscount float64 `gorm:"column:promo_discount"`
|
||||
ClientUsername string `gorm:"column:client_username"`
|
||||
ClientNom string `gorm:"column:client_nom"`
|
||||
ClientPrenom string `gorm:"column:client_prenom"`
|
||||
|
||||
@@ -55,6 +55,7 @@ type basketItem struct {
|
||||
Price float64 `gorm:"column:price"`
|
||||
IsReward bool `gorm:"column:is_reward"`
|
||||
RewardPoolKey string `gorm:"column:reward_pool_key"`
|
||||
PromoDiscount float64 `gorm:"column:promo_discount"`
|
||||
}
|
||||
|
||||
// validateCommandStatus vérifie si le statut est valide
|
||||
@@ -109,7 +110,7 @@ func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (*
|
||||
// bloque ici puis échoue proprement ("panier vide") une fois le premier
|
||||
// passage terminé, au lieu de créer une commande fantôme.
|
||||
var basketItems []basketItem
|
||||
if err := tx.Raw(`SELECT product_id, quantity, price, is_reward, reward_pool_key FROM baskets WHERE username = ? FOR UPDATE`, username).Scan(&basketItems).Error; err != nil {
|
||||
if err := tx.Raw(`SELECT product_id, quantity, price, is_reward, reward_pool_key, promo_discount FROM baskets WHERE username = ? FOR UPDATE`, username).Scan(&basketItems).Error; err != nil {
|
||||
return fmt.Errorf("erreur récupération panier: %w", err)
|
||||
}
|
||||
if len(basketItems) == 0 {
|
||||
@@ -162,6 +163,7 @@ func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (*
|
||||
Prix: item.Price,
|
||||
IsReward: item.IsReward,
|
||||
RewardPoolKey: item.RewardPoolKey,
|
||||
PromoDiscount: item.PromoDiscount,
|
||||
ClientUsername: username,
|
||||
ClientNom: clientNom,
|
||||
ClientPrenom: clientPrenom,
|
||||
|
||||
@@ -140,6 +140,20 @@ func InitDB() *Database {
|
||||
log.Fatalf("❌ Erreur migration command_items.reward_pool_key: %v", err)
|
||||
}
|
||||
|
||||
// Migration: baskets.promo_discount + command_items.promo_discount —
|
||||
// montant (en €) économisé par une promotion de prix sur cette ligne,
|
||||
// capturé une fois pour toutes au moment de AddToBasket (voir
|
||||
// db_basket.go) puis copié tel quel au checkout, pour permettre des
|
||||
// statistiques historiques fiables même si la config de promo change
|
||||
// ensuite (contrairement à un recalcul a posteriori sur les settings
|
||||
// courants, qui donnerait un résultat faux pour les anciennes commandes).
|
||||
if _, err = database.Exec(`ALTER TABLE baskets ADD COLUMN IF NOT EXISTS promo_discount NUMERIC(10,2) NOT NULL DEFAULT 0`); err != nil {
|
||||
log.Fatalf("❌ Erreur migration baskets.promo_discount: %v", err)
|
||||
}
|
||||
if _, err = database.Exec(`ALTER TABLE command_items ADD COLUMN IF NOT EXISTS promo_discount NUMERIC(10,2) NOT NULL DEFAULT 0`); err != nil {
|
||||
log.Fatalf("❌ Erreur migration command_items.promo_discount: %v", err)
|
||||
}
|
||||
|
||||
// Migration: command_items.quantite INTEGER → NUMERIC(10,3) pour supporter les quantités fractionnaires
|
||||
if _, err = database.Exec(`
|
||||
DO $$
|
||||
|
||||
@@ -379,6 +379,39 @@ func (d *Database) TotalRevenue(resetAt time.Time) (float64, error) {
|
||||
return total, err
|
||||
}
|
||||
|
||||
// TotalPromoDiscount renvoie le montant total (€) des réductions de prix
|
||||
// accordées par des promotions sur les commandes approuvées, filtré par le
|
||||
// reset "revenus" (même périmètre que TotalRevenue, dont c'est un
|
||||
// sous-indicateur). Basé sur command_items.promo_discount, capturé au moment
|
||||
// de AddToBasket — reflète donc les promos réellement appliquées à l'époque,
|
||||
// pas la config de promotions courante.
|
||||
func (d *Database) TotalPromoDiscount(resetAt time.Time) (float64, error) {
|
||||
where, args := statusFilterClause("c.status = 'approved'", resetAt, "c.created_at")
|
||||
var total float64
|
||||
query := `
|
||||
SELECT COALESCE(SUM(ci.promo_discount), 0)
|
||||
FROM command_items ci
|
||||
JOIN commandes c ON c.id = ci.command_id
|
||||
WHERE ` + where
|
||||
err := d.GDB.Raw(query, args...).Scan(&total).Error
|
||||
return total, err
|
||||
}
|
||||
|
||||
// PromoOrdersCount renvoie le nombre de commandes distinctes (approuvées)
|
||||
// ayant bénéficié d'au moins une réduction de prix promo, filtré par le
|
||||
// reset "revenus".
|
||||
func (d *Database) PromoOrdersCount(resetAt time.Time) (int64, error) {
|
||||
where, args := statusFilterClause("c.status = 'approved'", resetAt, "c.created_at")
|
||||
var count int64
|
||||
query := `
|
||||
SELECT COUNT(DISTINCT ci.command_id)
|
||||
FROM command_items ci
|
||||
JOIN commandes c ON c.id = ci.command_id
|
||||
WHERE ci.promo_discount > 0 AND ` + where
|
||||
err := d.GDB.Raw(query, args...).Scan(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// ActiveDaysLast30 renvoie le nombre de jours distincts ayant eu au moins une commande sur 30 jours.
|
||||
func (d *Database) ActiveDaysLast30(resetAt time.Time) (int64, error) {
|
||||
where, args := statusFilterClause("status != 'cancelled'", resetAt, "created_at")
|
||||
|
||||
Reference in New Issue
Block a user