chore: build
Backend - Build & Lint / build (push) Canceled after 24m25s
Frontend Admin - EAS Build / build (push) Successful in 1h35m12s

This commit is contained in:
Xor290
2026-09-13 22:07:04 +02:00
parent d7d7496a66
commit d9688acbc2
4 changed files with 212 additions and 21 deletions
+6 -2
View File
@@ -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,
+10 -8
View File
@@ -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"],
}
}
@@ -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>
<Text style={[styles.itemPrice, prod.is_reward ? { color: "#10b981" } : {}]}>
{prod.is_reward && (prod.prix ?? 0) === 0 ? "Offert" : `${(prod.prix ?? 0).toFixed(2)}`}
</Text>
{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>
<Text style={[styles.detailProductPrice, prod.is_reward ? { color: "#10b981" } : {}]}>
{prod.is_reward && (prod.prix ?? 0) === 0 ? "Offert" : `${(prod.prix ?? 0).toFixed(2)}`}
</Text>
{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>