Docker Compose with 24+ services: - PostgreSQL (PostGIS), Valkey, MinIO, Qdrant - Vault (PKI/TLS), Nginx (Reverse Proxy) - Backend Core API, Consent Service, Billing Service - RAG Service, Embedding Service - Gitea, Woodpecker CI/CD - Night Scheduler, Health Aggregator - Jitsi (Web/XMPP/JVB/Jicofo), Mailpit Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
206 lines
6.1 KiB
Go
206 lines
6.1 KiB
Go
package handlers
|
|
|
|
import (
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
|
|
"github.com/breakpilot/billing-service/internal/database"
|
|
"github.com/breakpilot/billing-service/internal/services"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stripe/stripe-go/v76/webhook"
|
|
)
|
|
|
|
// WebhookHandler handles Stripe webhook events
|
|
type WebhookHandler struct {
|
|
db *database.DB
|
|
webhookSecret string
|
|
subscriptionService *services.SubscriptionService
|
|
entitlementService *services.EntitlementService
|
|
}
|
|
|
|
// NewWebhookHandler creates a new WebhookHandler
|
|
func NewWebhookHandler(
|
|
db *database.DB,
|
|
webhookSecret string,
|
|
subscriptionService *services.SubscriptionService,
|
|
entitlementService *services.EntitlementService,
|
|
) *WebhookHandler {
|
|
return &WebhookHandler{
|
|
db: db,
|
|
webhookSecret: webhookSecret,
|
|
subscriptionService: subscriptionService,
|
|
entitlementService: entitlementService,
|
|
}
|
|
}
|
|
|
|
// HandleStripeWebhook handles incoming Stripe webhook events
|
|
// POST /api/v1/billing/webhook
|
|
func (h *WebhookHandler) HandleStripeWebhook(c *gin.Context) {
|
|
// Read the request body
|
|
body, err := io.ReadAll(c.Request.Body)
|
|
if err != nil {
|
|
log.Printf("Webhook: Error reading body: %v", err)
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "cannot read body"})
|
|
return
|
|
}
|
|
|
|
// Get the Stripe signature header
|
|
sigHeader := c.GetHeader("Stripe-Signature")
|
|
if sigHeader == "" {
|
|
log.Printf("Webhook: Missing Stripe-Signature header")
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "missing signature"})
|
|
return
|
|
}
|
|
|
|
// Verify the webhook signature
|
|
event, err := webhook.ConstructEvent(body, sigHeader, h.webhookSecret)
|
|
if err != nil {
|
|
log.Printf("Webhook: Signature verification failed: %v", err)
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid signature"})
|
|
return
|
|
}
|
|
|
|
ctx := c.Request.Context()
|
|
|
|
// Check if we've already processed this event (idempotency)
|
|
processed, err := h.subscriptionService.IsEventProcessed(ctx, event.ID)
|
|
if err != nil {
|
|
log.Printf("Webhook: Error checking event: %v", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
|
return
|
|
}
|
|
if processed {
|
|
log.Printf("Webhook: Event %s already processed", event.ID)
|
|
c.JSON(http.StatusOK, gin.H{"status": "already_processed"})
|
|
return
|
|
}
|
|
|
|
// Mark event as being processed
|
|
if err := h.subscriptionService.MarkEventProcessing(ctx, event.ID, string(event.Type)); err != nil {
|
|
log.Printf("Webhook: Error marking event: %v", err)
|
|
}
|
|
|
|
// Handle the event based on type
|
|
var handleErr error
|
|
switch event.Type {
|
|
case "checkout.session.completed":
|
|
handleErr = h.handleCheckoutSessionCompleted(ctx, event.Data.Raw)
|
|
|
|
case "customer.subscription.created":
|
|
handleErr = h.handleSubscriptionCreated(ctx, event.Data.Raw)
|
|
|
|
case "customer.subscription.updated":
|
|
handleErr = h.handleSubscriptionUpdated(ctx, event.Data.Raw)
|
|
|
|
case "customer.subscription.deleted":
|
|
handleErr = h.handleSubscriptionDeleted(ctx, event.Data.Raw)
|
|
|
|
case "invoice.paid":
|
|
handleErr = h.handleInvoicePaid(ctx, event.Data.Raw)
|
|
|
|
case "invoice.payment_failed":
|
|
handleErr = h.handleInvoicePaymentFailed(ctx, event.Data.Raw)
|
|
|
|
case "customer.created":
|
|
log.Printf("Webhook: Customer created - %s", event.ID)
|
|
|
|
default:
|
|
log.Printf("Webhook: Unhandled event type: %s", event.Type)
|
|
}
|
|
|
|
if handleErr != nil {
|
|
log.Printf("Webhook: Error handling %s: %v", event.Type, handleErr)
|
|
// Mark event as failed
|
|
h.subscriptionService.MarkEventFailed(ctx, event.ID, handleErr.Error())
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "handler error"})
|
|
return
|
|
}
|
|
|
|
// Mark event as processed
|
|
if err := h.subscriptionService.MarkEventProcessed(ctx, event.ID); err != nil {
|
|
log.Printf("Webhook: Error marking event processed: %v", err)
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"status": "processed"})
|
|
}
|
|
|
|
// handleCheckoutSessionCompleted handles successful checkout
|
|
func (h *WebhookHandler) handleCheckoutSessionCompleted(ctx interface{}, data []byte) error {
|
|
log.Printf("Webhook: Processing checkout.session.completed")
|
|
|
|
// Parse checkout session from data
|
|
// The actual implementation will parse the JSON and create/update subscription
|
|
|
|
// TODO: Implementation
|
|
// 1. Parse checkout session data
|
|
// 2. Extract customer_id, subscription_id, user_id (from metadata)
|
|
// 3. Create or update subscription record
|
|
// 4. Update entitlements
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleSubscriptionCreated handles new subscription creation
|
|
func (h *WebhookHandler) handleSubscriptionCreated(ctx interface{}, data []byte) error {
|
|
log.Printf("Webhook: Processing customer.subscription.created")
|
|
|
|
// TODO: Implementation
|
|
// 1. Parse subscription data
|
|
// 2. Extract status, plan, trial_end, etc.
|
|
// 3. Create subscription record
|
|
// 4. Set up initial entitlements
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleSubscriptionUpdated handles subscription updates
|
|
func (h *WebhookHandler) handleSubscriptionUpdated(ctx interface{}, data []byte) error {
|
|
log.Printf("Webhook: Processing customer.subscription.updated")
|
|
|
|
// TODO: Implementation
|
|
// 1. Parse subscription data
|
|
// 2. Update subscription record (status, plan, cancel_at_period_end, etc.)
|
|
// 3. Update entitlements if plan changed
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleSubscriptionDeleted handles subscription cancellation
|
|
func (h *WebhookHandler) handleSubscriptionDeleted(ctx interface{}, data []byte) error {
|
|
log.Printf("Webhook: Processing customer.subscription.deleted")
|
|
|
|
// TODO: Implementation
|
|
// 1. Parse subscription data
|
|
// 2. Update subscription status to canceled/expired
|
|
// 3. Remove or downgrade entitlements
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleInvoicePaid handles successful invoice payment
|
|
func (h *WebhookHandler) handleInvoicePaid(ctx interface{}, data []byte) error {
|
|
log.Printf("Webhook: Processing invoice.paid")
|
|
|
|
// TODO: Implementation
|
|
// 1. Parse invoice data
|
|
// 2. Update subscription period
|
|
// 3. Reset usage counters for new period
|
|
// 4. Store invoice record
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleInvoicePaymentFailed handles failed invoice payment
|
|
func (h *WebhookHandler) handleInvoicePaymentFailed(ctx interface{}, data []byte) error {
|
|
log.Printf("Webhook: Processing invoice.payment_failed")
|
|
|
|
// TODO: Implementation
|
|
// 1. Parse invoice data
|
|
// 2. Update subscription status to past_due
|
|
// 3. Send notification to user
|
|
// 4. Possibly restrict access
|
|
|
|
return nil
|
|
}
|