Use stripe's built-in subscriptions.
We're going to use Stripe's built-in subscriptions to manage our subscriptions,
which required us to change a lot of stuff. We're now tracking
stripe_subscription instead of stripe_customer, and we need to track the plan,
status, and if the user is canceling after this month. We also don't need to
know when to begin charging them (Stripe will do it), but we should track when
their trial starts and ends, when the current pay period they're in starts and
ends, when they canceled (if they've canceled), the number of failed charge
attempts they've had, and the last time we notified them about billing (To avoid
spamming users).
We get to delete all the stuff about periods, which is nice.
We updated our SubscriptionChange type to match. Notably, there are a lot of
non-user modifiable things now, but our Stripe webhook will need to use them to
update our database records and keep them in sync.
We no longer need to deal with sorting stuff, which is also nice.
Our SubscriptionStats have been updated to be... useful? Now we can track how
many users we have, and how many of them have failing credit cards, how many are
canceling at the end of their current payment period, and how many users are on
each plan.
We also switched around how the TestUpdateSubscription loops were written, to
avoid resetting more than we needed to. Before, we had to call store.reset()
after every single change iteration. Now we get to call it only when switching
stores. This makes a significant difference in the amount of time it takes to
run tests.
Finally, we added a test case for retrieving subscription stats. It's minimal,
but it works.
4 "code.secondbit.org/uuid.hg"
7 func stripeSubscriptionInMemstore(stripeSubscription string, m *Memstore) bool {
8 for _, sub := range m.subscriptions {
9 if sub.StripeSubscription == stripeSubscription {
16 func (m *Memstore) createSubscription(sub Subscription) error {
17 m.subscriptionLock.Lock()
18 defer m.subscriptionLock.Unlock()
20 if _, ok := m.subscriptions[sub.UserID.String()]; ok {
21 return ErrSubscriptionAlreadyExists
23 if stripeSubscriptionInMemstore(sub.StripeSubscription, m) {
24 return ErrStripeSubscriptionAlreadyExists
26 m.subscriptions[sub.UserID.String()] = sub
30 func (m *Memstore) updateSubscription(id uuid.ID, change SubscriptionChange) error {
32 return ErrSubscriptionChangeEmpty
35 m.subscriptionLock.Lock()
36 defer m.subscriptionLock.Unlock()
38 s, ok := m.subscriptions[id.String()]
40 return ErrSubscriptionNotFound
42 if change.StripeSubscription != nil {
43 if stripeSubscriptionInMemstore(*change.StripeSubscription, m) {
44 return ErrStripeSubscriptionAlreadyExists
48 m.subscriptions[id.String()] = s
52 func (m *Memstore) deleteSubscription(id uuid.ID) error {
53 m.subscriptionLock.Lock()
54 defer m.subscriptionLock.Unlock()
56 _, ok := m.subscriptions[id.String()]
58 return ErrSubscriptionNotFound
60 delete(m.subscriptions, id.String())
64 func (m *Memstore) getSubscriptions(ids []uuid.ID) (map[string]Subscription, error) {
66 return map[string]Subscription{}, ErrNoSubscriptionID
68 m.subscriptionLock.RLock()
69 defer m.subscriptionLock.RUnlock()
71 result := map[string]Subscription{}
73 for _, id := range ids {
74 s, ok := m.subscriptions[id.String()]
78 result[s.UserID.String()] = s
83 func (m *Memstore) getSubscriptionStats() (SubscriptionStats, error) {
84 m.subscriptionLock.RLock()
85 defer m.subscriptionLock.RUnlock()
87 stats := SubscriptionStats{
88 Plans: map[string]int64{},
91 for _, subscription := range m.subscriptions {
93 stats.Plans[subscription.Plan]++
95 if subscription.Canceling {
98 if subscription.Status == "past_due" || subscription.Status == "unpaid" {