package handlers import ( "net/http" "github.com/labstack/echo/v4" "github.com/treytartt/honeydue-api/internal/apperrors" "github.com/treytartt/honeydue-api/internal/middleware" "github.com/treytartt/honeydue-api/internal/services" ) // SubscriptionHandler handles subscription-related HTTP requests type SubscriptionHandler struct { subscriptionService *services.SubscriptionService stripeService *services.StripeService } // NewSubscriptionHandler creates a new subscription handler func NewSubscriptionHandler(subscriptionService *services.SubscriptionService, stripeService *services.StripeService) *SubscriptionHandler { return &SubscriptionHandler{ subscriptionService: subscriptionService, stripeService: stripeService, } } // GetSubscription handles GET /api/subscription/ func (h *SubscriptionHandler) GetSubscription(c echo.Context) error { user, err := middleware.MustGetAuthUser(c) if err != nil { return err } subscription, err := h.subscriptionService.GetSubscription(user.ID) if err != nil { return err } return c.JSON(http.StatusOK, subscription) } // GetSubscriptionStatus handles GET /api/subscription/status/ func (h *SubscriptionHandler) GetSubscriptionStatus(c echo.Context) error { user, err := middleware.MustGetAuthUser(c) if err != nil { return err } status, err := h.subscriptionService.GetSubscriptionStatus(user.ID) if err != nil { return err } return c.JSON(http.StatusOK, status) } // GetUpgradeTrigger handles GET /api/subscription/upgrade-trigger/:key/ func (h *SubscriptionHandler) GetUpgradeTrigger(c echo.Context) error { key := c.Param("key") trigger, err := h.subscriptionService.GetUpgradeTrigger(key) if err != nil { return err } return c.JSON(http.StatusOK, trigger) } // GetAllUpgradeTriggers handles GET /api/subscription/upgrade-triggers/ func (h *SubscriptionHandler) GetAllUpgradeTriggers(c echo.Context) error { triggers, err := h.subscriptionService.GetAllUpgradeTriggers() if err != nil { return err } return c.JSON(http.StatusOK, triggers) } // GetFeatureBenefits handles GET /api/subscription/features/ func (h *SubscriptionHandler) GetFeatureBenefits(c echo.Context) error { benefits, err := h.subscriptionService.GetFeatureBenefits() if err != nil { return err } return c.JSON(http.StatusOK, benefits) } // GetPromotions handles GET /api/subscription/promotions/ func (h *SubscriptionHandler) GetPromotions(c echo.Context) error { user, err := middleware.MustGetAuthUser(c) if err != nil { return err } promotions, err := h.subscriptionService.GetActivePromotions(user.ID) if err != nil { return err } return c.JSON(http.StatusOK, promotions) } // ProcessPurchase handles POST /api/subscription/purchase/ func (h *SubscriptionHandler) ProcessPurchase(c echo.Context) error { user, err := middleware.MustGetAuthUser(c) if err != nil { return err } var req services.ProcessPurchaseRequest if err := c.Bind(&req); err != nil { return apperrors.BadRequest("error.invalid_request") } if err := c.Validate(&req); err != nil { return err } var subscription *services.SubscriptionResponse switch req.Platform { case "ios": // StoreKit 2 uses transaction_id, StoreKit 1 uses receipt_data if req.TransactionID == "" && req.ReceiptData == "" { return apperrors.BadRequest("error.receipt_data_required") } subscription, err = h.subscriptionService.ProcessApplePurchase(user.ID, req.ReceiptData, req.TransactionID) case "android": if req.PurchaseToken == "" { return apperrors.BadRequest("error.purchase_token_required") } subscription, err = h.subscriptionService.ProcessGooglePurchase(user.ID, req.PurchaseToken, req.ProductID) default: return apperrors.BadRequest("error.invalid_platform") } if err != nil { return err } return c.JSON(http.StatusOK, map[string]interface{}{ "message": "message.subscription_upgraded", "subscription": subscription, }) } // CancelSubscription handles POST /api/subscription/cancel/ func (h *SubscriptionHandler) CancelSubscription(c echo.Context) error { user, err := middleware.MustGetAuthUser(c) if err != nil { return err } subscription, err := h.subscriptionService.CancelSubscription(user.ID) if err != nil { return err } return c.JSON(http.StatusOK, map[string]interface{}{ "message": "message.subscription_cancelled", "subscription": subscription, }) } // RestoreSubscription handles POST /api/subscription/restore/ func (h *SubscriptionHandler) RestoreSubscription(c echo.Context) error { user, err := middleware.MustGetAuthUser(c) if err != nil { return err } var req services.ProcessPurchaseRequest if err := c.Bind(&req); err != nil { return apperrors.BadRequest("error.invalid_request") } if err := c.Validate(&req); err != nil { return err } // Same logic as ProcessPurchase - validates receipt/token and restores var subscription *services.SubscriptionResponse switch req.Platform { case "ios": subscription, err = h.subscriptionService.ProcessApplePurchase(user.ID, req.ReceiptData, req.TransactionID) case "android": subscription, err = h.subscriptionService.ProcessGooglePurchase(user.ID, req.PurchaseToken, req.ProductID) default: return apperrors.BadRequest("error.invalid_platform") } if err != nil { return err } return c.JSON(http.StatusOK, map[string]interface{}{ "message": "message.subscription_restored", "subscription": subscription, }) } // CreateCheckoutSession handles POST /api/subscription/checkout/ // Creates a Stripe Checkout Session for web subscription purchases func (h *SubscriptionHandler) CreateCheckoutSession(c echo.Context) error { user, err := middleware.MustGetAuthUser(c) if err != nil { return err } if h.stripeService == nil { return apperrors.BadRequest("error.stripe_not_configured") } // Check if already Pro from another platform alreadyPro, existingPlatform, err := h.subscriptionService.IsAlreadyProFromOtherPlatform(user.ID, "stripe") if err != nil { return err } if alreadyPro { return c.JSON(http.StatusConflict, map[string]interface{}{ "error": "error.already_subscribed_other_platform", "existing_platform": existingPlatform, "message": "You already have an active Pro subscription via " + existingPlatform + ". Manage it there to avoid double billing.", }) } var req struct { PriceID string `json:"price_id" validate:"required"` SuccessURL string `json:"success_url" validate:"required,url"` CancelURL string `json:"cancel_url" validate:"required,url"` } if err := c.Bind(&req); err != nil { return apperrors.BadRequest("error.invalid_request") } if err := c.Validate(&req); err != nil { return err } sessionURL, err := h.stripeService.CreateCheckoutSession(user.ID, req.PriceID, req.SuccessURL, req.CancelURL) if err != nil { return err } return c.JSON(http.StatusOK, map[string]interface{}{ "checkout_url": sessionURL, }) } // CreatePortalSession handles POST /api/subscription/portal/ // Creates a Stripe Customer Portal session for managing web subscriptions func (h *SubscriptionHandler) CreatePortalSession(c echo.Context) error { user, err := middleware.MustGetAuthUser(c) if err != nil { return err } if h.stripeService == nil { return apperrors.BadRequest("error.stripe_not_configured") } var req struct { ReturnURL string `json:"return_url" validate:"required,url"` } if err := c.Bind(&req); err != nil { return apperrors.BadRequest("error.invalid_request") } if err := c.Validate(&req); err != nil { return err } portalURL, err := h.stripeService.CreatePortalSession(user.ID, req.ReturnURL) if err != nil { return err } return c.JSON(http.StatusOK, map[string]interface{}{ "portal_url": portalURL, }) }