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