veza/veza-backend-api/internal/api/routes_webhooks.go
senke 7cb4ef56e1
Some checks failed
Backend API CI / test-unit (push) Failing after 0s
Backend API CI / test-integration (push) Failing after 0s
feat(v0.912): Cashflow - payment E2E integration tests
- 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
2026-02-27 20:00:51 +01:00

110 lines
4 KiB
Go

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