ducky/subscriptions

Paddy 2015-09-30 Parent:1ff031bebf9e

15:aab6ba5ae392 Go to Latest

ducky/subscriptions/stripe.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@2 1 package subscriptions
paddy@2 2
paddy@2 3 import (
paddy@6 4 "errors"
paddy@2 5 "time"
paddy@2 6
paddy@2 7 "code.secondbit.org/uuid.hg"
paddy@2 8
paddy@2 9 "github.com/stripe/stripe-go"
paddy@2 10 "github.com/stripe/stripe-go/customer"
paddy@2 11 "github.com/stripe/stripe-go/sub"
paddy@2 12 )
paddy@2 13
paddy@6 14 const (
paddy@13 15 // PendingPlan holds the name for the plan users that haven't chosen a plan are subscribed to.
paddy@6 16 PendingPlan = "pending"
paddy@6 17 )
paddy@6 18
paddy@6 19 var (
paddy@13 20 // ErrNilCustomer is returned when a *Customer is expected, but nil is passed.
paddy@13 21 ErrNilCustomer = errors.New("nil customer passed")
paddy@13 22 // ErrNilCustomerSubs is returned when *Customer.Subs is expected to be a slice, but is nil.
paddy@13 23 ErrNilCustomerSubs = errors.New("customer with nil subscriptions list passed")
paddy@13 24 // ErrWrongNumberOfCustomerSubs is returned when a *Customer.Subs slice has more or fewer elements than expected.
paddy@6 25 ErrWrongNumberOfCustomerSubs = errors.New("customer with wrong number of subscriptions passed")
paddy@13 26 // ErrNilSubscription is returned when a *Subscription is expected, but nil is passed.
paddy@13 27 ErrNilSubscription = errors.New("nil subscription passed")
paddy@6 28 )
paddy@6 29
paddy@13 30 // Stripe is a wrapper around the clients for Stripe that we're using. It's responsible for
paddy@13 31 // all the interaction with the Stripe API that we're doing. It is a level higher than the
paddy@13 32 // raw Stripe clients, and can be thought of as Stripe-from-the-perspective-of-subscriptions.
paddy@2 33 type Stripe struct {
paddy@2 34 apiKey string
paddy@2 35 customers customer.Client
paddy@2 36 subscriptions sub.Client
paddy@2 37 }
paddy@2 38
paddy@13 39 // NewStripe creates a new Stripe instance using the passed parameters. The stripe.Backend
paddy@13 40 // parameter can be used to create a stub instead of something that actually hits the Stripe
paddy@13 41 // API. See the github.com/stripe/stripe-go documentation for more information about Backends.
paddy@2 42 func NewStripe(apiKey string, backend stripe.Backend) Stripe {
paddy@2 43 return Stripe{
paddy@2 44 apiKey: apiKey,
paddy@2 45 customers: customer.Client{
paddy@2 46 B: backend,
paddy@2 47 Key: apiKey,
paddy@2 48 },
paddy@2 49 subscriptions: sub.Client{
paddy@2 50 B: backend,
paddy@2 51 Key: apiKey,
paddy@2 52 },
paddy@2 53 }
paddy@2 54 }
paddy@2 55
paddy@13 56 // CreateStripeCustomer sets up a customer using the passed Stripe client, ensuring it follows all
paddy@13 57 // the conventions that subscriptions expects (the description being set properly, the UserID meta,
paddy@13 58 // etc.) Creating a customer automatically creates a subscription for that customer.
paddy@6 59 func CreateStripeCustomer(plan, email string, userID uuid.ID, s Stripe) (*stripe.Customer, error) {
paddy@2 60 customerParams := &stripe.CustomerParams{
paddy@2 61 Desc: "Customer for user " + userID.String(),
paddy@2 62 Email: email,
paddy@6 63 Plan: plan,
paddy@2 64 }
paddy@2 65 customerParams.AddMeta("UserID", userID.String())
paddy@2 66 c, err := s.customers.New(customerParams)
paddy@2 67 if err != nil {
paddy@2 68 return nil, err
paddy@2 69 }
paddy@2 70 return c, nil
paddy@2 71 }
paddy@2 72
paddy@13 73 // UpdateStripeSubscription modifies a subscription in Stripe, updating the plan and/or token.
paddy@13 74 // A nil plan or token indicates no change to the parameter.
paddy@6 75 func UpdateStripeSubscription(customerID string, plan, token *string, s Stripe) (*stripe.Sub, error) {
paddy@6 76 params := &stripe.SubParams{}
paddy@6 77 if plan != nil {
paddy@6 78 params.Plan = *plan
paddy@2 79 }
paddy@6 80 if token != nil {
paddy@6 81 params.Token = *token
paddy@6 82 }
paddy@6 83 subscription, err := s.subscriptions.Update(customerID, params)
paddy@2 84 if err != nil {
paddy@2 85 return nil, err
paddy@2 86 }
paddy@6 87 return subscription, nil
paddy@2 88 }
paddy@2 89
paddy@6 90 // New should be called when a user's profile is created. At this point, we know nothing about the subscription
paddy@6 91 // they actually _want_. We just sign them up for the dedicated "pending" plan. This is to make their free trial begin
paddy@6 92 // immediately and not have to worry about automatically locking them out until they actually create a subscription.
paddy@6 93 // Basically, we want everyone to have a subscription at all times, but some users will have placeholders until they
paddy@6 94 // actually update their subscription with a desired plan and payment method.
paddy@6 95 func New(req SubscriptionChange, s Stripe, store SubscriptionStore) (Subscription, error) {
paddy@6 96 subscription := Subscription{}
paddy@6 97 subscription.ApplyChange(req)
paddy@6 98 // BUG(paddy): need to validate the change
paddy@6 99
paddy@6 100 // create the customer in Stripe, storing the token for reuse
paddy@6 101 customer, err := CreateStripeCustomer(PendingPlan, *req.Email, req.UserID, s)
paddy@6 102 if err != nil {
paddy@6 103 return subscription, err
paddy@6 104 }
paddy@6 105 if customer == nil {
paddy@6 106 return subscription, ErrNilCustomer
paddy@6 107 }
paddy@6 108 if customer.Subs == nil {
paddy@6 109 return subscription, ErrNilCustomerSubs
paddy@6 110 }
paddy@6 111 if len(customer.Subs.Values) != 1 {
paddy@6 112 return subscription, ErrWrongNumberOfCustomerSubs
paddy@6 113 }
paddy@6 114 if customer.Subs.Values[0] == nil {
paddy@6 115 return subscription, ErrNilSubscription
paddy@6 116 }
paddy@6 117
paddy@6 118 change := StripeSubscriptionChange(subscription, *customer.Subs.Values[0])
paddy@6 119 subscription.ApplyChange(change)
paddy@6 120
paddy@6 121 err = store.CreateSubscription(subscription)
paddy@2 122 if err != nil {
paddy@3 123 return subscription, err
paddy@2 124 }
paddy@2 125
paddy@3 126 return subscription, nil
paddy@2 127 }
paddy@2 128
paddy@13 129 // StripeSubscriptionChange takes a Subscription and a stripe.Sub, and returns a SubscriptionChange
paddy@13 130 // that will change only the properties necessary to make the Subscription match the stripe.Sub.
paddy@2 131 func StripeSubscriptionChange(orig Subscription, subscription stripe.Sub) SubscriptionChange {
paddy@2 132 var change SubscriptionChange
paddy@6 133 if subscription.ID != orig.StripeSubscription {
paddy@6 134 change.StripeSubscription = &subscription.ID
paddy@6 135 }
paddy@2 136 if subscription.Plan != nil && orig.Plan != subscription.Plan.ID {
paddy@2 137 change.Plan = &subscription.Plan.ID
paddy@2 138 }
paddy@2 139 if string(subscription.Status) != orig.Status {
paddy@2 140 status := string(subscription.Status)
paddy@2 141 change.Status = &status
paddy@2 142 }
paddy@2 143 if subscription.EndCancel != orig.Canceling {
paddy@2 144 change.Canceling = &subscription.EndCancel
paddy@2 145 }
paddy@3 146 if !time.Unix(subscription.TrialStart, 0).Equal(orig.TrialStart) && !(subscription.TrialStart == 0 && orig.TrialStart.IsZero()) {
paddy@2 147 trialStart := time.Unix(subscription.TrialStart, 0)
paddy@2 148 change.TrialStart = &trialStart
paddy@2 149 }
paddy@3 150 if !time.Unix(subscription.TrialEnd, 0).Equal(orig.TrialEnd) && !(subscription.TrialEnd == 0 && orig.TrialEnd.IsZero()) {
paddy@2 151 trialEnd := time.Unix(subscription.TrialEnd, 0)
paddy@2 152 change.TrialEnd = &trialEnd
paddy@2 153 }
paddy@3 154 if !time.Unix(subscription.PeriodStart, 0).Equal(orig.PeriodStart) && !(subscription.PeriodStart == 0 && orig.PeriodStart.IsZero()) {
paddy@2 155 periodStart := time.Unix(subscription.PeriodStart, 0)
paddy@2 156 change.PeriodStart = &periodStart
paddy@2 157 }
paddy@3 158 if !time.Unix(subscription.PeriodEnd, 0).Equal(orig.PeriodEnd) && !(subscription.PeriodEnd == 0 && orig.PeriodEnd.IsZero()) {
paddy@2 159 periodEnd := time.Unix(subscription.PeriodEnd, 0)
paddy@2 160 change.PeriodEnd = &periodEnd
paddy@2 161 }
paddy@3 162 if !time.Unix(subscription.Canceled, 0).Equal(orig.CanceledAt) && !(subscription.Canceled == 0 && orig.CanceledAt.IsZero()) {
paddy@2 163 canceledAt := time.Unix(subscription.Canceled, 0)
paddy@2 164 change.CanceledAt = &canceledAt
paddy@2 165 }
paddy@2 166 return change
paddy@2 167 }