Total rebrand across all Go API source files: - Go module path: casera-api -> honeydue-api - All imports updated (130+ files) - Docker: containers, images, networks renamed - Email templates: support email, noreply, icon URL - Domains: casera.app/mycrib.treytartt.com -> honeyDue.treytartt.com - Bundle IDs: com.tt.casera -> com.tt.honeyDue - IAP product IDs updated - Landing page, admin panel, config defaults - Seeds, CI workflows, Makefile, docs - Database table names preserved (no migration needed) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
280 lines
7.6 KiB
Go
280 lines
7.6 KiB
Go
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,
|
|
})
|
|
}
|