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