ducky/subscriptions

Paddy 2015-07-18 Parent:c4cfceb2f2fb Child:1ff031bebf9e

9:8eb19bcbf17d Go to Latest

ducky/subscriptions/stripe.go

Return errors from responses in client. When the client makes a request, non-200 responses _are not_ considered errors. So we need to check the response.Errors property, and if it has errors, _then_ we consider the request to have an error. To make this happen, we created an httpErrors type that fulfills the error interface and just wraps the response Errors property. Then callers can type-cast it and interrogate it.

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 = "pending"
16 )
18 var (
19 ErrNilCustomer = errors.New("nil customer passed")
20 ErrNilCustomerSubs = errors.New("customer with nil subscriptions list passed")
21 ErrWrongNumberOfCustomerSubs = errors.New("customer with wrong number of subscriptions passed")
22 ErrNilSubscription = errors.New("nil subscription passed")
23 )
25 type Stripe struct {
26 apiKey string
27 customers customer.Client
28 subscriptions sub.Client
29 }
31 func NewStripe(apiKey string, backend stripe.Backend) Stripe {
32 return Stripe{
33 apiKey: apiKey,
34 customers: customer.Client{
35 B: backend,
36 Key: apiKey,
37 },
38 subscriptions: sub.Client{
39 B: backend,
40 Key: apiKey,
41 },
42 }
43 }
45 func CreateStripeCustomer(plan, email string, userID uuid.ID, s Stripe) (*stripe.Customer, error) {
46 customerParams := &stripe.CustomerParams{
47 Desc: "Customer for user " + userID.String(),
48 Email: email,
49 Plan: plan,
50 }
51 customerParams.AddMeta("UserID", userID.String())
52 c, err := s.customers.New(customerParams)
53 if err != nil {
54 return nil, err
55 }
56 return c, nil
57 }
59 func UpdateStripeSubscription(customerID string, plan, token *string, s Stripe) (*stripe.Sub, error) {
60 params := &stripe.SubParams{}
61 if plan != nil {
62 params.Plan = *plan
63 }
64 if token != nil {
65 params.Token = *token
66 }
67 subscription, err := s.subscriptions.Update(customerID, params)
68 if err != nil {
69 return nil, err
70 }
71 return subscription, nil
72 }
74 // New should be called when a user's profile is created. At this point, we know nothing about the subscription
75 // they actually _want_. We just sign them up for the dedicated "pending" plan. This is to make their free trial begin
76 // immediately and not have to worry about automatically locking them out until they actually create a subscription.
77 // Basically, we want everyone to have a subscription at all times, but some users will have placeholders until they
78 // actually update their subscription with a desired plan and payment method.
79 func New(req SubscriptionChange, s Stripe, store SubscriptionStore) (Subscription, error) {
80 subscription := Subscription{}
81 subscription.ApplyChange(req)
82 // BUG(paddy): need to validate the change
84 // create the customer in Stripe, storing the token for reuse
85 customer, err := CreateStripeCustomer(PendingPlan, *req.Email, req.UserID, s)
86 if err != nil {
87 return subscription, err
88 }
89 if customer == nil {
90 return subscription, ErrNilCustomer
91 }
92 if customer.Subs == nil {
93 return subscription, ErrNilCustomerSubs
94 }
95 if len(customer.Subs.Values) != 1 {
96 return subscription, ErrWrongNumberOfCustomerSubs
97 }
98 if customer.Subs.Values[0] == nil {
99 return subscription, ErrNilSubscription
100 }
102 change := StripeSubscriptionChange(subscription, *customer.Subs.Values[0])
103 subscription.ApplyChange(change)
105 err = store.CreateSubscription(subscription)
106 if err != nil {
107 return subscription, err
108 }
110 return subscription, nil
111 }
113 func StripeSubscriptionChange(orig Subscription, subscription stripe.Sub) SubscriptionChange {
114 var change SubscriptionChange
115 if subscription.ID != orig.StripeSubscription {
116 change.StripeSubscription = &subscription.ID
117 }
118 if subscription.Plan != nil && orig.Plan != subscription.Plan.ID {
119 change.Plan = &subscription.Plan.ID
120 }
121 if string(subscription.Status) != orig.Status {
122 status := string(subscription.Status)
123 change.Status = &status
124 }
125 if subscription.EndCancel != orig.Canceling {
126 change.Canceling = &subscription.EndCancel
127 }
128 if !time.Unix(subscription.TrialStart, 0).Equal(orig.TrialStart) && !(subscription.TrialStart == 0 && orig.TrialStart.IsZero()) {
129 trialStart := time.Unix(subscription.TrialStart, 0)
130 change.TrialStart = &trialStart
131 }
132 if !time.Unix(subscription.TrialEnd, 0).Equal(orig.TrialEnd) && !(subscription.TrialEnd == 0 && orig.TrialEnd.IsZero()) {
133 trialEnd := time.Unix(subscription.TrialEnd, 0)
134 change.TrialEnd = &trialEnd
135 }
136 if !time.Unix(subscription.PeriodStart, 0).Equal(orig.PeriodStart) && !(subscription.PeriodStart == 0 && orig.PeriodStart.IsZero()) {
137 periodStart := time.Unix(subscription.PeriodStart, 0)
138 change.PeriodStart = &periodStart
139 }
140 if !time.Unix(subscription.PeriodEnd, 0).Equal(orig.PeriodEnd) && !(subscription.PeriodEnd == 0 && orig.PeriodEnd.IsZero()) {
141 periodEnd := time.Unix(subscription.PeriodEnd, 0)
142 change.PeriodEnd = &periodEnd
143 }
144 if !time.Unix(subscription.Canceled, 0).Equal(orig.CanceledAt) && !(subscription.Canceled == 0 && orig.CanceledAt.IsZero()) {
145 canceledAt := time.Unix(subscription.Canceled, 0)
146 change.CanceledAt = &canceledAt
147 }
148 return change
149 }