package handlers import ( "errors" "net/http" "strconv" "veza-backend-api/internal/core/subscription" apperrors "veza-backend-api/internal/errors" "github.com/gin-gonic/gin" "github.com/google/uuid" "go.uber.org/zap" ) // SubscriptionHandler handles subscription-related HTTP endpoints type SubscriptionHandler struct { service *subscription.Service logger *zap.Logger } // NewSubscriptionHandler creates a new SubscriptionHandler func NewSubscriptionHandler(service *subscription.Service, logger *zap.Logger) *SubscriptionHandler { return &SubscriptionHandler{ service: service, logger: logger, } } // ListPlans returns all available subscription plans func (h *SubscriptionHandler) ListPlans(c *gin.Context) { plans, err := h.service.ListPlans(c.Request.Context()) if err != nil { RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to list plans", err)) return } RespondSuccess(c, http.StatusOK, gin.H{"plans": plans}) } // GetPlan returns a specific plan by ID func (h *SubscriptionHandler) GetPlan(c *gin.Context) { planID, err := uuid.Parse(c.Param("id")) if err != nil { RespondWithAppError(c, apperrors.NewValidationError("Invalid plan ID")) return } plan, err := h.service.GetPlan(c.Request.Context(), planID) if err != nil { if errors.Is(err, subscription.ErrPlanNotFound) { RespondWithAppError(c, apperrors.NewNotFoundError("Plan")) return } RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to get plan", err)) return } RespondSuccess(c, http.StatusOK, plan) } // GetMySubscription returns the authenticated user's current subscription func (h *SubscriptionHandler) GetMySubscription(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return } sub, err := h.service.GetUserSubscription(c.Request.Context(), userID) if err != nil { if errors.Is(err, subscription.ErrNoActiveSubscription) { RespondSuccess(c, http.StatusOK, gin.H{"subscription": nil, "plan": "free"}) return } RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to get subscription", err)) return } RespondSuccess(c, http.StatusOK, gin.H{"subscription": sub}) } // Subscribe creates a new subscription for the authenticated user func (h *SubscriptionHandler) Subscribe(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return } var req subscription.SubscribeRequest if err := c.ShouldBindJSON(&req); err != nil { RespondWithAppError(c, apperrors.NewValidationError("Invalid request: plan_id and billing_cycle required")) return } resp, err := h.service.Subscribe(c.Request.Context(), userID, req) if err != nil { switch { case errors.Is(err, subscription.ErrPlanNotFound): RespondWithAppError(c, apperrors.NewNotFoundError("Plan")) case errors.Is(err, subscription.ErrAlreadySubscribed): RespondWithAppError(c, apperrors.NewValidationError("Already subscribed to this plan")) case errors.Is(err, subscription.ErrInvalidBillingCycle): RespondWithAppError(c, apperrors.NewValidationError("Invalid billing cycle: must be 'monthly' or 'yearly'")) default: RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to subscribe", err)) } return } RespondSuccess(c, http.StatusCreated, resp) } // CancelSubscription cancels the user's subscription at end of period func (h *SubscriptionHandler) CancelSubscription(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return } sub, err := h.service.CancelSubscription(c.Request.Context(), userID) if err != nil { switch { case errors.Is(err, subscription.ErrNoActiveSubscription): RespondWithAppError(c, apperrors.NewNotFoundError("Active subscription")) case errors.Is(err, subscription.ErrFreePlanNoBilling): RespondWithAppError(c, apperrors.NewValidationError("Free plan cannot be canceled")) default: RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to cancel subscription", err)) } return } RespondSuccess(c, http.StatusOK, gin.H{ "subscription": sub, "message": "Subscription canceled. Access continues until " + sub.CurrentPeriodEnd.Format("2006-01-02"), }) } // ReactivateSubscription removes the cancellation of a subscription func (h *SubscriptionHandler) ReactivateSubscription(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return } sub, err := h.service.ReactivateSubscription(c.Request.Context(), userID) if err != nil { if errors.Is(err, subscription.ErrNoActiveSubscription) { RespondWithAppError(c, apperrors.NewNotFoundError("Active subscription")) return } RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to reactivate subscription", err)) return } RespondSuccess(c, http.StatusOK, gin.H{"subscription": sub}) } // ChangeBillingCycle switches between monthly and yearly billing func (h *SubscriptionHandler) ChangeBillingCycle(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return } var req struct { BillingCycle subscription.BillingCycle `json:"billing_cycle" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { RespondWithAppError(c, apperrors.NewValidationError("Invalid request: billing_cycle required")) return } sub, err := h.service.ChangeBillingCycle(c.Request.Context(), userID, req.BillingCycle) if err != nil { switch { case errors.Is(err, subscription.ErrNoActiveSubscription): RespondWithAppError(c, apperrors.NewNotFoundError("Active subscription")) case errors.Is(err, subscription.ErrInvalidBillingCycle): RespondWithAppError(c, apperrors.NewValidationError("Invalid billing cycle: must be 'monthly' or 'yearly'")) default: RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to change billing cycle", err)) } return } RespondSuccess(c, http.StatusOK, gin.H{"subscription": sub}) } // GetInvoices returns the user's subscription invoices func (h *SubscriptionHandler) GetInvoices(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return } limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20")) limit = clampLimit(limit) // SECURITY(MEDIUM-004) offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0")) invoices, err := h.service.GetUserInvoices(c.Request.Context(), userID, limit, offset) if err != nil { RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to get invoices", err)) return } RespondSuccess(c, http.StatusOK, gin.H{"invoices": invoices}) } // GetSubscriptionHistory returns the user's subscription history func (h *SubscriptionHandler) GetSubscriptionHistory(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return } limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20")) limit = clampLimit(limit) // SECURITY(MEDIUM-004) offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0")) subs, err := h.service.GetUserSubscriptionHistory(c.Request.Context(), userID, limit, offset) if err != nil { RespondWithAppError(c, apperrors.NewInternalErrorWrap("Failed to get subscription history", err)) return } RespondSuccess(c, http.StatusOK, gin.H{"subscriptions": subs}) }