ducky/subscriptions

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

7:9e138933e4ce Go to Latest

ducky/subscriptions/stripe.go

Create a client for working with subscriptions. We mostly copied our code.secondbit.org/auth.hg/client package to create a simple client library for communicating with our Subscriptions API. Right now, the client only has support for creating a subscription. It remains untested, but it builds.

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 }