Add Stripe billing, free trials, and cross-platform subscription guards
- Stripe integration: add StripeService with checkout sessions, customer portal, and webhook handling for subscription lifecycle events. - Free trials: auto-start configurable trial on first subscription check, with admin-controllable duration and enable/disable toggle. - Cross-platform guard: prevent duplicate subscriptions across iOS, Android, and Stripe by checking existing platform before allowing purchase. - Subscription model: add Stripe fields (customer_id, subscription_id, price_id), trial fields (trial_start, trial_end, trial_used), and SubscriptionSource/IsTrialActive helpers. - API: add trial and source fields to status response, update OpenAPI spec. - Clean up stale migration and audit docs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,11 +13,15 @@ import (
|
||||
// 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) *SubscriptionHandler {
|
||||
return &SubscriptionHandler{subscriptionService: subscriptionService}
|
||||
func NewSubscriptionHandler(subscriptionService *services.SubscriptionService, stripeService *services.StripeService) *SubscriptionHandler {
|
||||
return &SubscriptionHandler{
|
||||
subscriptionService: subscriptionService,
|
||||
stripeService: stripeService,
|
||||
}
|
||||
}
|
||||
|
||||
// GetSubscription handles GET /api/subscription/
|
||||
@@ -194,3 +198,82 @@ func (h *SubscriptionHandler) RestoreSubscription(c echo.Context) error {
|
||||
"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,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user