Files
projet_gestion_commande/backend/gestion/tests/stats_test.go
T
Xor290 22a8d5026c
Frontend Admin - EAS Build / build (push) Canceled after 0s
Frontend Client - EAS Build / build (push) Canceled after 0s
Backend - Build & Lint / build (push) Failing after 25m18s
Frontend Web - Build & Lint / build (push) Failing after 9m58s
chore: build
2026-08-06 12:06:05 +02:00

209 lines
7.6 KiB
Go

package tests
import (
"gestion/models"
"testing"
"time"
)
// newTestOrderForStats crée directement une commande (+ un item) avec un
// statut, un total, un crédit de parrainage utilisé et une date de création
// contrôlés, pour tester isolément les calculs de stats admin.
func newTestOrderForStats(t *testing.T, username, status string, productID int, quantite, prix, referralUsed float64, createdAt time.Time) int {
t.Helper()
var cmdID int
if err := testDB.GDB.Raw(
`INSERT INTO commandes (username, status, adresse, total_prix, referral_used, created_at, updated_at)
VALUES (?, ?, 'Adresse test', ?, ?, ?, ?) RETURNING id`,
username, status, prix, referralUsed, createdAt, createdAt,
).Scan(&cmdID).Error; err != nil {
t.Fatalf("création commande stats test: %v", err)
}
if err := testDB.GDB.Exec(
`INSERT INTO command_items (command_id, product_id, produit, quantite, prix, status)
VALUES (?, ?, 'item test', ?, ?, 'pending')`,
cmdID, productID, quantite, prix,
).Error; err != nil {
t.Fatalf("création item stats test: %v", err)
}
return cmdID
}
// TotalRevenue ne compte que les commandes approuvées, nettes du crédit de
// parrainage utilisé — les commandes annulées ou encore en cours sont exclues.
func TestTotalRevenue_NetsReferralAndExcludesNonApproved(t *testing.T) {
cleanupStockTestData(t)
username := newTestClient(t, "stats_total_revenue")
productID := newTestProduct(t, "StatsTotalRevenue", 100)
now := time.Now()
newTestOrderForStats(t, username, "approved", productID, 5, 100, 30, now) // net 70
newTestOrderForStats(t, username, "approved", productID, 2, 50, 0, now) // net 50
newTestOrderForStats(t, username, "cancelled", productID, 9, 999, 0, now) // exclue
newTestOrderForStats(t, username, "pending", productID, 9, 999, 0, now) // exclue (pas encore approuvée)
total, err := testDB.TotalRevenue(time.Time{})
if err != nil {
t.Fatalf("TotalRevenue: %v", err)
}
if total != 120 {
t.Errorf("TotalRevenue: got=%.2f want=120 (70+50, parrainage déduit, annulée/pending exclues)", total)
}
}
// TotalOrders exclut uniquement les commandes annulées (contrairement à
// TotalRevenue, il compte aussi les commandes non encore approuvées).
func TestTotalOrders_ExcludesOnlyCancelled(t *testing.T) {
cleanupStockTestData(t)
username := newTestClient(t, "stats_total_orders")
productID := newTestProduct(t, "StatsTotalOrders", 100)
now := time.Now()
newTestOrderForStats(t, username, "pending", productID, 1, 10, 0, now)
newTestOrderForStats(t, username, "approved", productID, 1, 10, 0, now)
newTestOrderForStats(t, username, "cancelled", productID, 1, 10, 0, now)
total, err := testDB.TotalOrders(time.Time{})
if err != nil {
t.Fatalf("TotalOrders: %v", err)
}
if total != 2 {
t.Errorf("TotalOrders: got=%d want=2 (pending+approved, cancelled exclue)", total)
}
}
// Régression du bug corrigé : le revenu par produit/catégorie (TopProducts,
// QuantityBreakdown, DailyProductDetailForDate) doit rester cohérent avec
// TotalRevenue même quand une commande approuvée a utilisé du crédit de
// parrainage — avant le fix, ces trois vues sommaient ci.prix brut sans
// déduire referral_used, produisant un total supérieur au résumé global.
func TestProductBreakdowns_RevenueMatchesTotalRevenue_WithReferralUsed(t *testing.T) {
cleanupStockTestData(t)
username := newTestClient(t, "stats_breakdown_referral")
productA := newTestProduct(t, "StatsBreakdownA", 100)
productB := newTestProduct(t, "StatsBreakdownB", 100)
now := time.Now()
newTestOrderForStats(t, username, "approved", productA, 5, 100, 30, now) // net 70
newTestOrderForStats(t, username, "approved", productB, 2, 50, 0, now) // net 50
totalRevenue, err := testDB.TotalRevenue(time.Time{})
if err != nil {
t.Fatalf("TotalRevenue: %v", err)
}
if totalRevenue != 120 {
t.Fatalf("précondition TotalRevenue: got=%.2f want=120", totalRevenue)
}
var prodRows []models.ProductRow
if err := testDB.TopProducts(&prodRows, time.Time{}, 15); err != nil {
t.Fatalf("TopProducts: %v", err)
}
sumTop := 0.0
for _, r := range prodRows {
sumTop += r.Revenue
}
if sumTop != totalRevenue {
t.Errorf("somme TopProducts.revenue = %.2f, doit correspondre à TotalRevenue = %.2f", sumTop, totalRevenue)
}
var qtyRows []models.QuantityBreakdownRow
if err := testDB.QuantityBreakdown(&qtyRows, time.Time{}); err != nil {
t.Fatalf("QuantityBreakdown: %v", err)
}
sumQty := 0.0
for _, r := range qtyRows {
sumQty += r.Revenue
}
if sumQty != totalRevenue {
t.Errorf("somme QuantityBreakdown.revenue = %.2f, doit correspondre à TotalRevenue = %.2f", sumQty, totalRevenue)
}
var dailyRows []models.DailyProductRow
if err := testDB.DailyProductDetailForDate(&dailyRows, now); err != nil {
t.Fatalf("DailyProductDetailForDate: %v", err)
}
sumDaily := 0.0
for _, r := range dailyRows {
sumDaily += r.Revenue
}
if sumDaily != totalRevenue {
t.Errorf("somme DailyProductDetailForDate.revenue = %.2f, doit correspondre à TotalRevenue = %.2f", sumDaily, totalRevenue)
}
}
// Cas limite : commande entièrement couverte par le crédit de parrainage
// (total_prix == referral_used) — la part de revenu attribuée à l'article
// doit être 0, sans division par zéro ni erreur SQL.
func TestProductBreakdowns_FullyCoveredByReferralYieldsZeroRevenue(t *testing.T) {
cleanupStockTestData(t)
username := newTestClient(t, "stats_breakdown_full_referral")
productID := newTestProduct(t, "StatsBreakdownFullReferral", 100)
now := time.Now()
newTestOrderForStats(t, username, "approved", productID, 4, 40, 40, now)
var prodRows []models.ProductRow
if err := testDB.TopProducts(&prodRows, time.Time{}, 15); err != nil {
t.Fatalf("TopProducts: %v", err)
}
if len(prodRows) != 1 {
t.Fatalf("attendu 1 produit, got=%d", len(prodRows))
}
if prodRows[0].Revenue != 0 {
t.Errorf("revenu attendu à 0 pour une commande entièrement couverte par le parrainage: got=%.2f", prodRows[0].Revenue)
}
}
// RevenueByDayLast30 doit rester cohérent avec TotalRevenue pour des
// commandes créées aujourd'hui (dans la fenêtre des 30 derniers jours).
func TestRevenueByDayLast30_MatchesTotalRevenue(t *testing.T) {
cleanupStockTestData(t)
username := newTestClient(t, "stats_revenue_by_day")
productID := newTestProduct(t, "StatsRevenueByDay", 100)
now := time.Now()
newTestOrderForStats(t, username, "approved", productID, 3, 90, 20, now) // net 70
totalRevenue, err := testDB.TotalRevenue(time.Time{})
if err != nil {
t.Fatalf("TotalRevenue: %v", err)
}
var dayRevRows []models.DayRevenueRow
if err := testDB.RevenueByDayLast30(&dayRevRows, time.Time{}); err != nil {
t.Fatalf("RevenueByDayLast30: %v", err)
}
sum := 0.0
for _, r := range dayRevRows {
sum += r.Revenue
}
if sum != totalRevenue {
t.Errorf("somme RevenueByDayLast30 = %.2f, doit correspondre à TotalRevenue = %.2f", sum, totalRevenue)
}
}
// DailyProductDetailForDate ne doit inclure que les commandes du jour demandé.
func TestDailyProductDetailForDate_OnlyIncludesGivenDate(t *testing.T) {
cleanupStockTestData(t)
username := newTestClient(t, "stats_daily_date_filter")
productID := newTestProduct(t, "StatsDailyDateFilter", 100)
today := time.Now()
yesterday := today.AddDate(0, 0, -1)
newTestOrderForStats(t, username, "approved", productID, 1, 10, 0, today)
newTestOrderForStats(t, username, "approved", productID, 1, 20, 0, yesterday)
var rows []models.DailyProductRow
if err := testDB.DailyProductDetailForDate(&rows, today); err != nil {
t.Fatalf("DailyProductDetailForDate: %v", err)
}
sum := 0.0
for _, r := range rows {
sum += r.Revenue
}
if sum != 10 {
t.Errorf("revenu du jour ne doit inclure que la commande d'aujourd'hui: got=%.2f want=10", sum)
}
}