chore: fix prices

This commit is contained in:
2026-03-02 03:28:45 +01:00
parent e7a6dc2230
commit 6c8af9acd4
10 changed files with 266 additions and 41 deletions
+91 -11
View File
@@ -10,9 +10,9 @@ import (
// AddProductInBasket ajoute un produit au panier de l'utilisateur
func (d *Database) AddProductInBasket(username, nameProduct string, quantity float64, category string) (*models.Panier, error) {
// Rechercher le produit par son nom et catégorie
// Rechercher le produit par son nom et catégorie (case-insensitive)
var productID int
productQuery := `SELECT id FROM products WHERE name = $1 AND category = $2`
productQuery := `SELECT id FROM products WHERE LOWER(name) = LOWER($1) AND LOWER(category) = LOWER($2)`
err := d.QueryRow(productQuery, nameProduct, category).Scan(&productID)
if err == sql.ErrNoRows {
return nil, fmt.Errorf("produit '%s' non trouvé dans la catégorie '%s'", nameProduct, category)
@@ -29,18 +29,19 @@ func (d *Database) AddProductInBasket(username, nameProduct string, quantity flo
// Vérifier si le produit existe déjà dans le panier
var existingID int
var existingQuantity float64
checkQuery := `SELECT id, quantity FROM baskets WHERE username = $1 AND product_id = $2`
err = d.QueryRow(checkQuery, username, productID).Scan(&existingID, &existingQuantity)
var existingQuantity, existingPrice float64
checkQuery := `SELECT id, quantity, price FROM baskets WHERE username = $1 AND product_id = $2`
err = d.QueryRow(checkQuery, username, productID).Scan(&existingID, &existingQuantity, &existingPrice)
if err == nil {
// Produit déjà dans le panier : mettre à jour quantité et prix
// Produit déjà dans le panier : cumuler quantité et prix total de la ligne
newQuantity := existingQuantity + quantity
newPrice := existingPrice + price
updateQuery := `UPDATE baskets SET quantity = $1, price = $2, created_at = CURRENT_TIMESTAMP
WHERE id = $3 RETURNING id, username, product_id, quantity, price, created_at`
var basket models.Panier
err = d.QueryRow(updateQuery, newQuantity, price, existingID).Scan(
err = d.QueryRow(updateQuery, newQuantity, newPrice, existingID).Scan(
&basket.ID,
&basket.Username,
&basket.ProductID,
@@ -75,7 +76,85 @@ func (d *Database) AddProductInBasket(username, nameProduct string, quantity flo
return &basket, nil
}
// GetProductPrice récupère le prix réel d'un produit pour une quantité donnée
// GetProductPriceByID récupère le prix d'un produit par son ID et quantité (NUMERIC exact)
func (d *Database) GetProductPriceByID(productID int, quantity float64) (float64, error) {
var price float64
// Comparaison NUMERIC précise : évite les problèmes float64 vs NUMERIC(10,3)
exactQuery := `
SELECT price FROM product_prices
WHERE product_id = $1 AND quantity = ROUND($2::NUMERIC, 3)
LIMIT 1
`
err := d.QueryRow(exactQuery, productID, quantity).Scan(&price)
if err == nil {
return price, nil
}
// Fallback : palier inférieur le plus proche
tierQuery := `
SELECT price FROM product_prices
WHERE product_id = $1 AND quantity <= ROUND($2::NUMERIC, 3)
ORDER BY quantity DESC LIMIT 1
`
err = d.QueryRow(tierQuery, productID, quantity).Scan(&price)
if err != nil {
return 0, fmt.Errorf("aucun prix trouvé pour product_id=%d qty=%.3f: %w", productID, quantity, err)
}
return price, nil
}
// GetProductStockByID récupère le stock d'un produit par son ID
func (d *Database) GetProductStockByID(productID int) (float64, error) {
var stock float64
err := d.QueryRow(`SELECT stock FROM products WHERE id = $1`, productID).Scan(&stock)
if err != nil {
return 0, fmt.Errorf("produit %d non trouvé: %w", productID, err)
}
return stock, nil
}
// AddProductInBasketByID ajoute un produit au panier en utilisant son ID directement
func (d *Database) AddProductInBasketByID(username string, productID int, quantity float64) (*models.Panier, error) {
price, err := d.GetProductPriceByID(productID, quantity)
if err != nil {
return nil, fmt.Errorf("erreur récupération prix: %w", err)
}
var existingID int
var existingQuantity, existingPrice float64
checkQuery := `SELECT id, quantity, price FROM baskets WHERE username = $1 AND product_id = $2`
err = d.QueryRow(checkQuery, username, productID).Scan(&existingID, &existingQuantity, &existingPrice)
if err == nil {
newQuantity := existingQuantity + quantity
newPrice := existingPrice + price
updateQuery := `UPDATE baskets SET quantity = $1, price = $2, created_at = CURRENT_TIMESTAMP
WHERE id = $3 RETURNING id, username, product_id, quantity, price, created_at`
var basket models.Panier
err = d.QueryRow(updateQuery, newQuantity, newPrice, existingID).Scan(
&basket.ID, &basket.Username, &basket.ProductID, &basket.Quantity, &basket.Price, &basket.CreatedAt,
)
if err != nil {
return nil, fmt.Errorf("erreur lors de la mise à jour du panier: %w", err)
}
return &basket, nil
}
insertQuery := `INSERT INTO baskets (username, product_id, quantity, price, created_at)
VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP)
RETURNING id, username, product_id, quantity, price, created_at`
var basket models.Panier
err = d.QueryRow(insertQuery, username, productID, quantity, price).Scan(
&basket.ID, &basket.Username, &basket.ProductID, &basket.Quantity, &basket.Price, &basket.CreatedAt,
)
if err != nil {
return nil, fmt.Errorf("erreur lors de l'ajout au panier: %w", err)
}
return &basket, nil
}
// GetProductPrice récupère le prix réel d'un produit pour une quantité donnée (legacy)
func (d *Database) GetProductPrice(name, category string, quantity float64) (float64, error) {
var price float64
@@ -100,7 +179,7 @@ func (d *Database) GetProductPrice(name, category string, quantity float64) (flo
func (d *Database) GetProductStock(name, category string) (float64, error) {
var stock float64
query := `SELECT stock FROM products WHERE name = $1 AND category = $2`
query := `SELECT stock FROM products WHERE LOWER(name) = LOWER($1) AND LOWER(category) = LOWER($2)`
err := d.QueryRow(query, name, category).Scan(&stock)
if err != nil {
return 0, fmt.Errorf("produit non trouvé: %w", err)
@@ -109,7 +188,7 @@ func (d *Database) GetProductStock(name, category string) (float64, error) {
}
func (d *Database) DecrementProductStock(name, category string, quantity float64) error {
query := `UPDATE products SET stock = stock - $1 WHERE name = $2 AND category = $3 AND stock >= $1`
query := `UPDATE products SET stock = stock - $1 WHERE LOWER(name) = LOWER($2) AND LOWER(category) = LOWER($3) AND stock >= $1`
result, err := d.Exec(query, quantity, name, category)
if err != nil {
return fmt.Errorf("erreur mise à jour stock: %w", err)
@@ -223,8 +302,9 @@ func (d *Database) ClearBasket(username string) error {
}
// GetBasketTotal calcule le montant total du panier d'un utilisateur
// price dans baskets = prix total de la ligne (cumul des ajouts)
func (d *Database) GetBasketTotal(username string) (float64, error) {
query := `SELECT COALESCE(SUM(quantity * price), 0) as total
query := `SELECT COALESCE(SUM(price), 0) as total
FROM baskets
WHERE username = $1`
+19
View File
@@ -990,3 +990,22 @@ func (d *Database) CanUserAccessCommand(
return exists, err
}
// SaveClientPushToken enregistre le push token Expo d'un client
func (d *Database) SaveClientPushToken(clientID int, pushToken string) error {
_, err := d.Exec(`UPDATE clients SET push_token = $1 WHERE id = $2`, pushToken, clientID)
return err
}
// DeleteClientPushToken supprime le push token d'un client
func (d *Database) DeleteClientPushToken(clientID int) error {
_, err := d.Exec(`UPDATE clients SET push_token = NULL WHERE id = $1`, clientID)
return err
}
// GetClientPushToken retourne le push token d'un client par son username
func (d *Database) GetClientPushToken(username string) (string, error) {
var token string
err := d.QueryRow(`SELECT COALESCE(push_token, '') FROM clients WHERE username = $1`, username).Scan(&token)
return token, err
}
+7 -2
View File
@@ -71,11 +71,16 @@ func InitDB() *Database {
log.Println("✅ Tables créées avec succès")
// Migration: ajouter colonne must_change_password si elle n'existe pas
if _, err = database.Exec(`ALTER TABLE clients ADD COLUMN IF NOT EXISTS must_change_password BOOLEAN NOT NULL DEFAULT TRUE`); err != nil {
// Migration: ajouter colonne must_change_password si elle n'existe pas (DEFAULT FALSE pour les clients existants)
if _, err = database.Exec(`ALTER TABLE clients ADD COLUMN IF NOT EXISTS must_change_password BOOLEAN NOT NULL DEFAULT FALSE`); err != nil {
log.Fatalf("❌ Erreur migration must_change_password: %v", err)
}
// Migration: ajouter colonne push_token pour les notifications push
if _, err = database.Exec(`ALTER TABLE clients ADD COLUMN IF NOT EXISTS push_token TEXT`); err != nil {
log.Fatalf("❌ Erreur migration push_token: %v", err)
}
// Lancer le nettoyage périodique des tokens expirés
go database.cleanExpiredTokensPeriodically()
+48 -1
View File
@@ -1,9 +1,11 @@
package db
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"time"
)
@@ -21,12 +23,57 @@ func (d *Database) NotifyClient(username string, commandID int, notifType, messa
notifJSON, _ := json.Marshal(notification)
Redis.LPush(RedisCtx, notifKey, notifJSON)
Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour) // Expire après 7 jours
Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour)
// Envoyer push notification si le client a un token enregistré
pushToken, err := d.GetClientPushToken(username)
if err == nil && pushToken != "" {
go sendExpoPush(pushToken, "Uber Stup", message, commandID, notifType)
}
log.Printf("📬 Notification envoyée à %s: %s", username, message)
return nil
}
func sendExpoPush(token, title, body string, commandID int, notifType string) {
payload := map[string]interface{}{
"to": token,
"title": title,
"body": body,
"channelId": "orders",
"data": map[string]interface{}{
"command_id": commandID,
"type": notifType,
},
"sound": "default",
}
jsonBody, err := json.Marshal(payload)
if err != nil {
log.Printf("❌ [EXPO_PUSH] Erreur marshal: %v", err)
return
}
req, err := http.NewRequest("POST", "https://exp.host/--/api/v2/push/send", bytes.NewBuffer(jsonBody))
if err != nil {
log.Printf("❌ [EXPO_PUSH] Erreur création requête: %v", err)
return
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
req.Header.Set("Accept-Encoding", "gzip, deflate")
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
log.Printf("❌ [EXPO_PUSH] Erreur envoi: %v", err)
return
}
defer resp.Body.Close()
log.Printf("✅ [EXPO_PUSH] Push envoyé à %s (status: %d)", token, resp.StatusCode)
}
// AddDeliveryRating ajoute une note pour un livreur
func (d *Database) AddDeliveryRating(livreurUsername string, commandID, rating int, comment string) error {
query := `