Compare commits
1
Commits
d7d7496a66
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9688acbc2 |
@@ -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,
|
||||
|
||||
@@ -76,6 +76,7 @@ func GetMyDeliveries(c *gin.Context) {
|
||||
"produit": item["produit"],
|
||||
"quantite": item["quantite"],
|
||||
"prix": item["prix"],
|
||||
"promo_discount": item["promo_discount"],
|
||||
"is_reward": item["is_reward"],
|
||||
}
|
||||
}
|
||||
@@ -158,6 +159,7 @@ func GetDeliveryDetails(c *gin.Context) {
|
||||
"produit": item["produit"],
|
||||
"quantite": item["quantite"],
|
||||
"prix": item["prix"],
|
||||
"promo_discount": item["promo_discount"],
|
||||
"is_reward": item["is_reward"],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{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 (
|
||||
<View key={idx} style={styles.itemRow}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: 4 }}>
|
||||
@@ -718,16 +725,34 @@ export default function DashboardScreen() {
|
||||
<Text style={{ fontSize: 10, color: "#f59e0b", fontWeight: "700" }}>Récompense</Text>
|
||||
</View>
|
||||
)}
|
||||
{hasPromo && (
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: 2, backgroundColor: "rgba(34,197,94,0.15)", borderRadius: 4, paddingHorizontal: 4, paddingVertical: 1 }}>
|
||||
<Ionicons name="pricetag-outline" size={10} color="#22c55e" />
|
||||
<Text style={{ fontSize: 10, color: "#22c55e", fontWeight: "700" }}>-{promoPercent}%</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<Text style={styles.itemQty}>
|
||||
Quantité: {prod.quantite}{prod.unit || ""}
|
||||
</Text>
|
||||
</View>
|
||||
{hasPromo ? (
|
||||
<View style={{ alignItems: "flex-end" }}>
|
||||
<Text style={{ fontSize: 12, color: colors.textMuted, textDecorationLine: "line-through" }}>
|
||||
{originalPrice.toFixed(2)}€
|
||||
</Text>
|
||||
<Text style={[styles.itemPrice, { color: "#22c55e" }]}>
|
||||
{(prod.prix ?? 0).toFixed(2)}€
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<Text style={[styles.itemPrice, prod.is_reward ? { color: "#10b981" } : {}]}>
|
||||
{prod.is_reward && (prod.prix ?? 0) === 0 ? "Offert" : `${(prod.prix ?? 0).toFixed(2)}€`}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
<View style={styles.totalRow}>
|
||||
<Text style={styles.totalLabel}>Total</Text>
|
||||
<Text style={styles.totalValue}>
|
||||
@@ -2010,7 +2035,14 @@ export default function DashboardScreen() {
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{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 (
|
||||
<View key={idx} style={styles.detailProductRow}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: 4 }}>
|
||||
@@ -2021,16 +2053,34 @@ export default function DashboardScreen() {
|
||||
<Text style={{ fontSize: 11, color: "#f59e0b", fontWeight: "700" }}>Récompense</Text>
|
||||
</View>
|
||||
)}
|
||||
{hasPromo && (
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: 2, backgroundColor: "rgba(34,197,94,0.15)", borderRadius: 4, paddingHorizontal: 4, paddingVertical: 1 }}>
|
||||
<Ionicons name="pricetag-outline" size={11} color="#22c55e" />
|
||||
<Text style={{ fontSize: 11, color: "#22c55e", fontWeight: "700" }}>-{promoPercent}%</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<Text style={styles.detailProductQty}>
|
||||
Quantité : {prod.quantite}{prod.unit || ""}
|
||||
</Text>
|
||||
</View>
|
||||
{hasPromo ? (
|
||||
<View style={{ alignItems: "flex-end" }}>
|
||||
<Text style={{ fontSize: 12, color: colors.textMuted, textDecorationLine: "line-through" }}>
|
||||
{originalPrice.toFixed(2)}€
|
||||
</Text>
|
||||
<Text style={[styles.detailProductPrice, { color: "#22c55e" }]}>
|
||||
{(prod.prix ?? 0).toFixed(2)}€
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<Text style={[styles.detailProductPrice, prod.is_reward ? { color: "#10b981" } : {}]}>
|
||||
{prod.is_reward && (prod.prix ?? 0) === 0 ? "Offert" : `${(prod.prix ?? 0).toFixed(2)}€`}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<Text style={styles.detailEmpty}>Aucun produit</Text>
|
||||
|
||||
Reference in New Issue
Block a user