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 }