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