- Add Apple App Store Server API integration for receipt/transaction validation - Add Google Play Developer API integration for purchase token validation - Add webhook endpoints for server-to-server subscription notifications - POST /api/subscription/webhook/apple/ (App Store Server Notifications v2) - POST /api/subscription/webhook/google/ (Real-time Developer Notifications) - Support both StoreKit 1 (receipt_data) and StoreKit 2 (transaction_id) - Add repository methods to find users by transaction ID or purchase token - Add configuration for IAP credentials (APPLE_IAP_*, GOOGLE_IAP_*) - Add setup documentation for configuring webhooks 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
190 lines
5.8 KiB
Go
190 lines
5.8 KiB
Go
package handlers
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/treytartt/casera-api/internal/i18n"
|
|
"github.com/treytartt/casera-api/internal/middleware"
|
|
"github.com/treytartt/casera-api/internal/models"
|
|
"github.com/treytartt/casera-api/internal/services"
|
|
)
|
|
|
|
// SubscriptionHandler handles subscription-related HTTP requests
|
|
type SubscriptionHandler struct {
|
|
subscriptionService *services.SubscriptionService
|
|
}
|
|
|
|
// NewSubscriptionHandler creates a new subscription handler
|
|
func NewSubscriptionHandler(subscriptionService *services.SubscriptionService) *SubscriptionHandler {
|
|
return &SubscriptionHandler{subscriptionService: subscriptionService}
|
|
}
|
|
|
|
// GetSubscription handles GET /api/subscription/
|
|
func (h *SubscriptionHandler) GetSubscription(c *gin.Context) {
|
|
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
|
|
|
subscription, err := h.subscriptionService.GetSubscription(user.ID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, subscription)
|
|
}
|
|
|
|
// GetSubscriptionStatus handles GET /api/subscription/status/
|
|
func (h *SubscriptionHandler) GetSubscriptionStatus(c *gin.Context) {
|
|
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
|
|
|
status, err := h.subscriptionService.GetSubscriptionStatus(user.ID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, status)
|
|
}
|
|
|
|
// GetUpgradeTrigger handles GET /api/subscription/upgrade-trigger/:key/
|
|
func (h *SubscriptionHandler) GetUpgradeTrigger(c *gin.Context) {
|
|
key := c.Param("key")
|
|
|
|
trigger, err := h.subscriptionService.GetUpgradeTrigger(key)
|
|
if err != nil {
|
|
if errors.Is(err, services.ErrUpgradeTriggerNotFound) {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.upgrade_trigger_not_found")})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, trigger)
|
|
}
|
|
|
|
// GetAllUpgradeTriggers handles GET /api/subscription/upgrade-triggers/
|
|
func (h *SubscriptionHandler) GetAllUpgradeTriggers(c *gin.Context) {
|
|
triggers, err := h.subscriptionService.GetAllUpgradeTriggers()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, triggers)
|
|
}
|
|
|
|
// GetFeatureBenefits handles GET /api/subscription/features/
|
|
func (h *SubscriptionHandler) GetFeatureBenefits(c *gin.Context) {
|
|
benefits, err := h.subscriptionService.GetFeatureBenefits()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, benefits)
|
|
}
|
|
|
|
// GetPromotions handles GET /api/subscription/promotions/
|
|
func (h *SubscriptionHandler) GetPromotions(c *gin.Context) {
|
|
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
|
|
|
promotions, err := h.subscriptionService.GetActivePromotions(user.ID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, promotions)
|
|
}
|
|
|
|
// ProcessPurchase handles POST /api/subscription/purchase/
|
|
func (h *SubscriptionHandler) ProcessPurchase(c *gin.Context) {
|
|
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
|
|
|
var req services.ProcessPurchaseRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
var subscription *services.SubscriptionResponse
|
|
var err error
|
|
|
|
switch req.Platform {
|
|
case "ios":
|
|
// StoreKit 2 uses transaction_id, StoreKit 1 uses receipt_data
|
|
if req.TransactionID == "" && req.ReceiptData == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.receipt_data_required")})
|
|
return
|
|
}
|
|
subscription, err = h.subscriptionService.ProcessApplePurchase(user.ID, req.ReceiptData, req.TransactionID)
|
|
case "android":
|
|
if req.PurchaseToken == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.purchase_token_required")})
|
|
return
|
|
}
|
|
subscription, err = h.subscriptionService.ProcessGooglePurchase(user.ID, req.PurchaseToken, req.ProductID)
|
|
}
|
|
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"message": i18n.LocalizedMessage(c, "message.subscription_upgraded"),
|
|
"subscription": subscription,
|
|
})
|
|
}
|
|
|
|
// CancelSubscription handles POST /api/subscription/cancel/
|
|
func (h *SubscriptionHandler) CancelSubscription(c *gin.Context) {
|
|
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
|
|
|
subscription, err := h.subscriptionService.CancelSubscription(user.ID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"message": i18n.LocalizedMessage(c, "message.subscription_cancelled"),
|
|
"subscription": subscription,
|
|
})
|
|
}
|
|
|
|
// RestoreSubscription handles POST /api/subscription/restore/
|
|
func (h *SubscriptionHandler) RestoreSubscription(c *gin.Context) {
|
|
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
|
|
|
var req services.ProcessPurchaseRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Same logic as ProcessPurchase - validates receipt/token and restores
|
|
var subscription *services.SubscriptionResponse
|
|
var err error
|
|
|
|
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)
|
|
}
|
|
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"message": i18n.LocalizedMessage(c, "message.subscription_restored"),
|
|
"subscription": subscription,
|
|
})
|
|
}
|