chore: build
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"gestion/handlers"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func deliveryDetailsContext(username string, commandID int) (*gin.Context, *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/livreur/deliveries/%d", commandID), nil)
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = req
|
||||
c.Params = gin.Params{{Key: "id", Value: fmt.Sprintf("%d", commandID)}}
|
||||
c.Set("database", testDB)
|
||||
c.Set("username", username)
|
||||
c.Set("role", "livreur")
|
||||
return c, rec
|
||||
}
|
||||
|
||||
// GetDeliveryDetails doit exposer is_reward par item, au même titre que
|
||||
// GetMyDeliveries (la liste) — sans quoi le modal "détails" côté livreur ne
|
||||
// peut pas signaler un article récompense (gratuit ou -50%), ni afficher son
|
||||
// prix effectif correctement.
|
||||
func TestGetDeliveryDetails_ExposesIsRewardPerItem(t *testing.T) {
|
||||
cleanupStockTestData(t)
|
||||
client := newTestClient(t, "delivdetails_client")
|
||||
livreur := newTestClient(t, "delivdetails_livreur")
|
||||
paidProductID := newTestProduct(t, "DelivDetailsPaid", 20)
|
||||
rewardProductID := newTestProduct(t, "DelivDetailsReward", 5)
|
||||
|
||||
cmdID := newTestCommandWithItem(t, client, "en_route", livreur, paidProductID, 1, 10)
|
||||
insertRewardCommandItem(t, cmdID, rewardProductID, 1, 5, "pool_0")
|
||||
|
||||
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"`
|
||||
IsReward bool `json:"is_reward"`
|
||||
} `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 {
|
||||
t.Fatalf("réponse non successful: body=%s", rec.Body.String())
|
||||
}
|
||||
if len(resp.Delivery.Items) != 2 {
|
||||
t.Fatalf("nombre d'items: got=%d want=2", len(resp.Delivery.Items))
|
||||
}
|
||||
|
||||
var sawReward, sawPaid bool
|
||||
for _, it := range resp.Delivery.Items {
|
||||
if it.IsReward {
|
||||
sawReward = true
|
||||
} else {
|
||||
sawPaid = true
|
||||
}
|
||||
}
|
||||
if !sawReward {
|
||||
t.Errorf("l'item récompense doit avoir is_reward=true dans la réponse: %+v", resp.Delivery.Items)
|
||||
}
|
||||
if !sawPaid {
|
||||
t.Errorf("l'item payant doit avoir is_reward=false dans la réponse: %+v", resp.Delivery.Items)
|
||||
}
|
||||
}
|
||||
@@ -204,7 +204,7 @@ func TestCalculateAndAddPointsForCommandTx_RewardItemDeductsThresholdFromPoolPoi
|
||||
setPointsPoolsSettings(t, []models.PointsPool{
|
||||
{Key: "pool_0", Name: "Pool Test", Categories: []string{"test"}, Tiers: []models.PointsTier{{Min: 0, Max: 0, Points: 3}}},
|
||||
})
|
||||
setPointsRewardSettings(t, models.PointsReward{Threshold: 20, Type: "free_product"})
|
||||
setPointsRewardSettings(t, models.PointsReward{Threshold: 20})
|
||||
setClientPoolPoints(t, username, "pool_0", 25) // solde de départ avant cette commande
|
||||
|
||||
cmdID := newTestCommandWithItem(t, username, "livre", "", paidProductID, 1, 10)
|
||||
|
||||
@@ -24,9 +24,14 @@ func claimRewardContext(username string, body []byte) (*gin.Context, *httptest.R
|
||||
return c, rec
|
||||
}
|
||||
|
||||
// configureRewardSettings applique la récompense donnée, avec pool_0 mappé
|
||||
// sur la catégorie "test" — nécessaire pour que eligibleRewardProducts
|
||||
// (qui croise pool.Categories et reward.CategoryConfigs) considère les
|
||||
// reward_items comme éligibles.
|
||||
func configureRewardSettings(t *testing.T, reward *models.PointsReward) {
|
||||
t.Helper()
|
||||
settings := db.DefaultSettings()
|
||||
settings.PointsPools[0].Categories = []string{"test"}
|
||||
settings.PointsReward = reward
|
||||
if err := testDB.UpdateSettings(settings); err != nil {
|
||||
t.Fatalf("UpdateSettings: %v", err)
|
||||
@@ -41,10 +46,10 @@ func TestClaimMyReward_HTTPFlow_AddsRewardToBasketAndDecrementsAvailable(t *test
|
||||
rewardProductID := newTestProduct(t, "RewardHTTPFlow", 5)
|
||||
|
||||
configureRewardSettings(t, &models.PointsReward{
|
||||
Threshold: 20,
|
||||
Type: "free_product",
|
||||
Description: "Un produit offert",
|
||||
RewardItems: []models.RewardItem{{ProductID: rewardProductID, Quantity: 1, Price: 12}},
|
||||
Threshold: 20,
|
||||
Description: "Un produit offert",
|
||||
CategoryConfigs: []models.RewardCategoryConfig{{Category: "test", Type: "free_product", AllProducts: true}},
|
||||
RewardItems: []models.RewardItem{{ProductID: rewardProductID, Quantity: 1, Price: 12}},
|
||||
})
|
||||
setClientPoolPoints(t, username, "pool_0", 20)
|
||||
|
||||
@@ -75,6 +80,43 @@ func TestClaimMyReward_HTTPFlow_AddsRewardToBasketAndDecrementsAvailable(t *test
|
||||
if len(rows) != 1 || rows[0].ProductID != rewardProductID {
|
||||
t.Errorf("le produit récompense doit être dans le panier: %+v", rows)
|
||||
}
|
||||
if rows[0].Price != 0 {
|
||||
t.Errorf("catégorie free_product: le prix en panier doit être 0: got=%.2f", rows[0].Price)
|
||||
}
|
||||
}
|
||||
|
||||
// Catégorie configurée en "half_price_product" : le produit récompense doit
|
||||
// être ajouté au panier à 50% du prix catalogue actif (pas 0€, pas le prix
|
||||
// indicatif RewardItem.Price saisi par l'admin).
|
||||
func TestClaimMyReward_HTTPFlow_HalfPriceCategoryChargesFiftyPercentOfCatalogPrice(t *testing.T) {
|
||||
cleanupStockTestData(t)
|
||||
username := newTestClient(t, "reward_http_halfprice")
|
||||
rewardProductID := newTestProduct(t, "RewardHTTPHalfPrice", 5)
|
||||
// newTestProduct crée un prix actif de 10.00€ pour quantity=1 (voir tests/main_test.go).
|
||||
|
||||
configureRewardSettings(t, &models.PointsReward{
|
||||
Threshold: 20,
|
||||
Description: "Un produit à moitié prix",
|
||||
CategoryConfigs: []models.RewardCategoryConfig{{Category: "test", Type: "half_price_product", AllProducts: true}},
|
||||
RewardItems: []models.RewardItem{{ProductID: rewardProductID, Quantity: 1, Price: 999}}, // Price indicatif, doit être ignoré
|
||||
})
|
||||
setClientPoolPoints(t, username, "pool_0", 20)
|
||||
|
||||
body, _ := json.Marshal(map[string]string{"pool_key": "pool_0"})
|
||||
c, rec := claimRewardContext(username, body)
|
||||
handlers.ClaimMyReward(c)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status HTTP: got=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
rows := basketRewardItems(t, username)
|
||||
if len(rows) != 1 || rows[0].ProductID != rewardProductID {
|
||||
t.Fatalf("le produit récompense doit être dans le panier: %+v", rows)
|
||||
}
|
||||
if rows[0].Price != 5.0 {
|
||||
t.Errorf("catégorie half_price_product: prix attendu = 50%% de 10.00€ = 5.00€: got=%.2f", rows[0].Price)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaimMyReward_HTTPFlow_RejectsWhenBelowThreshold(t *testing.T) {
|
||||
@@ -83,9 +125,9 @@ func TestClaimMyReward_HTTPFlow_RejectsWhenBelowThreshold(t *testing.T) {
|
||||
rewardProductID := newTestProduct(t, "RewardHTTPBelow", 5)
|
||||
|
||||
configureRewardSettings(t, &models.PointsReward{
|
||||
Threshold: 20,
|
||||
Type: "free_product",
|
||||
RewardItems: []models.RewardItem{{ProductID: rewardProductID, Quantity: 1, Price: 12}},
|
||||
Threshold: 20,
|
||||
CategoryConfigs: []models.RewardCategoryConfig{{Category: "test", Type: "free_product", AllProducts: true}},
|
||||
RewardItems: []models.RewardItem{{ProductID: rewardProductID, Quantity: 1, Price: 12}},
|
||||
})
|
||||
setClientPoolPoints(t, username, "pool_0", 5)
|
||||
|
||||
@@ -108,9 +150,12 @@ func TestClaimMyReward_HTTPFlow_FailsAtomicallyWhenProductMissing(t *testing.T)
|
||||
username := newTestClient(t, "reward_http_missing_product")
|
||||
|
||||
configureRewardSettings(t, &models.PointsReward{
|
||||
Threshold: 20,
|
||||
Type: "free_product",
|
||||
RewardItems: []models.RewardItem{{ProductID: 999999999, Quantity: 1, Price: 12}}, // produit inexistant
|
||||
Threshold: 20,
|
||||
// ProductIDs explicite (pas AllProducts) : le produit n'existe pas en
|
||||
// base, donc il n'apparaîtrait jamais dans productCategories et ne
|
||||
// serait jamais éligible via une correspondance AllProducts.
|
||||
CategoryConfigs: []models.RewardCategoryConfig{{Category: "test", Type: "free_product", ProductIDs: []int{999999999}}},
|
||||
RewardItems: []models.RewardItem{{ProductID: 999999999, Quantity: 1, Price: 12}}, // produit inexistant
|
||||
})
|
||||
setClientPoolPoints(t, username, "pool_0", 20)
|
||||
|
||||
|
||||
@@ -206,7 +206,12 @@ func TestClaimPoolRewardAndAddToBasket_RollsBackBothOnInvalidProduct(t *testing.
|
||||
|
||||
// ── AddRewardsToBasket : flags et remplacement ──────────────────────────────
|
||||
|
||||
func TestAddRewardsToBasket_SetsRewardFlagsAndZeroPrice(t *testing.T) {
|
||||
// AddRewardsToBasket ne recalcule plus le prix : elle stocke tel quel le
|
||||
// RewardItem.Price fourni par l'appelant (0 pour "free_product", prix -50%
|
||||
// déjà résolu par handlers/points.go pour "half_price_product") — voir
|
||||
// TestClaimMyReward_HTTPFlow_HalfPriceCategoryChargesFiftyPercentOfCatalogPrice
|
||||
// pour le flux complet qui résout ce prix par type de catégorie.
|
||||
func TestAddRewardsToBasket_SetsRewardFlagsAndStoresGivenPrice(t *testing.T) {
|
||||
cleanupStockTestData(t)
|
||||
username := newTestClient(t, "reward_basket_flags")
|
||||
productID := newTestProduct(t, "RewardBasketFlags", 20)
|
||||
@@ -231,8 +236,8 @@ func TestAddRewardsToBasket_SetsRewardFlagsAndZeroPrice(t *testing.T) {
|
||||
if row.RewardPoolKey != "pool_0" {
|
||||
t.Errorf("reward_pool_key: got=%q want=%q", row.RewardPoolKey, "pool_0")
|
||||
}
|
||||
if row.Price != 0 {
|
||||
t.Errorf("prix affiché doit être 0 (gratuit): got=%.2f", row.Price)
|
||||
if row.Price != 15.0 {
|
||||
t.Errorf("le prix fourni par l'appelant doit être stocké tel quel: got=%.2f want=15.00", row.Price)
|
||||
}
|
||||
if row.Quantity != 2 {
|
||||
t.Errorf("quantité: got=%.2f want=2", row.Quantity)
|
||||
|
||||
@@ -278,6 +278,55 @@ func TestUpdateProduct_UpdatesStockWhenProvided(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// productPriceActiveFlags relit active_price par palier de quantité pour un
|
||||
// produit, directement en base.
|
||||
func productPriceActiveFlags(t *testing.T, productID int) map[float64]bool {
|
||||
t.Helper()
|
||||
var rows []struct {
|
||||
Quantity float64 `gorm:"column:quantity"`
|
||||
ActivePrice bool `gorm:"column:active_price"`
|
||||
}
|
||||
if err := testDB.GDB.Raw(
|
||||
`SELECT quantity, active_price FROM product_prices WHERE product_id = ?`, productID,
|
||||
).Scan(&rows).Error; err != nil {
|
||||
t.Fatalf("lecture active_price: %v", err)
|
||||
}
|
||||
out := make(map[float64]bool, len(rows))
|
||||
for _, r := range rows {
|
||||
out[r.Quantity] = r.ActivePrice
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Un prix explicitement désactivé (active_price=false) doit rester désactivé
|
||||
// après UpdateProduct — piège classique de GORM : un champ bool à sa valeur
|
||||
// zéro (false) avec un tag gorm "default" est omis de l'INSERT, laissant la
|
||||
// base appliquer son propre défaut (TRUE) à la place. Voir le commentaire sur
|
||||
// ProductPrice.ActivePrice dans models/product.go.
|
||||
func TestUpdateProduct_PersistsInactivePriceFlag(t *testing.T) {
|
||||
cleanupStockTestData(t)
|
||||
cat := newTestCategory(t, "CatInactive")
|
||||
productID := newTestProduct(t, "UPInactive", 5)
|
||||
|
||||
body := []byte(fmt.Sprintf(
|
||||
`{"name":"UPInactive","category":%q,"description":"d2","unit":"g","stock":10,"prices":[{"quantity":1,"price":5,"active_price":false},{"quantity":5,"price":20,"active_price":true}]}`,
|
||||
cat,
|
||||
))
|
||||
c, rec := updateProductJSONContext("admin", body, productID)
|
||||
handlers.UpdateProduct(c)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("UpdateProduct valide doit réussir: got=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
flags := productPriceActiveFlags(t, productID)
|
||||
if flags[1] != false {
|
||||
t.Errorf("palier qty=1 doit rester désactivé après UpdateProduct: got active_price=%v", flags[1])
|
||||
}
|
||||
if flags[5] != true {
|
||||
t.Errorf("palier qty=5 doit rester actif après UpdateProduct: got active_price=%v", flags[5])
|
||||
}
|
||||
}
|
||||
|
||||
// ── DeleteProduct ────────────────────────────────────────────────────────
|
||||
|
||||
func TestDeleteProduct_RemovesProductWithoutMedia(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user