ducky/subscriptions

Paddy 2015-10-04 Parent:1ff031bebf9e

16:b063bc0a6e84 Go to Latest

ducky/subscriptions/stripe.go

Make api subpackage golint-passing. Add comments to all the exported functions, methods, and variables in the api subpackage, to make golint happy. Also, make the individual endpoints in the api subpackage unexported, as there's no real use case for exporting them. The handlers depend on the placeholders in the endpoint, so we need them to be controlled in unison, which means it's probably a bad idea to declare the route outside of the API package. And the only reason to expose the Handler is so people can declare custom endpoints.

History
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 }