ducky/subscriptions
ducky/subscriptions/stripe.go
Create a listener that will create subscriptions. We need a listener (as discussed in c4cfceb2f2fb) that will create a Subscription whenever an auth.Profile is created. This is the beginning of that effort. It hasn't been tested, and all the pieces aren't in place, but it's a rough skeleton. We have a Dockerfile that will correctly build a minimal container for the listener. We have a build-docker.sh script that will correctly build a binary that will be used in the Dockerfile. We have a ca-certificates.crt, which are pulled from Ubuntu, and are necessary before we can safely consume SSL endpoints. We created a small consumer script that listens for events off NSQ, and calls the appropriate endpoint for our Subscriptions API. This is untested, and it doesn't build at the moment, but that's awaiting changes in the code.secondbit.org/auth.hg package. Finally, we have a wrapper.sh file that will expose the Stripe secret key being used from a Kubernetes secret file as an environment variable, instead.
| 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@6 | 15 PendingPlan = "pending" |
| paddy@6 | 16 ) |
| paddy@6 | 17 |
| paddy@6 | 18 var ( |
| paddy@6 | 19 ErrNilCustomer = errors.New("nil customer passed") |
| paddy@6 | 20 ErrNilCustomerSubs = errors.New("customer with nil subscriptions list passed") |
| paddy@6 | 21 ErrWrongNumberOfCustomerSubs = errors.New("customer with wrong number of subscriptions passed") |
| paddy@6 | 22 ErrNilSubscription = errors.New("nil subscription passed") |
| paddy@6 | 23 ) |
| paddy@6 | 24 |
| paddy@2 | 25 type Stripe struct { |
| paddy@2 | 26 apiKey string |
| paddy@2 | 27 customers customer.Client |
| paddy@2 | 28 subscriptions sub.Client |
| paddy@2 | 29 } |
| paddy@2 | 30 |
| paddy@2 | 31 func NewStripe(apiKey string, backend stripe.Backend) Stripe { |
| paddy@2 | 32 return Stripe{ |
| paddy@2 | 33 apiKey: apiKey, |
| paddy@2 | 34 customers: customer.Client{ |
| paddy@2 | 35 B: backend, |
| paddy@2 | 36 Key: apiKey, |
| paddy@2 | 37 }, |
| paddy@2 | 38 subscriptions: sub.Client{ |
| paddy@2 | 39 B: backend, |
| paddy@2 | 40 Key: apiKey, |
| paddy@2 | 41 }, |
| paddy@2 | 42 } |
| paddy@2 | 43 } |
| paddy@2 | 44 |
| paddy@6 | 45 func CreateStripeCustomer(plan, email string, userID uuid.ID, s Stripe) (*stripe.Customer, error) { |
| paddy@2 | 46 customerParams := &stripe.CustomerParams{ |
| paddy@2 | 47 Desc: "Customer for user " + userID.String(), |
| paddy@2 | 48 Email: email, |
| paddy@6 | 49 Plan: plan, |
| paddy@2 | 50 } |
| paddy@2 | 51 customerParams.AddMeta("UserID", userID.String()) |
| paddy@2 | 52 c, err := s.customers.New(customerParams) |
| paddy@2 | 53 if err != nil { |
| paddy@2 | 54 return nil, err |
| paddy@2 | 55 } |
| paddy@2 | 56 return c, nil |
| paddy@2 | 57 } |
| paddy@2 | 58 |
| paddy@6 | 59 func UpdateStripeSubscription(customerID string, plan, token *string, s Stripe) (*stripe.Sub, error) { |
| paddy@6 | 60 params := &stripe.SubParams{} |
| paddy@6 | 61 if plan != nil { |
| paddy@6 | 62 params.Plan = *plan |
| paddy@2 | 63 } |
| paddy@6 | 64 if token != nil { |
| paddy@6 | 65 params.Token = *token |
| paddy@6 | 66 } |
| paddy@6 | 67 subscription, err := s.subscriptions.Update(customerID, params) |
| paddy@2 | 68 if err != nil { |
| paddy@2 | 69 return nil, err |
| paddy@2 | 70 } |
| paddy@6 | 71 return subscription, nil |
| paddy@2 | 72 } |
| paddy@2 | 73 |
| paddy@6 | 74 // New should be called when a user's profile is created. At this point, we know nothing about the subscription |
| paddy@6 | 75 // they actually _want_. We just sign them up for the dedicated "pending" plan. This is to make their free trial begin |
| paddy@6 | 76 // immediately and not have to worry about automatically locking them out until they actually create a subscription. |
| paddy@6 | 77 // Basically, we want everyone to have a subscription at all times, but some users will have placeholders until they |
| paddy@6 | 78 // actually update their subscription with a desired plan and payment method. |
| paddy@6 | 79 func New(req SubscriptionChange, s Stripe, store SubscriptionStore) (Subscription, error) { |
| paddy@6 | 80 subscription := Subscription{} |
| paddy@6 | 81 subscription.ApplyChange(req) |
| paddy@6 | 82 // BUG(paddy): need to validate the change |
| paddy@6 | 83 |
| paddy@6 | 84 // create the customer in Stripe, storing the token for reuse |
| paddy@6 | 85 customer, err := CreateStripeCustomer(PendingPlan, *req.Email, req.UserID, s) |
| paddy@6 | 86 if err != nil { |
| paddy@6 | 87 return subscription, err |
| paddy@6 | 88 } |
| paddy@6 | 89 if customer == nil { |
| paddy@6 | 90 return subscription, ErrNilCustomer |
| paddy@6 | 91 } |
| paddy@6 | 92 if customer.Subs == nil { |
| paddy@6 | 93 return subscription, ErrNilCustomerSubs |
| paddy@6 | 94 } |
| paddy@6 | 95 if len(customer.Subs.Values) != 1 { |
| paddy@6 | 96 return subscription, ErrWrongNumberOfCustomerSubs |
| paddy@6 | 97 } |
| paddy@6 | 98 if customer.Subs.Values[0] == nil { |
| paddy@6 | 99 return subscription, ErrNilSubscription |
| paddy@6 | 100 } |
| paddy@6 | 101 |
| paddy@6 | 102 change := StripeSubscriptionChange(subscription, *customer.Subs.Values[0]) |
| paddy@6 | 103 subscription.ApplyChange(change) |
| paddy@6 | 104 |
| paddy@6 | 105 err = store.CreateSubscription(subscription) |
| paddy@2 | 106 if err != nil { |
| paddy@3 | 107 return subscription, err |
| paddy@2 | 108 } |
| paddy@2 | 109 |
| paddy@3 | 110 return subscription, nil |
| paddy@2 | 111 } |
| paddy@2 | 112 |
| paddy@2 | 113 func StripeSubscriptionChange(orig Subscription, subscription stripe.Sub) SubscriptionChange { |
| paddy@2 | 114 var change SubscriptionChange |
| paddy@6 | 115 if subscription.ID != orig.StripeSubscription { |
| paddy@6 | 116 change.StripeSubscription = &subscription.ID |
| paddy@6 | 117 } |
| paddy@2 | 118 if subscription.Plan != nil && orig.Plan != subscription.Plan.ID { |
| paddy@2 | 119 change.Plan = &subscription.Plan.ID |
| paddy@2 | 120 } |
| paddy@2 | 121 if string(subscription.Status) != orig.Status { |
| paddy@2 | 122 status := string(subscription.Status) |
| paddy@2 | 123 change.Status = &status |
| paddy@2 | 124 } |
| paddy@2 | 125 if subscription.EndCancel != orig.Canceling { |
| paddy@2 | 126 change.Canceling = &subscription.EndCancel |
| paddy@2 | 127 } |
| paddy@3 | 128 if !time.Unix(subscription.TrialStart, 0).Equal(orig.TrialStart) && !(subscription.TrialStart == 0 && orig.TrialStart.IsZero()) { |
| paddy@2 | 129 trialStart := time.Unix(subscription.TrialStart, 0) |
| paddy@2 | 130 change.TrialStart = &trialStart |
| paddy@2 | 131 } |
| paddy@3 | 132 if !time.Unix(subscription.TrialEnd, 0).Equal(orig.TrialEnd) && !(subscription.TrialEnd == 0 && orig.TrialEnd.IsZero()) { |
| paddy@2 | 133 trialEnd := time.Unix(subscription.TrialEnd, 0) |
| paddy@2 | 134 change.TrialEnd = &trialEnd |
| paddy@2 | 135 } |
| paddy@3 | 136 if !time.Unix(subscription.PeriodStart, 0).Equal(orig.PeriodStart) && !(subscription.PeriodStart == 0 && orig.PeriodStart.IsZero()) { |
| paddy@2 | 137 periodStart := time.Unix(subscription.PeriodStart, 0) |
| paddy@2 | 138 change.PeriodStart = &periodStart |
| paddy@2 | 139 } |
| paddy@3 | 140 if !time.Unix(subscription.PeriodEnd, 0).Equal(orig.PeriodEnd) && !(subscription.PeriodEnd == 0 && orig.PeriodEnd.IsZero()) { |
| paddy@2 | 141 periodEnd := time.Unix(subscription.PeriodEnd, 0) |
| paddy@2 | 142 change.PeriodEnd = &periodEnd |
| paddy@2 | 143 } |
| paddy@3 | 144 if !time.Unix(subscription.Canceled, 0).Equal(orig.CanceledAt) && !(subscription.Canceled == 0 && orig.CanceledAt.IsZero()) { |
| paddy@2 | 145 canceledAt := time.Unix(subscription.Canceled, 0) |
| paddy@2 | 146 change.CanceledAt = &canceledAt |
| paddy@2 | 147 } |
| paddy@2 | 148 return change |
| paddy@2 | 149 } |