package api import ( "context" "io" "github.com/gin-gonic/gin" "go.uber.org/zap" "veza-backend-api/internal/core/marketplace" "veza-backend-api/internal/handlers" "veza-backend-api/internal/response" "veza-backend-api/internal/services" "veza-backend-api/internal/services/hyperswitch" "veza-backend-api/internal/workers" ) // setupWebhookRoutes configure les routes pour les webhooks func (r *APIRouter) setupWebhookRoutes(router *gin.RouterGroup) { webhookService := services.NewWebhookService(r.db.GormDB, r.logger, r.config.JWTSecret) webhookWorker := workers.NewWebhookWorker( r.db.GormDB, webhookService, r.logger, 100, 5, 3, ) go webhookWorker.Start(context.Background()) webhookHandler := handlers.NewWebhookHandler(webhookService, webhookWorker, r.logger) webhooks := router.Group("/webhooks") { // Hyperswitch payment webhook - PUBLIC (no auth), called by Hyperswitch webhooks.POST("/hyperswitch", r.hyperswitchWebhookHandler()) if r.config.AuthMiddleware != nil { protected := webhooks.Group("") protected.Use(r.config.AuthMiddleware.RequireAuth()) r.applyCSRFProtection(protected) protected.POST("", webhookHandler.RegisterWebhook()) protected.GET("", webhookHandler.ListWebhooks()) protected.DELETE("/:id", webhookHandler.DeleteWebhook()) protected.GET("/stats", webhookHandler.GetWebhookStats()) protected.POST("/:id/test", webhookHandler.TestWebhook()) protected.POST("/:id/regenerate-key", webhookHandler.RegenerateAPIKey()) } } } // hyperswitchWebhookHandler handles POST /webhooks/hyperswitch from Hyperswitch. func (r *APIRouter) hyperswitchWebhookHandler() gin.HandlerFunc { marketService := r.getMarketplaceService() webhookSecret := r.config.HyperswitchWebhookSecret return func(c *gin.Context) { body, err := io.ReadAll(c.Request.Body) if err != nil { r.logger.Error("Hyperswitch webhook: failed to read body", zap.Error(err)) response.InternalServerError(c, "Failed to read webhook body") return } if webhookSecret == "" { r.logger.Error("Hyperswitch webhook: HYPERSWITCH_WEBHOOK_SECRET not configured, rejecting webhook") response.InternalServerError(c, "Webhook secret not configured") return } sig := c.GetHeader("x-webhook-signature-512") if err := hyperswitch.VerifyWebhookSignature(body, sig, webhookSecret); err != nil { r.logger.Warn("Hyperswitch webhook: signature verification failed", zap.Error(err)) response.Unauthorized(c, "Invalid webhook signature") return } if err := marketService.ProcessPaymentWebhook(c.Request.Context(), body); err != nil { r.logger.Error("Hyperswitch webhook: processing failed", zap.Error(err)) response.InternalServerError(c, "Webhook processing failed") return } response.Success(c, gin.H{"received": true}) } } // getMarketplaceService returns the marketplace service with Hyperswitch wiring. // Used by webhook handler; mirrors setupMarketplaceRoutes service creation. func (r *APIRouter) getMarketplaceService() marketplace.MarketplaceService { if r.config.MarketplaceServiceOverride != nil { return r.config.MarketplaceServiceOverride.(marketplace.MarketplaceService) } uploadDir := r.config.UploadDir if uploadDir == "" { uploadDir = "uploads/tracks" } storageService := services.NewTrackStorageService(uploadDir, false, r.logger) opts := []marketplace.ServiceOption{} if r.config.HyperswitchEnabled && r.config.HyperswitchAPIKey != "" && r.config.HyperswitchURL != "" { 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)) } return marketplace.NewService(r.db.GormDB, r.logger, storageService, opts...) }