ducky/subscriptions

Paddy 2015-06-30 Parent:b240b6123548 Child:c4cfceb2f2fb

5:fe8f092cc149 Go to Latest

ducky/subscriptions/stripe.go

Make subscriptions Kubernetes-ready. Update our .hgignore file to include the docker-ready subscripionsd binary (which differs from the local subscriptionsd binary only in that it's statically-compiled for linux). Add a replication controller for subscriptionsd that will spin up the appropriate pods for us and make sure our Stripe and Subscriptionsd secret volumes are attached, so the pods may configure themselves with that private info. Create a Stripe secret volume with a placeholder for the stripe secret key. Create a Subscriptionsd secret volume with the DSN sent to the base64 encoding of an empty string right now (which means subscriptionsd will store data in memory, but whatever.) Create a subscriptionsd service that will route traffic to the subscriptionsd pods created by the replication controller. Create a minimal Dockerfile to get the subscriptionsd binary running on kubernetes. Add a build-docker helper script that will compile a Docker-ready subscriptionsd binary (by compiling it in a Docker container). Copy a Ubuntu ca-certificates.crt file that we'll inject into our Dockerfile so the Stripe SSL can be resolved. Busybox doesn't have any certificates, by default. Create a wrapper script that will be run in the Docker container to read the secrets from our secret volumes and inject them as environment variables, which is what subscriptionsd understands.

History
paddy@2 1 package subscriptions
paddy@2 2
paddy@2 3 import (
paddy@3 4 "log"
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@2 14 type Stripe struct {
paddy@2 15 apiKey string
paddy@2 16 customers customer.Client
paddy@2 17 subscriptions sub.Client
paddy@2 18 }
paddy@2 19
paddy@2 20 func NewStripe(apiKey string, backend stripe.Backend) Stripe {
paddy@2 21 return Stripe{
paddy@2 22 apiKey: apiKey,
paddy@2 23 customers: customer.Client{
paddy@2 24 B: backend,
paddy@2 25 Key: apiKey,
paddy@2 26 },
paddy@2 27 subscriptions: sub.Client{
paddy@2 28 B: backend,
paddy@2 29 Key: apiKey,
paddy@2 30 },
paddy@2 31 }
paddy@2 32 }
paddy@2 33
paddy@2 34 func CreateStripeCustomer(token, email string, userID uuid.ID, s Stripe) (*stripe.Customer, error) {
paddy@2 35 customerParams := &stripe.CustomerParams{
paddy@2 36 Desc: "Customer for user " + userID.String(),
paddy@2 37 Email: email,
paddy@2 38 }
paddy@2 39 customerParams.AddMeta("UserID", userID.String())
paddy@2 40 customerParams.SetSource(token)
paddy@2 41 c, err := s.customers.New(customerParams)
paddy@2 42 if err != nil {
paddy@2 43 return nil, err
paddy@2 44 }
paddy@2 45 return c, nil
paddy@2 46 }
paddy@2 47
paddy@2 48 func CreateStripeSubscription(customer, plan string, userID uuid.ID, s Stripe) (*stripe.Sub, error) {
paddy@2 49 subParams := &stripe.SubParams{
paddy@2 50 Plan: plan,
paddy@2 51 Customer: customer,
paddy@2 52 }
paddy@2 53 subParams.AddMeta("UserID", userID.String())
paddy@2 54
paddy@2 55 resp, err := s.subscriptions.New(subParams)
paddy@2 56 if err != nil {
paddy@2 57 return nil, err
paddy@2 58 }
paddy@2 59 return resp, nil
paddy@2 60 }
paddy@2 61
paddy@3 62 func New(req SubscriptionRequest, s Stripe, store SubscriptionStore) (Subscription, error) {
paddy@3 63 subscription := SubscriptionFromRequest(req)
paddy@2 64 // create the subscription in our datastore
paddy@2 65 // this will fail if they already have a subscription, which prevents duplicate/orphaned Stripe customers being created
paddy@3 66 err := store.CreateSubscription(subscription)
paddy@2 67 if err != nil {
paddy@3 68 return subscription, err
paddy@2 69 }
paddy@2 70
paddy@2 71 // create the customer in Stripe, storing the token for reuse
paddy@3 72 customer, err := CreateStripeCustomer(req.StripeToken, req.Email, req.UserID, s)
paddy@2 73 if err != nil {
paddy@2 74 // TODO: delete subscription object
paddy@3 75 return subscription, err
paddy@2 76 }
paddy@2 77
paddy@2 78 // create the subscription in Stripe, storing the ID for tracking and associating purposes
paddy@2 79 stripeSub, err := CreateStripeSubscription(customer.ID, subscription.Plan, subscription.UserID, s)
paddy@2 80 if err != nil {
paddy@2 81 // TODO: delete customer
paddy@2 82 // TODO: delete subscription object
paddy@3 83 return subscription, err
paddy@2 84 }
paddy@2 85
paddy@2 86 // update our subscription in the datastore with the latest information from Stripe
paddy@2 87 change := StripeSubscriptionChange(subscription, *stripeSub)
paddy@3 88 err = store.UpdateSubscription(subscription.UserID, change)
paddy@2 89 if err != nil {
paddy@3 90 log.Printf("Error pairing Stripe subscription %s to user %s: %+v\nUser needs to have their subscription updated manually.", change.StripeSubscription, req.UserID, err)
paddy@3 91 return subscription, nil
paddy@2 92 }
paddy@3 93 subscription.ApplyChange(change)
paddy@3 94 return subscription, nil
paddy@2 95 }
paddy@2 96
paddy@2 97 func StripeSubscriptionChange(orig Subscription, subscription stripe.Sub) SubscriptionChange {
paddy@2 98 var change SubscriptionChange
paddy@2 99 if subscription.Plan != nil && orig.Plan != subscription.Plan.ID {
paddy@2 100 change.Plan = &subscription.Plan.ID
paddy@2 101 }
paddy@2 102 if string(subscription.Status) != orig.Status {
paddy@2 103 status := string(subscription.Status)
paddy@2 104 change.Status = &status
paddy@2 105 }
paddy@2 106 if subscription.EndCancel != orig.Canceling {
paddy@2 107 change.Canceling = &subscription.EndCancel
paddy@2 108 }
paddy@3 109 if !time.Unix(subscription.TrialStart, 0).Equal(orig.TrialStart) && !(subscription.TrialStart == 0 && orig.TrialStart.IsZero()) {
paddy@2 110 trialStart := time.Unix(subscription.TrialStart, 0)
paddy@2 111 change.TrialStart = &trialStart
paddy@2 112 }
paddy@3 113 if !time.Unix(subscription.TrialEnd, 0).Equal(orig.TrialEnd) && !(subscription.TrialEnd == 0 && orig.TrialEnd.IsZero()) {
paddy@2 114 trialEnd := time.Unix(subscription.TrialEnd, 0)
paddy@2 115 change.TrialEnd = &trialEnd
paddy@2 116 }
paddy@3 117 if !time.Unix(subscription.PeriodStart, 0).Equal(orig.PeriodStart) && !(subscription.PeriodStart == 0 && orig.PeriodStart.IsZero()) {
paddy@2 118 periodStart := time.Unix(subscription.PeriodStart, 0)
paddy@2 119 change.PeriodStart = &periodStart
paddy@2 120 }
paddy@3 121 if !time.Unix(subscription.PeriodEnd, 0).Equal(orig.PeriodEnd) && !(subscription.PeriodEnd == 0 && orig.PeriodEnd.IsZero()) {
paddy@2 122 periodEnd := time.Unix(subscription.PeriodEnd, 0)
paddy@2 123 change.PeriodEnd = &periodEnd
paddy@2 124 }
paddy@3 125 if !time.Unix(subscription.Canceled, 0).Equal(orig.CanceledAt) && !(subscription.Canceled == 0 && orig.CanceledAt.IsZero()) {
paddy@2 126 canceledAt := time.Unix(subscription.Canceled, 0)
paddy@2 127 change.CanceledAt = &canceledAt
paddy@2 128 }
paddy@2 129 return change
paddy@2 130 }