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:
Trey t
2026-03-05 11:36:14 -06:00
parent d5bb123cd0
commit 72db9050f8
35 changed files with 1555 additions and 1120 deletions

View File

@@ -58,7 +58,13 @@ func SetupRouter(deps *Dependencies) *echo.Echo {
e.Use(custommiddleware.RequestIDMiddleware())
e.Use(utils.EchoRecovery())
e.Use(custommiddleware.StructuredLogger())
e.Use(middleware.BodyLimit("1M")) // 1MB default for JSON payloads
e.Use(middleware.BodyLimitWithConfig(middleware.BodyLimitConfig{
Limit: "1M", // 1MB default for JSON payloads
Skipper: func(c echo.Context) bool {
// Allow larger payloads for webhook endpoints (Apple/Google/Stripe notifications)
return strings.HasPrefix(c.Request().URL.Path, "/api/subscription/webhook")
},
}))
e.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{
Timeout: 30 * time.Second,
Skipper: func(c echo.Context) bool {
@@ -143,11 +149,15 @@ func SetupRouter(deps *Dependencies) *echo.Echo {
residenceService.SetSubscriptionService(subscriptionService) // Wire up subscription service for tier limit enforcement
taskTemplateService := services.NewTaskTemplateService(taskTemplateRepo)
// Initialize Stripe service
stripeService := services.NewStripeService(subscriptionRepo, userRepo)
// Initialize webhook event repo for deduplication
webhookEventRepo := repositories.NewWebhookEventRepository(deps.DB)
// Initialize webhook handler for Apple/Google subscription notifications
// Initialize webhook handler for Apple/Google/Stripe subscription notifications
subscriptionWebhookHandler := handlers.NewSubscriptionWebhookHandler(subscriptionRepo, userRepo, webhookEventRepo, cfg.Features.WebhooksEnabled)
subscriptionWebhookHandler.SetStripeService(stripeService)
// Initialize middleware
authMiddleware := custommiddleware.NewAuthMiddleware(deps.DB, deps.Cache)
@@ -166,7 +176,7 @@ func SetupRouter(deps *Dependencies) *echo.Echo {
contractorHandler := handlers.NewContractorHandler(contractorService)
documentHandler := handlers.NewDocumentHandler(documentService, deps.StorageService)
notificationHandler := handlers.NewNotificationHandler(notificationService)
subscriptionHandler := handlers.NewSubscriptionHandler(subscriptionService)
subscriptionHandler := handlers.NewSubscriptionHandler(subscriptionService, stripeService)
staticDataHandler := handlers.NewStaticDataHandler(residenceService, taskService, contractorService, taskTemplateService, deps.Cache)
taskTemplateHandler := handlers.NewTaskTemplateHandler(taskTemplateService)
@@ -458,6 +468,8 @@ func setupSubscriptionRoutes(api *echo.Group, subscriptionHandler *handlers.Subs
subscription.POST("/purchase/", subscriptionHandler.ProcessPurchase)
subscription.POST("/cancel/", subscriptionHandler.CancelSubscription)
subscription.POST("/restore/", subscriptionHandler.RestoreSubscription)
subscription.POST("/checkout/", subscriptionHandler.CreateCheckoutSession)
subscription.POST("/portal/", subscriptionHandler.CreatePortalSession)
}
}
@@ -499,6 +511,7 @@ func setupWebhookRoutes(api *echo.Group, webhookHandler *handlers.SubscriptionWe
{
webhooks.POST("/apple/", webhookHandler.HandleAppleWebhook)
webhooks.POST("/google/", webhookHandler.HandleGoogleWebhook)
webhooks.POST("/stripe/", webhookHandler.HandleStripeWebhook)
}
}