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) } }