ducky/subscriptions
ducky/subscriptions/stripe.go
Export all subscriptionStore methods. We're not going to wrap all our subscriptionStore interactions in a Context type, so we need to expose all the functions so other packages can call them. Also, it's now SubscriptionStore. Also creates a SubscriptionRequest type that is used to create a new Subscription instance, along with a Validate method on it, to detect errors when creating a SubscriptionRequest. Update our CreateSubscription function to be New, and have it take a SubscriptionRequest, a Stripe instance, and a SubscriptionStore as arguments. It'll create the Subscription in the SubscriptionStore, create a customer in Stripe, and create the subscription in Stripe, then associate the Stripe subscription with the created Subscription. Also, fix our StripeSubscriptionChange function to correctly equate a missing/zero Unix timestamp from Stripe with a missing/zero time.Time.
| 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 } |