Updated subscriptions
This commit is contained in:
@@ -38,8 +38,8 @@ type accountResponse struct {
|
||||
|
||||
var tierPlanLabels = map[domain.SubscriptionTier]string{ //nolint:gochecknoglobals
|
||||
domain.TierFree: "Free Trial",
|
||||
domain.TierPremium: "Premium / $1.99 mo",
|
||||
domain.TierPro: "Pro / $11.99 mo",
|
||||
domain.TierPremium: "Premium / $8.78 mo",
|
||||
domain.TierPro: "Pro / $16.78 mo",
|
||||
}
|
||||
|
||||
func (h *AccountHandler) GetAccount(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -1,36 +1,86 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/stripe/stripe-go/v82"
|
||||
portalsession "github.com/stripe/stripe-go/v82/billingportal/session"
|
||||
checkoutsession "github.com/stripe/stripe-go/v82/checkout/session"
|
||||
"github.com/stripe/stripe-go/v82/webhook"
|
||||
|
||||
kpfirebase "github.com/kjannette/koin-ping/backend/internal/firebase"
|
||||
"github.com/kjannette/koin-ping/backend/internal/config"
|
||||
"github.com/kjannette/koin-ping/backend/internal/domain"
|
||||
"github.com/kjannette/koin-ping/backend/internal/middleware"
|
||||
"github.com/kjannette/koin-ping/backend/internal/models"
|
||||
"github.com/stripe/stripe-go/v82"
|
||||
portalsession "github.com/stripe/stripe-go/v82/billingportal/session"
|
||||
checkoutsession "github.com/stripe/stripe-go/v82/checkout/session"
|
||||
"github.com/stripe/stripe-go/v82/webhook"
|
||||
)
|
||||
|
||||
const webhookMaxBodyBytes = 65536
|
||||
|
||||
type StripeHandler struct {
|
||||
users *models.UserModel
|
||||
cfg *config.Config
|
||||
users *models.UserModel
|
||||
alerts *models.AlertRuleModel
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func NewStripeHandler(users *models.UserModel, cfg *config.Config) *StripeHandler {
|
||||
func NewStripeHandler(users *models.UserModel, alerts *models.AlertRuleModel, cfg *config.Config) *StripeHandler {
|
||||
stripe.Key = cfg.StripeSecretKey
|
||||
return &StripeHandler{users: users, cfg: cfg}
|
||||
return &StripeHandler{users: users, alerts: alerts, cfg: cfg}
|
||||
}
|
||||
|
||||
func (h *StripeHandler) priceIDForTier(tier domain.SubscriptionTier) (string, error) {
|
||||
func (h *StripeHandler) ensureUserFirebaseAndAlertsEnabled(ctx context.Context, localUserID string) {
|
||||
user, err := h.users.GetByID(ctx, localUserID)
|
||||
if err != nil || user == nil || user.FirebaseUID == "" {
|
||||
return
|
||||
}
|
||||
if firebaseErr := kpfirebase.SetUserDisabled(ctx, user.FirebaseUID, false); firebaseErr != nil {
|
||||
log.Printf("Billing access restore: firebase enable failed for user %s: %v", localUserID, firebaseErr)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
n, alertsErr := h.alerts.EnableAllForUser(ctx, localUserID)
|
||||
if alertsErr != nil {
|
||||
log.Printf("Billing access restore: enable alerts failed for user %s: %v", localUserID, alertsErr)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Billing access restored: user %s, %d alert rules enabled", localUserID, n)
|
||||
}
|
||||
|
||||
func (h *StripeHandler) restorePaidSubscriptionAccess(ctx context.Context, stripeCustomerID, status string) {
|
||||
if stripeCustomerID == "" || (status != "active" && status != "trialing") {
|
||||
return
|
||||
}
|
||||
|
||||
u, err := h.users.GetByStripeCustomerID(ctx, stripeCustomerID)
|
||||
if err != nil || u == nil {
|
||||
if err != nil {
|
||||
log.Printf("restorePaidSubscriptionAccess: lookup %s: %v", stripeCustomerID, err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
h.ensureUserFirebaseAndAlertsEnabled(ctx, u.ID)
|
||||
}
|
||||
|
||||
func (h *StripeHandler) priceIDForTier(tier domain.SubscriptionTier, interval string) (string, error) {
|
||||
if interval == "annual" {
|
||||
switch tier {
|
||||
case domain.TierPremium:
|
||||
return h.cfg.StripePriceIDPremiumAnnual, nil
|
||||
case domain.TierPro:
|
||||
return h.cfg.StripePriceIDProAnnual, nil
|
||||
default:
|
||||
return "", fmt.Errorf("no Stripe price for tier %q", tier) //nolint:err113
|
||||
}
|
||||
}
|
||||
switch tier {
|
||||
case domain.TierPremium:
|
||||
return h.cfg.StripePriceIDPremium, nil
|
||||
@@ -46,7 +96,8 @@ func (h *StripeHandler) CreateCheckoutSession(w http.ResponseWriter, r *http.Req
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
|
||||
var body struct {
|
||||
Tier string `json:"tier"`
|
||||
Tier string `json:"tier"`
|
||||
Interval string `json:"interval"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "BAD_REQUEST", "Invalid request body")
|
||||
@@ -56,6 +107,9 @@ func (h *StripeHandler) CreateCheckoutSession(w http.ResponseWriter, r *http.Req
|
||||
if body.Tier == "" {
|
||||
body.Tier = "premium"
|
||||
}
|
||||
if body.Interval == "" {
|
||||
body.Interval = "annual"
|
||||
}
|
||||
|
||||
tier := domain.SubscriptionTier(body.Tier)
|
||||
if tier != domain.TierPremium && tier != domain.TierPro {
|
||||
@@ -63,7 +117,7 @@ func (h *StripeHandler) CreateCheckoutSession(w http.ResponseWriter, r *http.Req
|
||||
return
|
||||
}
|
||||
|
||||
priceID, err := h.priceIDForTier(tier)
|
||||
priceID, err := h.priceIDForTier(tier, body.Interval)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", err.Error())
|
||||
return
|
||||
@@ -177,6 +231,8 @@ func (h *StripeHandler) VerifyCheckoutSession(w http.ResponseWriter, r *http.Req
|
||||
if subscriptionID != "" && customerID != "" {
|
||||
if err := h.users.ActivateSubscription(r.Context(), customerID, subscriptionID, "active", tier); err != nil {
|
||||
log.Printf("VerifyCheckout: failed to activate subscription: %v", err)
|
||||
} else {
|
||||
h.restorePaidSubscriptionAccess(r.Context(), customerID, "active")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,6 +253,8 @@ func (h *StripeHandler) ActivateFreeTier(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
h.ensureUserFirebaseAndAlertsEnabled(r.Context(), userID)
|
||||
|
||||
log.Printf("Free tier activated for user %s", userID)
|
||||
writeJSON(w, http.StatusOK, map[string]string{
|
||||
"subscription_status": "active",
|
||||
@@ -209,8 +267,9 @@ func (h *StripeHandler) ActivateFreeTier(w http.ResponseWriter, r *http.Request)
|
||||
// The Firebase account is created on the frontend only after payment succeeds.
|
||||
func (h *StripeHandler) CreateOnboardingCheckout(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
Email string `json:"email"`
|
||||
Tier string `json:"tier"`
|
||||
Email string `json:"email"`
|
||||
Tier string `json:"tier"`
|
||||
Interval string `json:"interval"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "BAD_REQUEST", "Invalid request body")
|
||||
@@ -225,6 +284,9 @@ func (h *StripeHandler) CreateOnboardingCheckout(w http.ResponseWriter, r *http.
|
||||
if body.Tier == "" {
|
||||
body.Tier = "premium"
|
||||
}
|
||||
if body.Interval == "" {
|
||||
body.Interval = "annual"
|
||||
}
|
||||
|
||||
tier := domain.SubscriptionTier(body.Tier)
|
||||
if tier != domain.TierPremium && tier != domain.TierPro {
|
||||
@@ -232,7 +294,7 @@ func (h *StripeHandler) CreateOnboardingCheckout(w http.ResponseWriter, r *http.
|
||||
return
|
||||
}
|
||||
|
||||
priceID, err := h.priceIDForTier(tier)
|
||||
priceID, err := h.priceIDForTier(tier, body.Interval)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", err.Error())
|
||||
return
|
||||
@@ -362,6 +424,8 @@ func (h *StripeHandler) handleCheckoutCompleted(r *http.Request, event stripe.Ev
|
||||
if subscriptionID != "" && customerID != "" {
|
||||
if err := h.users.ActivateSubscription(r.Context(), customerID, subscriptionID, "active", tier); err != nil {
|
||||
log.Printf("Failed to activate subscription: %v", err)
|
||||
} else {
|
||||
h.restorePaidSubscriptionAccess(r.Context(), customerID, "active")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,8 +465,12 @@ func (h *StripeHandler) handleSubscriptionUpdated(r *http.Request, event stripe.
|
||||
|
||||
if err := h.users.ActivateSubscription(r.Context(), customerID, sub.ID, status, tier); err != nil {
|
||||
log.Printf("Failed to update subscription: %v", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
h.restorePaidSubscriptionAccess(r.Context(), customerID, status)
|
||||
|
||||
log.Printf("Subscription %s updated to %s (tier %s) for customer %s", sub.ID, status, tier, customerID)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user