ducky/subscriptions
2015-10-04
Parent:1ff031bebf9e
ducky/subscriptions/stripe.go
Document our client to make golint happy. Take care of all the documentation warnings in the client subpackage, which means golint now returns successfully.
| 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 } |