diff --git a/backend/gestion/db/db_command_items.go b/backend/gestion/db/db_command_items.go index ba894e1d..23ed5a24 100644 --- a/backend/gestion/db/db_command_items.go +++ b/backend/gestion/db/db_command_items.go @@ -233,6 +233,7 @@ func (d *Database) GetCommandItems(commandID int) ([]map[string]any, error) { ProductID *int64 `gorm:"column:product_id"` Quantite float64 `gorm:"column:quantite"` Prix float64 `gorm:"column:prix"` + PromoDiscount float64 `gorm:"column:promo_discount"` IsReward bool `gorm:"column:is_reward"` RewardPoolKey string `gorm:"column:reward_pool_key"` ClientUsername string `gorm:"column:client_username"` @@ -262,6 +263,7 @@ func (d *Database) GetCommandItems(commandID int) ([]map[string]any, error) { ci.product_id, ci.quantite, ci.prix, + ci.promo_discount, ci.is_reward, ci.reward_pool_key, ci.client_username, @@ -310,6 +312,7 @@ func (d *Database) GetCommandItems(commandID int) ([]map[string]any, error) { "product_id": productIDValue, "quantite": row.Quantite, "prix": row.Prix, + "promo_discount": row.PromoDiscount, "is_reward": row.IsReward, "reward_pool_key": row.RewardPoolKey, "client_username": row.ClientUsername, @@ -353,6 +356,7 @@ func (d *Database) GetCommandItemsBatch(commandIDs []int) (map[int][]map[string] ProductID *int64 `gorm:"column:product_id"` Quantite float64 `gorm:"column:quantite"` Prix float64 `gorm:"column:prix"` + PromoDiscount float64 `gorm:"column:promo_discount"` IsReward bool `gorm:"column:is_reward"` RewardPoolKey string `gorm:"column:reward_pool_key"` ClientUsername string `gorm:"column:client_username"` @@ -377,7 +381,7 @@ func (d *Database) GetCommandItemsBatch(commandIDs []int) (map[int][]map[string] err := d.GDB.Raw(` SELECT ci.id, ci.command_id, ci.produit, ci.product_id, - ci.quantite, ci.prix, ci.is_reward, ci.reward_pool_key, + ci.quantite, ci.prix, ci.promo_discount, ci.is_reward, ci.reward_pool_key, ci.client_username, ci.client_nom, ci.client_prenom, ci.client_telephone, ci.delivery_address, ci.status, ci.created_at, ci.updated_at, c.status as command_status, c.adresse as command_address, @@ -407,7 +411,7 @@ func (d *Database) GetCommandItemsBatch(commandIDs []int) (map[int][]map[string] item := map[string]any{ "id": row.ID, "command_id": row.CommandID, "produit": row.Produit, "product_id": productIDValue, - "quantite": row.Quantite, "prix": row.Prix, + "quantite": row.Quantite, "prix": row.Prix, "promo_discount": row.PromoDiscount, "is_reward": row.IsReward, "reward_pool_key": row.RewardPoolKey, "client_username": row.ClientUsername, "client_nom": row.ClientNom, "client_prenom": row.ClientPrenom, "client_telephone": row.ClientTelephone, diff --git a/backend/gestion/handlers/deleviry.go b/backend/gestion/handlers/deleviry.go index 0e36d91c..4e4414fd 100644 --- a/backend/gestion/handlers/deleviry.go +++ b/backend/gestion/handlers/deleviry.go @@ -73,10 +73,11 @@ func GetMyDeliveries(c *gin.Context) { itemsSummary := make([]gin.H, len(items)) for j, item := range items { itemsSummary[j] = gin.H{ - "produit": item["produit"], - "quantite": item["quantite"], - "prix": item["prix"], - "is_reward": item["is_reward"], + "produit": item["produit"], + "quantite": item["quantite"], + "prix": item["prix"], + "promo_discount": item["promo_discount"], + "is_reward": item["is_reward"], } } @@ -155,10 +156,11 @@ func GetDeliveryDetails(c *gin.Context) { itemsSummary := make([]gin.H, len(items)) for i, item := range items { itemsSummary[i] = gin.H{ - "produit": item["produit"], - "quantite": item["quantite"], - "prix": item["prix"], - "is_reward": item["is_reward"], + "produit": item["produit"], + "quantite": item["quantite"], + "prix": item["prix"], + "promo_discount": item["promo_discount"], + "is_reward": item["is_reward"], } } diff --git a/backend/gestion/tests/deleviry_promo_test.go b/backend/gestion/tests/deleviry_promo_test.go new file mode 100644 index 00000000..ba448f7f --- /dev/null +++ b/backend/gestion/tests/deleviry_promo_test.go @@ -0,0 +1,135 @@ +package tests + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "gestion/handlers" + + "github.com/gin-gonic/gin" +) + +func myDeliveriesContext(username, status string) (*gin.Context, *httptest.ResponseRecorder) { + url := "/api/v1/livreur/deliveries" + if status != "" { + url += "?status=" + status + } + req := httptest.NewRequest(http.MethodGet, url, nil) + rec := httptest.NewRecorder() + c, _ := gin.CreateTestContext(rec) + c.Request = req + c.Set("database", testDB) + c.Set("username", username) + c.Set("role", "livreur") + return c, rec +} + +// Le livreur doit voir qu'un article a bénéficié d'une promotion de prix +// (promo_discount > 0), pour pouvoir justifier au client un montant total +// inférieur au prix catalogue — voir GetDeliveryDetails/GetMyDeliveries +// (backend/gestion/handlers/deleviry.go) et GetCommandItems/GetCommandItemsBatch +// (backend/gestion/db/db_command_items.go). +func TestGetDeliveryDetails_ExposesPromoDiscountPerItem(t *testing.T) { + cleanupStockTestData(t) + client := newTestClient(t, "delivpromo_client") + livreur := newTestClient(t, "delivpromo_livreur") + productID := newTestProduct(t, "DelivPromoDiscount", 20) + + // 3g normalement à 50€, facturés 25€ (-50%) : promo_discount = 25€. + cmdID := newTestCommandWithItem(t, client, "en_route", livreur, productID, 3, 25) + if err := testDB.GDB.Exec( + `UPDATE command_items SET promo_discount = 25 WHERE command_id = ? AND product_id = ?`, + cmdID, productID, + ).Error; err != nil { + t.Fatalf("mise à jour promo_discount: %v", err) + } + + c, rec := deliveryDetailsContext(livreur, cmdID) + handlers.GetDeliveryDetails(c) + + if rec.Code != http.StatusOK { + t.Fatalf("status HTTP: got=%d body=%s", rec.Code, rec.Body.String()) + } + + var resp struct { + Success bool `json:"success"` + Delivery struct { + Items []struct { + Produit string `json:"produit"` + Prix float64 `json:"prix"` + PromoDiscount float64 `json:"promo_discount"` + } `json:"items"` + } `json:"delivery"` + } + if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { + t.Fatalf("décodage réponse: %v body=%s", err, rec.Body.String()) + } + if !resp.Success || len(resp.Delivery.Items) != 1 { + t.Fatalf("réponse inattendue: body=%s", rec.Body.String()) + } + + item := resp.Delivery.Items[0] + if item.PromoDiscount != 25 { + t.Errorf("promo_discount doit être exposé au livreur: got=%.2f want=25.00 (body=%s)", item.PromoDiscount, rec.Body.String()) + } + if item.Prix != 25 { + t.Errorf("le prix affiché doit rester le prix déjà réduit facturé: got=%.2f want=25.00", item.Prix) + } +} + +// Même vérification côté GetMyDeliveries (liste des livraisons), qui passe +// par un chemin de requête différent (GetCommandItemsBatch) que +// GetDeliveryDetails (GetCommandItems). +func TestGetMyDeliveries_ExposesPromoDiscountPerItem(t *testing.T) { + cleanupStockTestData(t) + client := newTestClient(t, "delivpromo_list_client") + livreur := newTestClient(t, "delivpromo_list_livreur") + productID := newTestProduct(t, "DelivPromoListDiscount", 20) + + cmdID := newTestCommandWithItem(t, client, "en_route", livreur, productID, 3, 25) + if err := testDB.GDB.Exec( + `UPDATE command_items SET promo_discount = 25 WHERE command_id = ? AND product_id = ?`, + cmdID, productID, + ).Error; err != nil { + t.Fatalf("mise à jour promo_discount: %v", err) + } + + c, rec := myDeliveriesContext(livreur, "") + handlers.GetMyDeliveries(c) + + if rec.Code != http.StatusOK { + t.Fatalf("status HTTP: got=%d body=%s", rec.Code, rec.Body.String()) + } + + var resp struct { + Success bool `json:"success"` + Deliveries []struct { + ID int `json:"id"` + Items []struct { + PromoDiscount float64 `json:"promo_discount"` + } `json:"items"` + } `json:"deliveries"` + } + if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { + t.Fatalf("décodage réponse: %v body=%s", err, rec.Body.String()) + } + if !resp.Success { + t.Fatalf("réponse non successful: body=%s", rec.Body.String()) + } + + var found bool + for _, d := range resp.Deliveries { + if d.ID != cmdID { + continue + } + if len(d.Items) != 1 || d.Items[0].PromoDiscount != 25 { + t.Fatalf("promo_discount doit être exposé dans GetMyDeliveries: %+v", d.Items) + } + found = true + } + if !found { + t.Fatalf("commande %d introuvable dans la réponse: body=%s", cmdID, rec.Body.String()) + } +} diff --git a/frontend-admin/src/screens/delivery/DashboardScreen.tsx b/frontend-admin/src/screens/delivery/DashboardScreen.tsx index 626da74d..3911d0a0 100644 --- a/frontend-admin/src/screens/delivery/DashboardScreen.tsx +++ b/frontend-admin/src/screens/delivery/DashboardScreen.tsx @@ -75,7 +75,7 @@ interface EnrichedDelivery extends DeliveryItem { clientUsername?: string; clientNom?: string; clientPrenom?: string; - items?: Array<{ produit: string; quantite: number; prix: number; unit?: string; is_reward?: boolean }>; + items?: Array<{ produit: string; quantite: number; prix: number; unit?: string; is_reward?: boolean; promo_discount?: number }>; } export default function DashboardScreen() { @@ -707,7 +707,14 @@ export default function DashboardScreen() { )} - {item.items.map((prod, idx) => ( + {item.items.map((prod, idx) => { + const promoDiscount = prod.promo_discount ?? 0; + const hasPromo = !prod.is_reward && promoDiscount > 0; + const originalPrice = (prod.prix ?? 0) + promoDiscount; + const promoPercent = hasPromo && originalPrice > 0 + ? Math.round((promoDiscount / originalPrice) * 100) + : 0; + return ( @@ -718,16 +725,34 @@ export default function DashboardScreen() { Récompense )} + {hasPromo && ( + + + -{promoPercent}% + + )} Quantité: {prod.quantite}{prod.unit || ""} - - {prod.is_reward && (prod.prix ?? 0) === 0 ? "Offert" : `${(prod.prix ?? 0).toFixed(2)}€`} - + {hasPromo ? ( + + + {originalPrice.toFixed(2)}€ + + + {(prod.prix ?? 0).toFixed(2)}€ + + + ) : ( + + {prod.is_reward && (prod.prix ?? 0) === 0 ? "Offert" : `${(prod.prix ?? 0).toFixed(2)}€`} + + )} - ))} + ); + })} Total @@ -2010,7 +2035,14 @@ export default function DashboardScreen() { )} - {detailsDelivery.items.map((prod, idx) => ( + {detailsDelivery.items.map((prod, idx) => { + const promoDiscount = prod.promo_discount ?? 0; + const hasPromo = !prod.is_reward && promoDiscount > 0; + const originalPrice = (prod.prix ?? 0) + promoDiscount; + const promoPercent = hasPromo && originalPrice > 0 + ? Math.round((promoDiscount / originalPrice) * 100) + : 0; + return ( @@ -2021,16 +2053,34 @@ export default function DashboardScreen() { Récompense )} + {hasPromo && ( + + + -{promoPercent}% + + )} Quantité : {prod.quantite}{prod.unit || ""} - - {prod.is_reward && (prod.prix ?? 0) === 0 ? "Offert" : `${(prod.prix ?? 0).toFixed(2)}€`} - + {hasPromo ? ( + + + {originalPrice.toFixed(2)}€ + + + {(prod.prix ?? 0).toFixed(2)}€ + + + ) : ( + + {prod.is_reward && (prod.prix ?? 0) === 0 ? "Offert" : `${(prod.prix ?? 0).toFixed(2)}€`} + + )} - ))} + ); + })} ) : ( Aucun produit