ducky/subscriptions
2015-09-27
Parent:c4cfceb2f2fb
ducky/subscriptions/stripe.go
Add golint comments. Comment on some more of our exported types, functions, and variables, both to make golint happy and because uncommented code never ever ends well.
1 package subscriptions
3 import (
4 "errors"
5 "time"
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"
12 )
14 const (
15 // PendingPlan holds the name for the plan users that haven't chosen a plan are subscribed to.
16 PendingPlan = "pending"
17 )
19 var (
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")
28 )
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.
33 type Stripe struct {
34 apiKey string
35 customers customer.Client
36 subscriptions sub.Client
37 }
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 {
43 return Stripe{
44 apiKey: apiKey,
45 customers: customer.Client{
46 B: backend,
47 Key: apiKey,
48 },
49 subscriptions: sub.Client{
50 B: backend,
51 Key: apiKey,
52 },
53 }
54 }
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(),
62 Email: email,
63 Plan: plan,
64 }
65 customerParams.AddMeta("UserID", userID.String())
66 c, err := s.customers.New(customerParams)
67 if err != nil {
68 return nil, err
69 }
70 return c, nil
71 }
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{}
77 if plan != nil {
78 params.Plan = *plan
79 }
80 if token != nil {
81 params.Token = *token
82 }
83 subscription, err := s.subscriptions.Update(customerID, params)
84 if err != nil {
85 return nil, err
86 }
87 return subscription, nil
88 }
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)
102 if err != nil {
103 return subscription, err
104 }
105 if customer == nil {
106 return subscription, ErrNilCustomer
107 }
108 if customer.Subs == nil {
109 return subscription, ErrNilCustomerSubs
110 }
111 if len(customer.Subs.Values) != 1 {
112 return subscription, ErrWrongNumberOfCustomerSubs
113 }
114 if customer.Subs.Values[0] == nil {
115 return subscription, ErrNilSubscription
116 }
118 change := StripeSubscriptionChange(subscription, *customer.Subs.Values[0])
119 subscription.ApplyChange(change)
121 err = store.CreateSubscription(subscription)
122 if err != nil {
123 return subscription, err
124 }
126 return subscription, nil
127 }
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
135 }
136 if subscription.Plan != nil && orig.Plan != subscription.Plan.ID {
137 change.Plan = &subscription.Plan.ID
138 }
139 if string(subscription.Status) != orig.Status {
140 status := string(subscription.Status)
141 change.Status = &status
142 }
143 if subscription.EndCancel != orig.Canceling {
144 change.Canceling = &subscription.EndCancel
145 }
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
149 }
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
153 }
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
157 }
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
161 }
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
165 }
166 return change
167 }