- Add MarketplaceServiceOverride and AuthMiddlewareOverride to config for tests - Wire overrides in routes_webhooks and routes_marketplace (authForMarketplaceInterface) - payment_flow_test: cart -> checkout -> webhook -> order completed, license, transfer - webhook_idempotency_test: 3 identical webhooks -> 1 order, 1 license - webhook_security_test: empty secret 500, invalid sig 401, valid sig 200 - refund_flow_test: completed order -> refund -> order refunded, license revoked - Shared computeWebhookSignature helper in webhook_test_helpers.go - SetMaxOpenConns(1) for sqlite :memory: in idempotency test to avoid flakiness Ref: docs/ROADMAP_V09XX_TO_V1.md v0.912 Cashflow
154 lines
6.6 KiB
Go
154 lines
6.6 KiB
Go
package api
|
|
|
|
import (
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
"go.uber.org/zap"
|
|
|
|
"veza-backend-api/internal/config"
|
|
"veza-backend-api/internal/core/marketplace"
|
|
"veza-backend-api/internal/handlers"
|
|
"veza-backend-api/internal/middleware"
|
|
"veza-backend-api/internal/services"
|
|
"veza-backend-api/internal/services/hyperswitch"
|
|
)
|
|
|
|
// authForMarketplaceInterface has the auth methods needed by marketplace routes (allows test overrides)
|
|
type authForMarketplaceInterface interface {
|
|
RequireAuth() gin.HandlerFunc
|
|
RequireContentCreatorRole() gin.HandlerFunc
|
|
RequireOwnershipOrAdmin(string, middleware.ResourceOwnerResolver) gin.HandlerFunc
|
|
}
|
|
|
|
func (r *APIRouter) authForMarketplace() authForMarketplaceInterface {
|
|
if r.config.AuthMiddlewareOverride != nil {
|
|
return r.config.AuthMiddlewareOverride.(authForMarketplaceInterface)
|
|
}
|
|
return r.config.AuthMiddleware
|
|
}
|
|
|
|
// setupMarketplaceRoutes configure les routes de la marketplace
|
|
func (r *APIRouter) setupMarketplaceRoutes(router *gin.RouterGroup) {
|
|
uploadDir := r.config.UploadDir
|
|
if uploadDir == "" {
|
|
uploadDir = "uploads/tracks"
|
|
}
|
|
|
|
var marketService marketplace.MarketplaceService
|
|
var marketServicePtr *marketplace.Service
|
|
if r.config.MarketplaceServiceOverride != nil {
|
|
marketService = r.config.MarketplaceServiceOverride.(marketplace.MarketplaceService)
|
|
marketServicePtr = r.config.MarketplaceServiceOverride.(*marketplace.Service)
|
|
} else {
|
|
storageService := services.NewTrackStorageService(uploadDir, false, r.logger)
|
|
opts := []marketplace.ServiceOption{}
|
|
if r.config.HyperswitchEnabled && r.config.HyperswitchAPIKey != "" && r.config.HyperswitchURL != "" {
|
|
if r.config.SentryEnvironment == config.EnvProduction && !r.config.HyperswitchLiveMode {
|
|
r.logger.Warn("Hyperswitch is enabled in production but HYPERSWITCH_LIVE_MODE=false; using test keys",
|
|
zap.String("hint", "Set HYPERSWITCH_LIVE_MODE=true and use live API keys for production payments"))
|
|
}
|
|
hsClient := hyperswitch.NewClient(r.config.HyperswitchURL, r.config.HyperswitchAPIKey)
|
|
hsProvider := hyperswitch.NewProvider(hsClient)
|
|
opts = append(opts,
|
|
marketplace.WithPaymentProvider(hsProvider),
|
|
marketplace.WithHyperswitchConfig(true, r.config.CheckoutSuccessURL),
|
|
)
|
|
}
|
|
if r.config.StripeConnectEnabled && r.config.StripeConnectSecretKey != "" {
|
|
scs := services.NewStripeConnectService(r.db.GormDB, r.config.StripeConnectSecretKey, r.logger)
|
|
opts = append(opts, marketplace.WithTransferService(scs, r.config.PlatformFeeRate))
|
|
}
|
|
svc := marketplace.NewService(r.db.GormDB, r.logger, storageService, opts...)
|
|
marketService = svc
|
|
marketServicePtr = svc
|
|
}
|
|
|
|
var stripeConnectSvc *services.StripeConnectService
|
|
if r.config.StripeConnectEnabled && r.config.StripeConnectSecretKey != "" {
|
|
stripeConnectSvc = services.NewStripeConnectService(r.db.GormDB, r.config.StripeConnectSecretKey, r.logger)
|
|
}
|
|
|
|
productPreviewDir := uploadDir
|
|
if productPreviewDir == "" {
|
|
productPreviewDir = "uploads"
|
|
}
|
|
marketHandler := handlers.NewMarketplaceHandler(marketService, r.logger, productPreviewDir)
|
|
|
|
group := router.Group("/marketplace")
|
|
group.GET("/products", marketHandler.ListProducts)
|
|
group.GET("/products/:id", marketHandler.GetProduct)
|
|
group.GET("/products/:id/preview", marketHandler.StreamProductPreview)
|
|
group.GET("/products/:id/reviews", marketHandler.ListReviews)
|
|
|
|
if auth := r.authForMarketplace(); auth != nil {
|
|
protected := group.Group("")
|
|
protected.Use(auth.RequireAuth())
|
|
r.applyCSRFProtection(protected)
|
|
|
|
createGroup := protected.Group("")
|
|
createGroup.Use(auth.RequireContentCreatorRole())
|
|
createGroup.POST("/products", marketHandler.CreateProduct)
|
|
createGroup.POST("/products/:id/preview", marketHandler.UploadProductPreview)
|
|
|
|
productOwnerResolver := func(c *gin.Context) (uuid.UUID, error) {
|
|
productIDStr := c.Param("id")
|
|
productID, err := uuid.Parse(productIDStr)
|
|
if err != nil {
|
|
return uuid.Nil, err
|
|
}
|
|
product, err := marketService.GetProduct(c.Request.Context(), productID)
|
|
if err != nil {
|
|
return uuid.Nil, err
|
|
}
|
|
return product.SellerID, nil
|
|
}
|
|
protected.PUT("/products/:id", auth.RequireOwnershipOrAdmin("product", productOwnerResolver), marketHandler.UpdateProduct)
|
|
protected.PUT("/products/:id/images", auth.RequireOwnershipOrAdmin("product", productOwnerResolver), marketHandler.UpdateProductImages)
|
|
|
|
protected.GET("/orders", marketHandler.ListOrders)
|
|
protected.GET("/orders/:id", marketHandler.GetOrder)
|
|
protected.GET("/orders/:id/invoice", marketHandler.GetOrderInvoice)
|
|
protected.POST("/orders/:id/refund", marketHandler.RefundOrder)
|
|
protected.POST("/orders", marketHandler.CreateOrder)
|
|
protected.GET("/download/:product_id", marketHandler.GetDownloadURL)
|
|
protected.GET("/licenses/mine", marketHandler.GetMyLicenses)
|
|
protected.POST("/products/:id/reviews", marketHandler.CreateReview)
|
|
|
|
marketplaceExtHandler := handlers.NewMarketplaceExtHandler(marketServicePtr, r.logger)
|
|
protected.GET("/wishlist", marketplaceExtHandler.GetWishlist)
|
|
protected.POST("/wishlist", marketplaceExtHandler.AddToWishlist)
|
|
protected.DELETE("/wishlist/:productId", marketplaceExtHandler.RemoveFromWishlist)
|
|
}
|
|
|
|
sell := router.Group("/sell")
|
|
if auth := r.authForMarketplace(); auth != nil {
|
|
sellProtected := sell.Group("")
|
|
sellProtected.Use(auth.RequireAuth())
|
|
sellProtected.Use(auth.RequireContentCreatorRole())
|
|
r.applyCSRFProtection(sellProtected)
|
|
sellProtected.GET("/stats", marketHandler.GetSellStats)
|
|
sellProtected.GET("/stats/evolution", marketHandler.GetSellStatsEvolution)
|
|
sellProtected.GET("/stats/top-products", marketHandler.GetSellTopProducts)
|
|
sellProtected.GET("/sales", marketHandler.GetSellSales)
|
|
|
|
sellHandler := handlers.NewSellHandler(r.db.GormDB, stripeConnectSvc, r.logger)
|
|
sellProtected.POST("/connect/onboard", sellHandler.ConnectOnboard)
|
|
sellProtected.GET("/connect/callback", sellHandler.ConnectCallback)
|
|
sellProtected.GET("/balance", sellHandler.GetBalance)
|
|
sellProtected.GET("/transfers", sellHandler.GetSellerTransfers)
|
|
}
|
|
|
|
commerce := router.Group("/commerce")
|
|
if auth := r.authForMarketplace(); auth != nil {
|
|
cartProtected := commerce.Group("")
|
|
cartProtected.Use(auth.RequireAuth())
|
|
r.applyCSRFProtection(cartProtected)
|
|
|
|
marketplaceExtHandler := handlers.NewMarketplaceExtHandler(marketServicePtr, r.logger)
|
|
cartProtected.GET("/cart", marketplaceExtHandler.GetCart)
|
|
cartProtected.GET("/promo/:code", marketplaceExtHandler.ValidatePromo)
|
|
cartProtected.POST("/cart/items", marketplaceExtHandler.AddToCart)
|
|
cartProtected.DELETE("/cart/items/:id", marketplaceExtHandler.RemoveFromCart)
|
|
cartProtected.POST("/cart/checkout", marketplaceExtHandler.Checkout)
|
|
}
|
|
}
|