Add Apple/Google IAP validation and subscription webhooks

- 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>
This commit is contained in:
Trey t
2025-12-14 13:58:37 -06:00
parent 81885c4ea3
commit c58aaa5d5f
10 changed files with 1909 additions and 52 deletions

View File

@@ -105,6 +105,43 @@ func (r *SubscriptionRepository) UpdatePurchaseToken(userID uint, token string)
Update("google_purchase_token", token).Error
}
// FindByAppleReceiptContains finds a subscription by Apple transaction ID
// Used by webhooks to find the user associated with a transaction
func (r *SubscriptionRepository) FindByAppleReceiptContains(transactionID string) (*models.UserSubscription, error) {
var sub models.UserSubscription
// Search for transaction ID in the stored receipt data
err := r.db.Where("apple_receipt_data LIKE ?", "%"+transactionID+"%").First(&sub).Error
if err != nil {
return nil, err
}
return &sub, nil
}
// FindByGoogleToken finds a subscription by Google purchase token
// Used by webhooks to find the user associated with a purchase
func (r *SubscriptionRepository) FindByGoogleToken(purchaseToken string) (*models.UserSubscription, error) {
var sub models.UserSubscription
err := r.db.Where("google_purchase_token = ?", purchaseToken).First(&sub).Error
if err != nil {
return nil, err
}
return &sub, nil
}
// SetCancelledAt sets the cancellation timestamp
func (r *SubscriptionRepository) SetCancelledAt(userID uint, cancelledAt time.Time) error {
return r.db.Model(&models.UserSubscription{}).
Where("user_id = ?", userID).
Update("cancelled_at", cancelledAt).Error
}
// ClearCancelledAt clears the cancellation timestamp (user resubscribed)
func (r *SubscriptionRepository) ClearCancelledAt(userID uint) error {
return r.db.Model(&models.UserSubscription{}).
Where("user_id = ?", userID).
Update("cancelled_at", nil).Error
}
// === Tier Limits ===
// GetTierLimits gets the limits for a subscription tier