Support for annual billing intervals
This commit is contained in:
parent
07afaf961d
commit
ef9d6d9f6c
17 changed files with 453 additions and 183 deletions
|
@ -45,11 +45,11 @@ type Server struct {
|
|||
visitors map[string]*visitor // ip:<ip> or user:<user>
|
||||
firebaseClient *firebaseClient
|
||||
messages int64
|
||||
userManager *user.Manager // Might be nil!
|
||||
messageCache *messageCache // Database that stores the messages
|
||||
fileCache *fileCache // File system based cache that stores attachments
|
||||
stripe stripeAPI // Stripe API, can be replaced with a mock
|
||||
priceCache *util.LookupCache[map[string]string] // Stripe price ID -> formatted price
|
||||
userManager *user.Manager // Might be nil!
|
||||
messageCache *messageCache // Database that stores the messages
|
||||
fileCache *fileCache // File system based cache that stores attachments
|
||||
stripe stripeAPI // Stripe API, can be replaced with a mock
|
||||
priceCache *util.LookupCache[map[string]int64] // Stripe price ID -> price as cents (USD implied!)
|
||||
closeChan chan bool
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
|
|
@ -100,6 +100,7 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *vis
|
|||
Customer: true,
|
||||
Subscription: u.Billing.StripeSubscriptionID != "",
|
||||
Status: string(u.Billing.StripeSubscriptionStatus),
|
||||
Interval: string(u.Billing.StripeSubscriptionInterval),
|
||||
PaidUntil: u.Billing.StripeSubscriptionPaidUntil.Unix(),
|
||||
CancelAt: u.Billing.StripeSubscriptionCancelAt.Unix(),
|
||||
}
|
||||
|
|
|
@ -80,14 +80,17 @@ func (s *Server) handleBillingTiersGet(w http.ResponseWriter, _ *http.Request, _
|
|||
return err
|
||||
}
|
||||
for _, tier := range tiers {
|
||||
priceStr, ok := prices[tier.StripePriceID]
|
||||
if tier.StripePriceID == "" || !ok {
|
||||
priceMonth, priceYear := prices[tier.StripeMonthlyPriceID], prices[tier.StripeYearlyPriceID]
|
||||
if priceMonth == 0 || priceYear == 0 { // Only allow tiers that have both prices!
|
||||
continue
|
||||
}
|
||||
response = append(response, &apiAccountBillingTier{
|
||||
Code: tier.Code,
|
||||
Name: tier.Name,
|
||||
Price: priceStr,
|
||||
Code: tier.Code,
|
||||
Name: tier.Name,
|
||||
Prices: &apiAccountBillingPrices{
|
||||
Month: priceMonth,
|
||||
Year: priceYear,
|
||||
},
|
||||
Limits: &apiAccountLimits{
|
||||
Basis: string(visitorLimitBasisTier),
|
||||
Messages: tier.MessageLimit,
|
||||
|
@ -117,11 +120,21 @@ func (s *Server) handleAccountBillingSubscriptionCreate(w http.ResponseWriter, r
|
|||
tier, err := s.userManager.Tier(req.Tier)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if tier.StripePriceID == "" {
|
||||
}
|
||||
var priceID string
|
||||
if req.Interval == string(stripe.PriceRecurringIntervalMonth) && tier.StripeMonthlyPriceID != "" {
|
||||
priceID = tier.StripeMonthlyPriceID
|
||||
} else if req.Interval == string(stripe.PriceRecurringIntervalYear) && tier.StripeYearlyPriceID != "" {
|
||||
priceID = tier.StripeYearlyPriceID
|
||||
} else {
|
||||
return errNotAPaidTier
|
||||
}
|
||||
logvr(v, r).
|
||||
With(tier).
|
||||
Fields(log.Context{
|
||||
"stripe_price_id": priceID,
|
||||
"stripe_subscription_interval": req.Interval,
|
||||
}).
|
||||
Tag(tagStripe).
|
||||
Info("Creating Stripe checkout flow")
|
||||
var stripeCustomerID *string
|
||||
|
@ -143,7 +156,7 @@ func (s *Server) handleAccountBillingSubscriptionCreate(w http.ResponseWriter, r
|
|||
AllowPromotionCodes: stripe.Bool(true),
|
||||
LineItems: []*stripe.CheckoutSessionLineItemParams{
|
||||
{
|
||||
Price: stripe.String(tier.StripePriceID),
|
||||
Price: stripe.String(priceID),
|
||||
Quantity: stripe.Int64(1),
|
||||
},
|
||||
},
|
||||
|
@ -180,10 +193,11 @@ func (s *Server) handleAccountBillingSubscriptionCreateSuccess(w http.ResponseWr
|
|||
sub, err := s.stripe.GetSubscription(sess.Subscription.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if sub.Items == nil || len(sub.Items.Data) != 1 || sub.Items.Data[0].Price == nil {
|
||||
} else if sub.Items == nil || len(sub.Items.Data) != 1 || sub.Items.Data[0].Price == nil || sub.Items.Data[0].Price.Recurring == nil {
|
||||
return wrapErrHTTP(errHTTPBadRequestBillingRequestInvalid, "more than one line item in existing subscription")
|
||||
}
|
||||
tier, err := s.userManager.TierByStripePrice(sub.Items.Data[0].Price.ID)
|
||||
priceID, interval := sub.Items.Data[0].Price.ID, sub.Items.Data[0].Price.Recurring.Interval
|
||||
tier, err := s.userManager.TierByStripePrice(priceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -197,8 +211,10 @@ func (s *Server) handleAccountBillingSubscriptionCreateSuccess(w http.ResponseWr
|
|||
Tag(tagStripe).
|
||||
Fields(log.Context{
|
||||
"stripe_customer_id": sess.Customer.ID,
|
||||
"stripe_price_id": priceID,
|
||||
"stripe_subscription_id": sub.ID,
|
||||
"stripe_subscription_status": string(sub.Status),
|
||||
"stripe_subscription_interval": string(interval),
|
||||
"stripe_subscription_paid_until": sub.CurrentPeriodEnd,
|
||||
}).
|
||||
Info("Stripe checkout flow succeeded, updating user tier and subscription")
|
||||
|
@ -213,7 +229,7 @@ func (s *Server) handleAccountBillingSubscriptionCreateSuccess(w http.ResponseWr
|
|||
if _, err := s.stripe.UpdateCustomer(sess.Customer.ID, customerParams); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.updateSubscriptionAndTier(r, v, u, tier, sess.Customer.ID, sub.ID, string(sub.Status), sub.CurrentPeriodEnd, sub.CancelAt); err != nil {
|
||||
if err := s.updateSubscriptionAndTier(r, v, u, tier, sess.Customer.ID, sub.ID, string(sub.Status), string(interval), sub.CurrentPeriodEnd, sub.CancelAt); err != nil {
|
||||
return err
|
||||
}
|
||||
http.Redirect(w, r, s.config.BaseURL+accountPath, http.StatusSeeOther)
|
||||
|
@ -235,15 +251,24 @@ func (s *Server) handleAccountBillingSubscriptionUpdate(w http.ResponseWriter, r
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var priceID string
|
||||
if req.Interval == string(stripe.PriceRecurringIntervalMonth) && tier.StripeMonthlyPriceID != "" {
|
||||
priceID = tier.StripeMonthlyPriceID
|
||||
} else if req.Interval == string(stripe.PriceRecurringIntervalYear) && tier.StripeYearlyPriceID != "" {
|
||||
priceID = tier.StripeYearlyPriceID
|
||||
} else {
|
||||
return errNotAPaidTier
|
||||
}
|
||||
logvr(v, r).
|
||||
Tag(tagStripe).
|
||||
Fields(log.Context{
|
||||
"new_tier_id": tier.ID,
|
||||
"new_tier_name": tier.Name,
|
||||
"new_tier_stripe_price_id": tier.StripePriceID,
|
||||
"new_tier_id": tier.ID,
|
||||
"new_tier_code": tier.Code,
|
||||
"new_tier_stripe_price_id": priceID,
|
||||
"new_tier_stripe_subscription_interval": req.Interval,
|
||||
// Other stripe_* fields filled by visitor context
|
||||
}).
|
||||
Info("Changing Stripe subscription and billing tier to %s/%s (price %s)", tier.ID, tier.Name, tier.StripePriceID)
|
||||
Info("Changing Stripe subscription and billing tier to %s/%s (price %s, %s)", tier.ID, tier.Name, priceID, req.Interval)
|
||||
sub, err := s.stripe.GetSubscription(u.Billing.StripeSubscriptionID)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -252,11 +277,11 @@ func (s *Server) handleAccountBillingSubscriptionUpdate(w http.ResponseWriter, r
|
|||
}
|
||||
params := &stripe.SubscriptionParams{
|
||||
CancelAtPeriodEnd: stripe.Bool(false),
|
||||
ProrationBehavior: stripe.String(string(stripe.SubscriptionSchedulePhaseProrationBehaviorCreateProrations)),
|
||||
ProrationBehavior: stripe.String(string(stripe.SubscriptionSchedulePhaseProrationBehaviorAlwaysInvoice)),
|
||||
Items: []*stripe.SubscriptionItemsParams{
|
||||
{
|
||||
ID: stripe.String(sub.Items.Data[0].ID),
|
||||
Price: stripe.String(tier.StripePriceID),
|
||||
Price: stripe.String(priceID),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -345,20 +370,22 @@ func (s *Server) handleAccountBillingWebhookSubscriptionUpdated(r *http.Request,
|
|||
ev, err := util.UnmarshalJSON[apiStripeSubscriptionUpdatedEvent](io.NopCloser(bytes.NewReader(event.Data.Raw)))
|
||||
if err != nil {
|
||||
return err
|
||||
} else if ev.ID == "" || ev.Customer == "" || ev.Status == "" || ev.CurrentPeriodEnd == 0 || ev.Items == nil || len(ev.Items.Data) != 1 || ev.Items.Data[0].Price == nil || ev.Items.Data[0].Price.ID == "" {
|
||||
} else if ev.ID == "" || ev.Customer == "" || ev.Status == "" || ev.CurrentPeriodEnd == 0 || ev.Items == nil || len(ev.Items.Data) != 1 || ev.Items.Data[0].Price == nil || ev.Items.Data[0].Price.ID == "" || ev.Items.Data[0].Price.Recurring == nil {
|
||||
logvr(v, r).Tag(tagStripe).Field("stripe_request", fmt.Sprintf("%#v", ev)).Warn("Unexpected request from Stripe")
|
||||
return errHTTPBadRequestBillingRequestInvalid
|
||||
}
|
||||
subscriptionID, priceID := ev.ID, ev.Items.Data[0].Price.ID
|
||||
subscriptionID, priceID, interval := ev.ID, ev.Items.Data[0].Price.ID, ev.Items.Data[0].Price.Recurring.Interval
|
||||
logvr(v, r).
|
||||
Tag(tagStripe).
|
||||
Fields(log.Context{
|
||||
"stripe_webhook_type": event.Type,
|
||||
"stripe_customer_id": ev.Customer,
|
||||
"stripe_price_id": priceID,
|
||||
"stripe_subscription_id": ev.ID,
|
||||
"stripe_subscription_status": ev.Status,
|
||||
"stripe_subscription_interval": interval,
|
||||
"stripe_subscription_paid_until": ev.CurrentPeriodEnd,
|
||||
"stripe_subscription_cancel_at": ev.CancelAt,
|
||||
"stripe_price_id": priceID,
|
||||
}).
|
||||
Info("Updating subscription to status %s, with price %s", ev.Status, priceID)
|
||||
userFn := func() (*user.User, error) {
|
||||
|
@ -376,7 +403,7 @@ func (s *Server) handleAccountBillingWebhookSubscriptionUpdated(r *http.Request,
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.updateSubscriptionAndTier(r, v, u, tier, ev.Customer, subscriptionID, ev.Status, ev.CurrentPeriodEnd, ev.CancelAt); err != nil {
|
||||
if err := s.updateSubscriptionAndTier(r, v, u, tier, ev.Customer, subscriptionID, ev.Status, string(interval), ev.CurrentPeriodEnd, ev.CancelAt); err != nil {
|
||||
return err
|
||||
}
|
||||
s.publishSyncEventAsync(s.visitor(netip.IPv4Unspecified(), u))
|
||||
|
@ -399,14 +426,14 @@ func (s *Server) handleAccountBillingWebhookSubscriptionDeleted(r *http.Request,
|
|||
Tag(tagStripe).
|
||||
Field("stripe_webhook_type", event.Type).
|
||||
Info("Subscription deleted, downgrading to unpaid tier")
|
||||
if err := s.updateSubscriptionAndTier(r, v, u, nil, ev.Customer, "", "", 0, 0); err != nil {
|
||||
if err := s.updateSubscriptionAndTier(r, v, u, nil, ev.Customer, "", "", "", 0, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
s.publishSyncEventAsync(s.visitor(netip.IPv4Unspecified(), u))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) updateSubscriptionAndTier(r *http.Request, v *visitor, u *user.User, tier *user.Tier, customerID, subscriptionID, status string, paidUntil, cancelAt int64) error {
|
||||
func (s *Server) updateSubscriptionAndTier(r *http.Request, v *visitor, u *user.User, tier *user.Tier, customerID, subscriptionID, status, interval string, paidUntil, cancelAt int64) error {
|
||||
reservationsLimit := visitorDefaultReservationsLimit
|
||||
if tier != nil {
|
||||
reservationsLimit = tier.ReservationLimit
|
||||
|
@ -423,9 +450,8 @@ func (s *Server) updateSubscriptionAndTier(r *http.Request, v *visitor, u *user.
|
|||
logvr(v, r).
|
||||
Tag(tagStripe).
|
||||
Fields(log.Context{
|
||||
"new_tier_id": tier.ID,
|
||||
"new_tier_name": tier.Name,
|
||||
"new_tier_stripe_price_id": tier.StripePriceID,
|
||||
"new_tier_id": tier.ID,
|
||||
"new_tier_code": tier.Code,
|
||||
}).
|
||||
Info("Changing tier to tier %s (%s) for user %s", tier.ID, tier.Name, u.Name)
|
||||
if err := s.userManager.ChangeTier(u.Name, tier.Code); err != nil {
|
||||
|
@ -437,6 +463,7 @@ func (s *Server) updateSubscriptionAndTier(r *http.Request, v *visitor, u *user.
|
|||
StripeCustomerID: customerID,
|
||||
StripeSubscriptionID: subscriptionID,
|
||||
StripeSubscriptionStatus: stripe.SubscriptionStatus(status),
|
||||
StripeSubscriptionInterval: stripe.PriceRecurringInterval(interval),
|
||||
StripeSubscriptionPaidUntil: time.Unix(paidUntil, 0),
|
||||
StripeSubscriptionCancelAt: time.Unix(cancelAt, 0),
|
||||
}
|
||||
|
@ -448,20 +475,16 @@ func (s *Server) updateSubscriptionAndTier(r *http.Request, v *visitor, u *user.
|
|||
|
||||
// fetchStripePrices contacts the Stripe API to retrieve all prices. This is used by the server to cache the prices
|
||||
// in memory, and ultimately for the web app to display the price table.
|
||||
func (s *Server) fetchStripePrices() (map[string]string, error) {
|
||||
func (s *Server) fetchStripePrices() (map[string]int64, error) {
|
||||
log.Debug("Caching prices from Stripe API")
|
||||
priceMap := make(map[string]string)
|
||||
priceMap := make(map[string]int64)
|
||||
prices, err := s.stripe.ListPrices(&stripe.PriceListParams{Active: stripe.Bool(true)})
|
||||
if err != nil {
|
||||
log.Warn("Fetching Stripe prices failed: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
for _, p := range prices {
|
||||
if p.UnitAmount%100 == 0 {
|
||||
priceMap[p.ID] = fmt.Sprintf("$%d", p.UnitAmount/100)
|
||||
} else {
|
||||
priceMap[p.ID] = fmt.Sprintf("$%.2f", float64(p.UnitAmount)/100)
|
||||
}
|
||||
priceMap[p.ID] = p.UnitAmount
|
||||
log.Trace("- Caching price %s = %v", p.ID, priceMap[p.ID])
|
||||
}
|
||||
return priceMap, nil
|
||||
|
|
|
@ -37,7 +37,9 @@ func TestPayments_Tiers(t *testing.T) {
|
|||
On("ListPrices", mock.Anything).
|
||||
Return([]*stripe.Price{
|
||||
{ID: "price_123", UnitAmount: 500},
|
||||
{ID: "price_124", UnitAmount: 5000},
|
||||
{ID: "price_456", UnitAmount: 1000},
|
||||
{ID: "price_457", UnitAmount: 10000},
|
||||
{ID: "price_999", UnitAmount: 9999},
|
||||
}, nil)
|
||||
|
||||
|
@ -58,7 +60,8 @@ func TestPayments_Tiers(t *testing.T) {
|
|||
AttachmentFileSizeLimit: 999,
|
||||
AttachmentTotalSizeLimit: 888,
|
||||
AttachmentExpiryDuration: time.Minute,
|
||||
StripePriceID: "price_123",
|
||||
StripeMonthlyPriceID: "price_123",
|
||||
StripeYearlyPriceID: "price_124",
|
||||
}))
|
||||
require.Nil(t, s.userManager.AddTier(&user.Tier{
|
||||
ID: "ti_444",
|
||||
|
@ -71,7 +74,8 @@ func TestPayments_Tiers(t *testing.T) {
|
|||
AttachmentFileSizeLimit: 999111,
|
||||
AttachmentTotalSizeLimit: 888111,
|
||||
AttachmentExpiryDuration: time.Hour,
|
||||
StripePriceID: "price_456",
|
||||
StripeMonthlyPriceID: "price_456",
|
||||
StripeYearlyPriceID: "price_457",
|
||||
}))
|
||||
response := request(t, s, "GET", "/v1/tiers", "", nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
|
@ -98,6 +102,8 @@ func TestPayments_Tiers(t *testing.T) {
|
|||
require.Equal(t, "pro", tier.Code)
|
||||
require.Equal(t, "Pro", tier.Name)
|
||||
require.Equal(t, "tier", tier.Limits.Basis)
|
||||
require.Equal(t, int64(500), tier.Prices.Month)
|
||||
require.Equal(t, int64(5000), tier.Prices.Year)
|
||||
require.Equal(t, int64(777), tier.Limits.Reservations)
|
||||
require.Equal(t, int64(1000), tier.Limits.Messages)
|
||||
require.Equal(t, int64(3600), tier.Limits.MessagesExpiryDuration)
|
||||
|
@ -109,6 +115,8 @@ func TestPayments_Tiers(t *testing.T) {
|
|||
tier = tiers[2]
|
||||
require.Equal(t, "business", tier.Code)
|
||||
require.Equal(t, "Business", tier.Name)
|
||||
require.Equal(t, int64(1000), tier.Prices.Month)
|
||||
require.Equal(t, int64(10000), tier.Prices.Year)
|
||||
require.Equal(t, "tier", tier.Limits.Basis)
|
||||
require.Equal(t, int64(777333), tier.Limits.Reservations)
|
||||
require.Equal(t, int64(2000), tier.Limits.Messages)
|
||||
|
@ -136,14 +144,14 @@ func TestPayments_SubscriptionCreate_NotAStripeCustomer_Success(t *testing.T) {
|
|||
|
||||
// Create tier and user
|
||||
require.Nil(t, s.userManager.AddTier(&user.Tier{
|
||||
ID: "ti_123",
|
||||
Code: "pro",
|
||||
StripePriceID: "price_123",
|
||||
ID: "ti_123",
|
||||
Code: "pro",
|
||||
StripeMonthlyPriceID: "price_123",
|
||||
}))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||
|
||||
// Create subscription
|
||||
response := request(t, s, "POST", "/v1/account/billing/subscription", `{"tier": "pro"}`, map[string]string{
|
||||
response := request(t, s, "POST", "/v1/account/billing/subscription", `{"tier": "pro", "interval": "month"}`, map[string]string{
|
||||
"Authorization": util.BasicAuth("phil", "phil"),
|
||||
})
|
||||
require.Equal(t, 200, response.Code)
|
||||
|
@ -172,9 +180,9 @@ func TestPayments_SubscriptionCreate_StripeCustomer_Success(t *testing.T) {
|
|||
|
||||
// Create tier and user
|
||||
require.Nil(t, s.userManager.AddTier(&user.Tier{
|
||||
ID: "ti_123",
|
||||
Code: "pro",
|
||||
StripePriceID: "price_123",
|
||||
ID: "ti_123",
|
||||
Code: "pro",
|
||||
StripeMonthlyPriceID: "price_123",
|
||||
}))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||
|
||||
|
@ -187,7 +195,7 @@ func TestPayments_SubscriptionCreate_StripeCustomer_Success(t *testing.T) {
|
|||
require.Nil(t, s.userManager.ChangeBilling(u.Name, billing))
|
||||
|
||||
// Create subscription
|
||||
response := request(t, s, "POST", "/v1/account/billing/subscription", `{"tier": "pro"}`, map[string]string{
|
||||
response := request(t, s, "POST", "/v1/account/billing/subscription", `{"tier": "pro", "interval": "month"}`, map[string]string{
|
||||
"Authorization": util.BasicAuth("phil", "phil"),
|
||||
})
|
||||
require.Equal(t, 200, response.Code)
|
||||
|
@ -214,9 +222,9 @@ func TestPayments_AccountDelete_Cancels_Subscription(t *testing.T) {
|
|||
|
||||
// Create tier and user
|
||||
require.Nil(t, s.userManager.AddTier(&user.Tier{
|
||||
ID: "ti_123",
|
||||
Code: "pro",
|
||||
StripePriceID: "price_123",
|
||||
ID: "ti_123",
|
||||
Code: "pro",
|
||||
StripeMonthlyPriceID: "price_123",
|
||||
}))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||
|
||||
|
@ -267,7 +275,7 @@ func TestPayments_Checkout_Success_And_Increase_Rate_Limits_Reset_Visitor(t *tes
|
|||
require.Nil(t, s.userManager.AddTier(&user.Tier{
|
||||
ID: "ti_123",
|
||||
Code: "starter",
|
||||
StripePriceID: "price_1234",
|
||||
StripeMonthlyPriceID: "price_1234",
|
||||
ReservationLimit: 1,
|
||||
MessageLimit: 220, // 220 * 5% = 11 requests before rate limiting kicks in
|
||||
MessageExpiryDuration: time.Hour,
|
||||
|
@ -298,7 +306,12 @@ func TestPayments_Checkout_Success_And_Increase_Rate_Limits_Reset_Visitor(t *tes
|
|||
Items: &stripe.SubscriptionItemList{
|
||||
Data: []*stripe.SubscriptionItem{
|
||||
{
|
||||
Price: &stripe.Price{ID: "price_1234"},
|
||||
Price: &stripe.Price{
|
||||
ID: "price_1234",
|
||||
Recurring: &stripe.PriceRecurring{
|
||||
Interval: stripe.PriceRecurringIntervalMonth,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -333,6 +346,7 @@ func TestPayments_Checkout_Success_And_Increase_Rate_Limits_Reset_Visitor(t *tes
|
|||
require.Equal(t, "", u.Billing.StripeCustomerID)
|
||||
require.Equal(t, "", u.Billing.StripeSubscriptionID)
|
||||
require.Equal(t, stripe.SubscriptionStatus(""), u.Billing.StripeSubscriptionStatus)
|
||||
require.Equal(t, stripe.PriceRecurringInterval(""), u.Billing.StripeSubscriptionInterval)
|
||||
require.Equal(t, int64(0), u.Billing.StripeSubscriptionPaidUntil.Unix())
|
||||
require.Equal(t, int64(0), u.Billing.StripeSubscriptionCancelAt.Unix())
|
||||
require.Equal(t, int64(0), u.Stats.Messages) // Messages and emails are not persisted for no-tier users!
|
||||
|
@ -349,6 +363,7 @@ func TestPayments_Checkout_Success_And_Increase_Rate_Limits_Reset_Visitor(t *tes
|
|||
require.Equal(t, "acct_5555", u.Billing.StripeCustomerID)
|
||||
require.Equal(t, "sub_1234", u.Billing.StripeSubscriptionID)
|
||||
require.Equal(t, stripe.SubscriptionStatusActive, u.Billing.StripeSubscriptionStatus)
|
||||
require.Equal(t, stripe.PriceRecurringIntervalMonth, u.Billing.StripeSubscriptionInterval)
|
||||
require.Equal(t, int64(123456789), u.Billing.StripeSubscriptionPaidUntil.Unix())
|
||||
require.Equal(t, int64(0), u.Billing.StripeSubscriptionCancelAt.Unix())
|
||||
require.Equal(t, int64(0), u.Stats.Messages)
|
||||
|
@ -423,7 +438,7 @@ func TestPayments_Webhook_Subscription_Updated_Downgrade_From_PastDue_To_Active(
|
|||
require.Nil(t, s.userManager.AddTier(&user.Tier{
|
||||
ID: "ti_1",
|
||||
Code: "starter",
|
||||
StripePriceID: "price_1234", // !
|
||||
StripeMonthlyPriceID: "price_1234", // !
|
||||
ReservationLimit: 1, // !
|
||||
MessageLimit: 100,
|
||||
MessageExpiryDuration: time.Hour,
|
||||
|
@ -435,7 +450,7 @@ func TestPayments_Webhook_Subscription_Updated_Downgrade_From_PastDue_To_Active(
|
|||
require.Nil(t, s.userManager.AddTier(&user.Tier{
|
||||
ID: "ti_2",
|
||||
Code: "pro",
|
||||
StripePriceID: "price_1111", // !
|
||||
StripeMonthlyPriceID: "price_1111", // !
|
||||
ReservationLimit: 3, // !
|
||||
MessageLimit: 200,
|
||||
MessageExpiryDuration: time.Hour,
|
||||
|
@ -457,6 +472,7 @@ func TestPayments_Webhook_Subscription_Updated_Downgrade_From_PastDue_To_Active(
|
|||
StripeCustomerID: "acct_5555",
|
||||
StripeSubscriptionID: "sub_1234",
|
||||
StripeSubscriptionStatus: stripe.SubscriptionStatusPastDue,
|
||||
StripeSubscriptionInterval: stripe.PriceRecurringIntervalMonth,
|
||||
StripeSubscriptionPaidUntil: time.Unix(123, 0),
|
||||
StripeSubscriptionCancelAt: time.Unix(456, 0),
|
||||
}
|
||||
|
@ -499,9 +515,10 @@ func TestPayments_Webhook_Subscription_Updated_Downgrade_From_PastDue_To_Active(
|
|||
require.Equal(t, "starter", u.Tier.Code) // Not "pro"
|
||||
require.Equal(t, "acct_5555", u.Billing.StripeCustomerID)
|
||||
require.Equal(t, "sub_1234", u.Billing.StripeSubscriptionID)
|
||||
require.Equal(t, stripe.SubscriptionStatusActive, u.Billing.StripeSubscriptionStatus) // Not "past_due"
|
||||
require.Equal(t, int64(1674268231), u.Billing.StripeSubscriptionPaidUntil.Unix()) // Updated
|
||||
require.Equal(t, int64(1674299999), u.Billing.StripeSubscriptionCancelAt.Unix()) // Updated
|
||||
require.Equal(t, stripe.SubscriptionStatusActive, u.Billing.StripeSubscriptionStatus) // Not "past_due"
|
||||
require.Equal(t, stripe.PriceRecurringIntervalYear, u.Billing.StripeSubscriptionInterval) // Not "month"
|
||||
require.Equal(t, int64(1674268231), u.Billing.StripeSubscriptionPaidUntil.Unix()) // Updated
|
||||
require.Equal(t, int64(1674299999), u.Billing.StripeSubscriptionCancelAt.Unix()) // Updated
|
||||
|
||||
// Verify that reservations were deleted
|
||||
r, err := s.userManager.Reservations("phil")
|
||||
|
@ -546,10 +563,10 @@ func TestPayments_Webhook_Subscription_Deleted(t *testing.T) {
|
|||
|
||||
// Create a user with a Stripe subscription and 3 reservations
|
||||
require.Nil(t, s.userManager.AddTier(&user.Tier{
|
||||
ID: "ti_1",
|
||||
Code: "pro",
|
||||
StripePriceID: "price_1234",
|
||||
ReservationLimit: 1,
|
||||
ID: "ti_1",
|
||||
Code: "pro",
|
||||
StripeMonthlyPriceID: "price_1234",
|
||||
ReservationLimit: 1,
|
||||
}))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||
require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
|
||||
|
@ -562,6 +579,7 @@ func TestPayments_Webhook_Subscription_Deleted(t *testing.T) {
|
|||
StripeCustomerID: "acct_5555",
|
||||
StripeSubscriptionID: "sub_1234",
|
||||
StripeSubscriptionStatus: stripe.SubscriptionStatusPastDue,
|
||||
StripeSubscriptionInterval: stripe.PriceRecurringIntervalMonth,
|
||||
StripeSubscriptionPaidUntil: time.Unix(123, 0),
|
||||
StripeSubscriptionCancelAt: time.Unix(0, 0),
|
||||
}))
|
||||
|
@ -615,11 +633,11 @@ func TestPayments_Subscription_Update_Different_Tier(t *testing.T) {
|
|||
stripeMock.
|
||||
On("UpdateSubscription", "sub_123", &stripe.SubscriptionParams{
|
||||
CancelAtPeriodEnd: stripe.Bool(false),
|
||||
ProrationBehavior: stripe.String(string(stripe.SubscriptionSchedulePhaseProrationBehaviorCreateProrations)),
|
||||
ProrationBehavior: stripe.String(string(stripe.SubscriptionSchedulePhaseProrationBehaviorAlwaysInvoice)),
|
||||
Items: []*stripe.SubscriptionItemsParams{
|
||||
{
|
||||
ID: stripe.String("someid_123"),
|
||||
Price: stripe.String("price_456"),
|
||||
Price: stripe.String("price_457"),
|
||||
},
|
||||
},
|
||||
}).
|
||||
|
@ -627,14 +645,16 @@ func TestPayments_Subscription_Update_Different_Tier(t *testing.T) {
|
|||
|
||||
// Create tier and user
|
||||
require.Nil(t, s.userManager.AddTier(&user.Tier{
|
||||
ID: "ti_123",
|
||||
Code: "pro",
|
||||
StripePriceID: "price_123",
|
||||
ID: "ti_123",
|
||||
Code: "pro",
|
||||
StripeMonthlyPriceID: "price_123",
|
||||
StripeYearlyPriceID: "price_124",
|
||||
}))
|
||||
require.Nil(t, s.userManager.AddTier(&user.Tier{
|
||||
ID: "ti_456",
|
||||
Code: "business",
|
||||
StripePriceID: "price_456",
|
||||
ID: "ti_456",
|
||||
Code: "business",
|
||||
StripeMonthlyPriceID: "price_456",
|
||||
StripeYearlyPriceID: "price_457",
|
||||
}))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||
require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
|
||||
|
@ -644,7 +664,7 @@ func TestPayments_Subscription_Update_Different_Tier(t *testing.T) {
|
|||
}))
|
||||
|
||||
// Call endpoint to change subscription
|
||||
rr := request(t, s, "PUT", "/v1/account/billing/subscription", `{"tier":"business"}`, map[string]string{
|
||||
rr := request(t, s, "PUT", "/v1/account/billing/subscription", `{"tier":"business","interval":"year"}`, map[string]string{
|
||||
"Authorization": util.BasicAuth("phil", "phil"),
|
||||
})
|
||||
require.Equal(t, 200, rr.Code)
|
||||
|
@ -795,7 +815,10 @@ const subscriptionUpdatedEventJSON = `
|
|||
"data": [
|
||||
{
|
||||
"price": {
|
||||
"id": "price_1234"
|
||||
"id": "price_1234",
|
||||
"recurring": {
|
||||
"interval": "year"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -818,7 +841,10 @@ const subscriptionDeletedEventJSON = `
|
|||
"data": [
|
||||
{
|
||||
"price": {
|
||||
"id": "price_1234"
|
||||
"id": "price_1234",
|
||||
"recurring": {
|
||||
"interval": "month"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -309,6 +309,7 @@ type apiAccountBilling struct {
|
|||
Customer bool `json:"customer"`
|
||||
Subscription bool `json:"subscription"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Interval string `json:"interval,omitempty"`
|
||||
PaidUntil int64 `json:"paid_until,omitempty"`
|
||||
CancelAt int64 `json:"cancel_at,omitempty"`
|
||||
}
|
||||
|
@ -343,11 +344,16 @@ type apiConfigResponse struct {
|
|||
DisallowedTopics []string `json:"disallowed_topics"`
|
||||
}
|
||||
|
||||
type apiAccountBillingPrices struct {
|
||||
Month int64 `json:"month"`
|
||||
Year int64 `json:"year"`
|
||||
}
|
||||
|
||||
type apiAccountBillingTier struct {
|
||||
Code string `json:"code,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Price string `json:"price,omitempty"`
|
||||
Limits *apiAccountLimits `json:"limits"`
|
||||
Code string `json:"code,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Prices *apiAccountBillingPrices `json:"prices,omitempty"`
|
||||
Limits *apiAccountLimits `json:"limits"`
|
||||
}
|
||||
|
||||
type apiAccountBillingSubscriptionCreateResponse struct {
|
||||
|
@ -355,7 +361,8 @@ type apiAccountBillingSubscriptionCreateResponse struct {
|
|||
}
|
||||
|
||||
type apiAccountBillingSubscriptionChangeRequest struct {
|
||||
Tier string `json:"tier"`
|
||||
Tier string `json:"tier"`
|
||||
Interval string `json:"interval"`
|
||||
}
|
||||
|
||||
type apiAccountBillingPortalRedirectResponse struct {
|
||||
|
@ -385,7 +392,10 @@ type apiStripeSubscriptionUpdatedEvent struct {
|
|||
Items *struct {
|
||||
Data []*struct {
|
||||
Price *struct {
|
||||
ID string `json:"id"`
|
||||
ID string `json:"id"`
|
||||
Recurring *struct {
|
||||
Interval string `json:"interval"`
|
||||
} `json:"recurring"`
|
||||
} `json:"price"`
|
||||
} `json:"data"`
|
||||
} `json:"items"`
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue