//go:build integration // +build integration package integration import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" "gorm.io/driver/sqlite" "gorm.io/gorm" "veza-backend-api/internal/core/marketplace" "veza-backend-api/internal/models" "veza-backend-api/internal/services" ) // TestPayoutFlow_E2E_SaleToPayoutRequest verifies the full payout flow: // sell product -> payment completed -> seller balance credited -> manual payout request // v0.13.5 TASK-MKT-003: Validation E2E flux de payout créateur func TestPayoutFlow_E2E_SaleToPayoutRequest(t *testing.T) { db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) require.NoError(t, db.AutoMigrate( &models.User{}, &models.Track{}, &marketplace.Product{}, &marketplace.Order{}, &marketplace.OrderItem{}, &marketplace.License{}, &marketplace.SellerTransfer{}, &marketplace.SellerBalance{}, &marketplace.SellerPayout{}, )) // CartItem uses gen_random_uuid() (PostgreSQL) — create manually for SQLite require.NoError(t, db.Exec(` CREATE TABLE IF NOT EXISTS cart_items ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, product_id TEXT NOT NULL, quantity INTEGER DEFAULT 1, created_at DATETIME, updated_at DATETIME ) `).Error) db.Callback().Create().Before("gorm:create").Register("generate_uuid_cart_payout", func(d *gorm.DB) { if item, ok := d.Statement.Dest.(*marketplace.CartItem); ok && item.ID == uuid.Nil { item.ID = uuid.New() } }) buyerID := uuid.New() sellerID := uuid.New() trackID := uuid.New() require.NoError(t, db.Create(&models.User{ID: buyerID}).Error) require.NoError(t, db.Create(&models.User{ID: sellerID}).Error) require.NoError(t, db.Create(&models.Track{ID: trackID, UserID: sellerID, FilePath: "/test.mp3"}).Error) product := &marketplace.Product{ ID: uuid.New(), SellerID: sellerID, Title: "Premium Beat", Price: 199.99, ProductType: "track", TrackID: &trackID, Status: marketplace.ProductStatusActive, } require.NoError(t, db.Create(product).Error) mockPay := &mockPaymentProvider{paymentID: "pay_payout_test", clientSecret: "pi_secret_payout"} mockTransfer := &mockTransferService{} storageService := services.NewTrackStorageService("uploads/test", false, zap.NewNop()) marketService := marketplace.NewService(db, zap.NewNop(), storageService, marketplace.WithPaymentProvider(mockPay), marketplace.WithHyperswitchConfig(true, "/purchases"), marketplace.WithTransferService(mockTransfer, 0.10), ) router := setupPaymentFlowRouter(t, db, marketService) // Step 1: Add to cart addCartBody, _ := json.Marshal(map[string]interface{}{ "product_id": product.ID.String(), "quantity": 1, }) req := httptest.NewRequest(http.MethodPost, "/api/v1/commerce/cart/items", bytes.NewReader(addCartBody)) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-User-ID", buyerID.String()) w := httptest.NewRecorder() router.ServeHTTP(w, req) require.Equal(t, http.StatusCreated, w.Code, "add to cart: %s", w.Body.String()) // Step 2: Checkout req = httptest.NewRequest(http.MethodPost, "/api/v1/commerce/cart/checkout", bytes.NewReader([]byte("{}"))) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-User-ID", buyerID.String()) w = httptest.NewRecorder() router.ServeHTTP(w, req) require.Equal(t, http.StatusCreated, w.Code, "checkout: %s", w.Body.String()) // Step 3: Simulate webhook payment_succeeded payload, _ := json.Marshal(map[string]string{ "payment_id": "pay_payout_test", "status": "succeeded", }) signature := computeWebhookSignature(payload, "test-secret-at-least-32-chars-long") req = httptest.NewRequest(http.MethodPost, "/api/v1/webhooks/hyperswitch", bytes.NewReader(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("x-webhook-signature-512", signature) w = httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code, "webhook: %s", w.Body.String()) // Step 4: Verify seller balance was credited req = httptest.NewRequest(http.MethodGet, "/api/v1/sell/marketplace-balance", nil) req.Header.Set("X-User-ID", sellerID.String()) w = httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code, "get balance: %s", w.Body.String()) var balResp map[string]interface{} require.NoError(t, json.Unmarshal(w.Body.Bytes(), &balResp)) data, ok := balResp["data"].(map[string]interface{}) require.True(t, ok, "balance response should have data: %v", balResp) // Balance should be > 0 after sale (product price minus commission) availableCents := data["available_cents"].(float64) assert.Greater(t, availableCents, float64(0), "seller balance should be positive after sale") // Step 5: Request manual payout req = httptest.NewRequest(http.MethodPost, "/api/v1/sell/payouts/request", bytes.NewReader([]byte("{}"))) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-User-ID", sellerID.String()) w = httptest.NewRecorder() router.ServeHTTP(w, req) // May succeed or fail depending on minimum payout threshold — both are valid assert.Contains(t, []int{http.StatusCreated, http.StatusOK, http.StatusBadRequest, http.StatusUnprocessableEntity}, w.Code, "payout request should respond: %s", w.Body.String()) // Step 6: Verify payout history req = httptest.NewRequest(http.MethodGet, "/api/v1/sell/payouts", nil) req.Header.Set("X-User-ID", sellerID.String()) w = httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code, "get payouts: %s", w.Body.String()) }