Make it possible to create a user without payment details.
We want a new subscription flow, in which the system (not the user) is
responsible for creating a subscription when a new profile is created. This is
to prevent issues where a user has an account, but no subscription. This is bad
because the free trial starts ticking from the day the subscription is created,
so we should try to make the subscription and account creation get created as
close to each other as possible.
Our plan is to instead have the authd service fire off an NSQ event when an
auth.Profile is created, which a subscription listener will be listening for.
When that happens, the listener will use the subscription API to create a
subscription. Then the user will update the subscription with their payment info
and the plan they want to use.
To accomplish this, we changed the way things were handled. The
SubscriptionRequest type, along with its Validate method, were removed. Instead,
we get the SubscriptionChange type which handles both the creation of a
subscription and the updating of a subscription.
We also added an endpoint for patching subscriptions, useful for adding the
StripeSubscription or updating the plan. By default, every subscription is
created with a "Pending" plan which has a 31 day free trial. This is so we can
detect users that haven't actually set up their subscription yet, but their free
trial is still timed correctly.
We changed the way we handle scopes, creating actual auth.Scope instances
instead of just declaring an ID for them. This is useful when we have a client,
for example.
With this change, we lose all the validation we had on creating a Subscription,
and we need to rewrite that validation logic. This is because we no longer have
a specific type for "creating a subscription", so we can't just call a validate
method. We should have a helper method validateCreateRequest(change
SubscriptionChange) that will return the API errors we want, so it's easier to
unit test.
We should really be restricting the CreateSubscriptionHandler to
ScopeSubscriptionAdmin, anyways, since Subscriptions should only ever be created
by the system tools or administrators.
We created a PatchSubscriptionHandler that exposes an interface to updating
properties of a Subscription. It allows users to update their own Subscriptions
or requires the ScopeSubscriptionAdmin scope before allowing you to update
another user's Subscription. It, likewise, needs validation still. We also added
the concept of "system-controlled properties" of the SubscriptionChange type,
which only admins or the system tools can update.
We updated our planOptions to distinguish between plans that do and do not need
administrative credentials to be chosen. Our free and pending plans are
available to administrators only.
We updated our StripeChange object to be better organised (separating out the
system and user-controlled properties), and we added a StripeSource and Email
property, so the Stripe part can be better managed, and all our requests can be
made using just this type. This required updating our SubscriptionChange.IsEmpty
helper, which has been updated (along with its tests) and it passes all tests.
To replace our SubscriptionRequest.Validate helper, we created a
ChangingSystemProperties helper (which returns the system-controlled properties
being changed as a slice of JSON pointers, fit for use in error messages) and an
IsAcceptablePlan helper, which returns true if the plan exists and the user has
the authority to select it.
We also updated our stripe helpers to remove the CreateStripeSubscription (we
create one when we create the customer) and create an UpdateStripeSubscription
instead. It does what you'd think it does. We also added some comments to New,
so it at least has some notes about how it's meant to be used and why. Now it
just creates the customer in stripe, then creates a Subscription based on that
customer. We also updated our StripeSubscriptionChange helper to detect when the
StripeSubscription property changed.
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"
15 PendingPlan = "pending"
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")
27 customers customer.Client
28 subscriptions sub.Client
31 func NewStripe(apiKey string, backend stripe.Backend) Stripe {
34 customers: customer.Client{
38 subscriptions: sub.Client{
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(),
51 customerParams.AddMeta("UserID", userID.String())
52 c, err := s.customers.New(customerParams)
59 func UpdateStripeSubscription(customerID string, plan, token *string, s Stripe) (*stripe.Sub, error) {
60 params := &stripe.SubParams{}
67 subscription, err := s.subscriptions.Update(customerID, params)
71 return subscription, nil
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)
87 return subscription, err
90 return subscription, ErrNilCustomer
92 if customer.Subs == nil {
93 return subscription, ErrNilCustomerSubs
95 if len(customer.Subs.Values) != 1 {
96 return subscription, ErrWrongNumberOfCustomerSubs
98 if customer.Subs.Values[0] == nil {
99 return subscription, ErrNilSubscription
102 change := StripeSubscriptionChange(subscription, *customer.Subs.Values[0])
103 subscription.ApplyChange(change)
105 err = store.CreateSubscription(subscription)
107 return subscription, err
110 return subscription, nil
113 func StripeSubscriptionChange(orig Subscription, subscription stripe.Sub) SubscriptionChange {
114 var change SubscriptionChange
115 if subscription.ID != orig.StripeSubscription {
116 change.StripeSubscription = &subscription.ID
118 if subscription.Plan != nil && orig.Plan != subscription.Plan.ID {
119 change.Plan = &subscription.Plan.ID
121 if string(subscription.Status) != orig.Status {
122 status := string(subscription.Status)
123 change.Status = &status
125 if subscription.EndCancel != orig.Canceling {
126 change.Canceling = &subscription.EndCancel
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
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
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
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
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