ducky/subscriptions

Paddy 2015-09-30 Parent:fb2c0e498e37

15:aab6ba5ae392 Go to Latest

ducky/subscriptions/subscription_memstore.go

Log Postgres test failures more verbosely, fix SubscriptionChange.IsEmpty. SubscriptionChange.IsEmpty() would return false even if no actual database operations are going to be performed. This is because we allow information we _don't_ store in the database (Stripe source, Stripe email) to be specified in a SubscriptionChange object, just so we can easily access them. Then we use the Stripe API to store them in Stripe's databases, and turn them into data _we_ store in our database. Think of them as pre-processed values that are never stored raw. The problem is, we were treating these properties the same as the properties we actually stored in the database, and (worse) were running database tests for combinations of these properties, which was causing test failures because we were trying to update no columns in the database. Whoops. I removed these properties from the IsEmpty helper, and removed them from the code that generates the SubscriptionChange permutations for testing. This allows tests to pass, but also stays closer to what the system was designed to do. In tracking down this bug, I discovered that the logging we had for errors when running Postgres tests was inadequate, so I updated the logs when that failure occurs while testing Postgres to help surface future failures faster.

History
paddy@0 1 package subscriptions
paddy@0 2
paddy@0 3 import (
paddy@0 4 "code.secondbit.org/uuid.hg"
paddy@0 5 )
paddy@0 6
paddy@2 7 func stripeSubscriptionInMemstore(stripeSubscription string, m *Memstore) bool {
paddy@0 8 for _, sub := range m.subscriptions {
paddy@2 9 if sub.StripeSubscription == stripeSubscription {
paddy@0 10 return true
paddy@0 11 }
paddy@0 12 }
paddy@0 13 return false
paddy@0 14 }
paddy@0 15
paddy@14 16 // CreateSubscription stores the passed Subscription in the Memstore. If
paddy@14 17 // a Subscription sharing the same UserID already exists, an
paddy@14 18 // ErrSubscriptionAlreadyExists error will be returned. If a Subscription
paddy@14 19 // sharing the same StripeSubscription already exists, an
paddy@14 20 // ErrStripeSubscriptionAlreadyExists error will be returned.
paddy@3 21 func (m *Memstore) CreateSubscription(sub Subscription) error {
paddy@0 22 m.subscriptionLock.Lock()
paddy@0 23 defer m.subscriptionLock.Unlock()
paddy@0 24
paddy@1 25 if _, ok := m.subscriptions[sub.UserID.String()]; ok {
paddy@0 26 return ErrSubscriptionAlreadyExists
paddy@0 27 }
paddy@2 28 if stripeSubscriptionInMemstore(sub.StripeSubscription, m) {
paddy@2 29 return ErrStripeSubscriptionAlreadyExists
paddy@0 30 }
paddy@1 31 m.subscriptions[sub.UserID.String()] = sub
paddy@0 32 return nil
paddy@0 33 }
paddy@0 34
paddy@14 35 // UpdateSubscription applies the SubscriptionChange passed to the Subscription
paddy@14 36 // stored in the Memstore associated with the passed ID. If change is empty,
paddy@14 37 // an ErrSubscriptionChangeEmpty error is returned. If no Subscription is found
paddy@14 38 // in the Memstore with the passed ID, an ErrSubscriptionNotFound error is returned.
paddy@14 39 // If change is updating the StripeSubscription, and a Subscription in the Memstore
paddy@14 40 // already has that value set for StripeSubscription, an
paddy@14 41 // ErrStripeSubscriptionAlreadyExists error is returned.
paddy@3 42 func (m *Memstore) UpdateSubscription(id uuid.ID, change SubscriptionChange) error {
paddy@0 43 if change.IsEmpty() {
paddy@0 44 return ErrSubscriptionChangeEmpty
paddy@0 45 }
paddy@0 46
paddy@0 47 m.subscriptionLock.Lock()
paddy@0 48 defer m.subscriptionLock.Unlock()
paddy@0 49
paddy@0 50 s, ok := m.subscriptions[id.String()]
paddy@0 51 if !ok {
paddy@0 52 return ErrSubscriptionNotFound
paddy@0 53 }
paddy@2 54 if change.StripeSubscription != nil {
paddy@2 55 if stripeSubscriptionInMemstore(*change.StripeSubscription, m) {
paddy@2 56 return ErrStripeSubscriptionAlreadyExists
paddy@0 57 }
paddy@0 58 }
paddy@0 59 s.ApplyChange(change)
paddy@0 60 m.subscriptions[id.String()] = s
paddy@0 61 return nil
paddy@0 62 }
paddy@0 63
paddy@14 64 // DeleteSubscription removes the Subscription stored in the Memstore associated
paddy@14 65 // with the passed ID from the Memstore. If no Subscription is found
paddy@14 66 // in the Memstore with the passed ID, an ErrSubscriptionNotFound error is returned.
paddy@3 67 func (m *Memstore) DeleteSubscription(id uuid.ID) error {
paddy@0 68 m.subscriptionLock.Lock()
paddy@0 69 defer m.subscriptionLock.Unlock()
paddy@0 70
paddy@0 71 _, ok := m.subscriptions[id.String()]
paddy@0 72 if !ok {
paddy@0 73 return ErrSubscriptionNotFound
paddy@0 74 }
paddy@0 75 delete(m.subscriptions, id.String())
paddy@0 76 return nil
paddy@0 77 }
paddy@0 78
paddy@14 79 // GetSubscriptions retrieves the Subscriptions stored in the Memstore associated
paddy@14 80 // with the passed IDs. If no IDs are passed, an ErrNoSubscriptionID error is
paddy@14 81 // returned. No matter how many of the IDs are found (including none), a map is
paddy@14 82 // returned, with the key being a String()ed version of the ID for the Subscription in
paddy@14 83 // the value. If no error is returned, the map will represent all of the Subscriptions// matching the passed IDs that exist in the Memstore, even if it's empty.
paddy@3 84 func (m *Memstore) GetSubscriptions(ids []uuid.ID) (map[string]Subscription, error) {
paddy@0 85 if len(ids) < 1 {
paddy@0 86 return map[string]Subscription{}, ErrNoSubscriptionID
paddy@0 87 }
paddy@0 88 m.subscriptionLock.RLock()
paddy@0 89 defer m.subscriptionLock.RUnlock()
paddy@0 90
paddy@0 91 result := map[string]Subscription{}
paddy@0 92
paddy@0 93 for _, id := range ids {
paddy@0 94 s, ok := m.subscriptions[id.String()]
paddy@0 95 if !ok {
paddy@0 96 continue
paddy@0 97 }
paddy@1 98 result[s.UserID.String()] = s
paddy@0 99 }
paddy@0 100 return result, nil
paddy@0 101 }
paddy@0 102
paddy@14 103 // GetSubscriptionStats returns statistics about the subscription data stored in the
paddy@14 104 // Memstore as a SubscriptionStats variable. The number of Subscriptions, the
paddy@14 105 // breakdown of how many Subscriptions belong to each plan, the number of
paddy@14 106 // Subscriptions that are canceling, and the number of Subscriptions whose payment
paddy@14 107 // information is failing are all tracked.
paddy@3 108 func (m *Memstore) GetSubscriptionStats() (SubscriptionStats, error) {
paddy@0 109 m.subscriptionLock.RLock()
paddy@0 110 defer m.subscriptionLock.RUnlock()
paddy@0 111
paddy@2 112 stats := SubscriptionStats{
paddy@2 113 Plans: map[string]int64{},
paddy@2 114 }
paddy@1 115
paddy@0 116 for _, subscription := range m.subscriptions {
paddy@1 117 stats.Number++
paddy@2 118 stats.Plans[subscription.Plan]++
paddy@0 119
paddy@2 120 if subscription.Canceling {
paddy@2 121 stats.Canceling++
paddy@2 122 }
paddy@2 123 if subscription.Status == "past_due" || subscription.Status == "unpaid" {
paddy@2 124 stats.Failing++
paddy@2 125 }
paddy@1 126 }
paddy@1 127 return stats, nil
paddy@0 128 }