You are on page 1of 34

Build a SaaS app in Go

Dominic St-Pierre

2018/09/02
Contents

1 Billing and subscriptions 2


1.1 Stripe, the friendly online payment API for devs . . . . . . . . . 2
1.2 Is this user a paid customer? . . . . . . . . . . . . . . . . . . . . 3
1.3 Per-account and per-user billing . . . . . . . . . . . . . . . . . . . 7
1.4 How to simultaneously handle multiple versions of pricing . . . . 13
1.5 From trial to a paid customer, with optional coupon code . . . . 17
1.6 Upgrading and downgrading from plan to plan . . . . . . . . . . 21
1.7 Add/update/remove credit cards . . . . . . . . . . . . . . . . . . 24
1.8 Previous invoices and upcoming invoice preview . . . . . . . . . . 28
1.9 Handling key Stripe webhook actions . . . . . . . . . . . . . . . . 30
1.10 Smooth cancellation . . . . . . . . . . . . . . . . . . . . . . . . . 32

1
Chapter 1

Billing and subscriptions

1.1 Stripe, the friendly online payment API for


devs
There’s nothing more rewarding than getting money from someone on the inter-
net you don’t know in exchange for something you’ve built.
We will add an entirely functional subscriptions module to our SaaS app, it will
handle the following aspects of billing:
1. Is this user a paid customer?
2. Per-account and per-user billing.
3. How to simultaneously handle multiple versions of pricing.
4. From trial to a paid customer, with optional coupon code.
5. Upgrading and downgrading from plan to plan.
6. Add/update/remove credit cards.
7. Previous invoices and upcoming invoice preview.
8. Handling key Stripe webhook events.
9. Smooth cancellation.
We tend to neglect the billing part of our SaaS when we build the MVP. Since
billing is not a core functionality, we tend to focus on solving the problem.
That being said, billing is still a crucial aspect when it becomes time for a trial
user to commit to your product. A solid billing experience can play a significant
role in their decision to enter the credit card number or not.
Having the flexibility to charge either per-account or per-user by default is
another benefit. In the last two SaaS apps I’ve built, we started charging per-
account on day one. After two-three months it made more sense to move to a
per-user pricing model. That’s a nightmare to implement at that point. The
primary reason is that you usually have 100 things with higher priority than

2
this. From a non-technical point of view, this change seems trivial. It’s not.
This change is hard to do after the fact. We can say the same for handling
multiple pricing sets. It is not something you should be thinking about too
much when building your initial version. But after six months when you double
your price (because you should always charge more!), you’ll be grateful to have
already this baked in and not worry about how to handle existing users on the
old plans vs. having new ones on the higher pricing plans.

1.2 Is this user a paid customer?


I think this is the right way to start. Having a route that returns if the current
user is a paid customer or not. If it’s a paid customer, it will return the data
needed for an intro billing page on the UI.
We need a struct that will hold this data, we’ll name it BillingOverview.
File: controllers/billing.go
1 // BillingOverview represents if an account is a paid
customer or not
2 type BillingOverview struct {
3 StripeID string `json :" stripeId "`
4 Plan string `json :" plan"`
5 IsYearly bool `json :" isYearly "`
6 IsNew bool `json :" isNew "`
7 Cards [] BillingCardData `json :" cards "`
8 CostForNewUser int
`json :" costForNewUser "`
9 CurrentPlan *dal. BillingPlan `json :" currentPlan "`
10 Seats int `json :" seats "`
11 Logins [] dal.Login `json :" logins "`
12 }
13
14 // BillingCardData represents a Stripe credit card
15 type BillingCardData struct {
16 ID string `json :"id"`
17 Name string `json :" name"`
18 Number string `json :" number "`
19 Month string `json :" month "`
20 Year string `json :" year"`
21 CVC string `json :" cvc"`
22 Brand string `json :" brand "`
23 Expiration string `json :" expiration "`
24 }

3
Our BillingOverview is self-explanatory. Let’s talk a bit about Seats and
Logins.
They are useful for per-user billing and we will discuss on the next sub-chapter.
Just like our controllers so far, we will attach all our functions to a pointer
receiver of type billing. The following is the start of our billing.go file.
1 package controllers
2
3 import (
4 "os",
5 " github .com/ stripe /stripe -go"
6 )
7
8 func init () {
9 stripe .Key = os. Getenv (" STRIPE_KEY ")
10 }
11
12 // Billing handles everything related to the billing
requests
13 type Billing struct {}
14
15
16 func newBilling () * engine . Route {
17 var b interface {} = User {}
18 return & engine .Route{
19 Logger : true ,
20 MinimumRole : model .RoleAdmin ,
21 Handler : b.( http. Handler ),
22 }
23 }
24
25 func (b Billing ) ServeHTTP (w http. ResponseWriter , r
*http. Request ) {
26 var head string
27 head , r.URL.Path = engine . ShiftPath (r.URL.Path)
28 if r. Method == http. MethodGet {
29 if head == " overview " {
30 b. overview (w, r)
31 return
32 }
33 }
34 }
35
36 func (b Billing ) overview (w http. ResponseWriter , r
*http. Request ) {

4
37 // check if this account is a paid or not
38 }

We import the os package to initialize our Stripe Key in the init function.
This is an easy way to have a test Stripe Key in development and a live Stripe
key in production. We will see in chatper 12: Automated deployment how
you can configure your app for either dev/staging/production environments.
Our first task is to determine if this account is a paid customer or not. Since
our Account model contains a StripeID field, we can safely assume that when
a value is in there, they are a paid customer.
We created the structs for the Account, User and Login models in chapter 6:
Authorization middleware.
1 func (b Billing ) overview (w http. ResponseWriter , r
*http. Request ) {
2 ctx := r. Context ()
3 keys := ctx.Value ( engine . ContextAuth ).( engine .Auth)
4 db := ctx.Value( engine . ContextDatabase ).(* data.DB)
5
6 // this struct will be returned should we be a paid
customer or not
7 ov := BillingOverview {}
8
9 // Get the current account
10 account , err := db. Accounts .Get(keys. AccountID )
11 if err != nil {
12 engine . Respond (w, r, http. StatusNotFound , err)
13 return
14 }
15
16 // TODO: getting logins and calculating paying seats
17
18 if len( account . StripeID ) == 0 {
19 ov.IsNew = true
20
21 // if they are on trial , we set the current plan
to
22 // that so the UI can based permissions on that
plan.
23 if account . TrialInfo . IsTrial {
24 if p, ok :=
data. GetPlan ( account . TrialInfo .Plan); ok {
25 ov. CurrentPlan = &p
26 }
27 }
28

5
29 engine . Respond (w, r, http.StatusOK , ov)
30 return
31 }
32
33 // paid customer code next ...
34 }

We’re getting the account that matches our authorized credentials, see chapter
6: Authorization middleware for detail. If there’s not a StripeID value yet in
the database then they are not a paid customer.
The UI will need to call the ChangePlan function for a previous customer.
In this case, we indicate this is a new account and we make sure to return the
proper plan if they are currently in an active trial.
The following code is executed when the StripeID field contains a Stripe cus-
tomer ID.
We will need a new Stripe package for the following code:
1 import (
2 ...
3 " github .com/ stripe /stripe -go/card"
4 " github .com/ stripe /stripe -go/ customer "
5 ...
6 )

1 // getting stripe customer


2 cus , err := customer .Get( account .StripeID , nil)
3 if err != nil {
4 engine . Respond (w, r, http. StatusBadRequest , err)
5 return
6 }
7
8 ov. StripeID = cus.ID
9 ov.Plan = account .Plan
10 ov. IsYearly = account . IsYearly
11
12 if p, ok := data. GetPlan ( account .Plan); ok {
13 ov. CurrentPlan = &p
14 }
15
16 cards := card.List (& stripe . CardListParams { Customer :
account . StripeID })
17 for cards .Next () {
18 c := cards.Card ()
19 if !c. Deleted {
20 ov.Cards = append (ov.Cards , BillingCardData {

6
21 ID: c.ID ,
22 Name: c.Name ,
23 Number : c.LastFour ,
24 Month: fmt. Sprintf ("%d", c. Month ),
25 Year: fmt. Sprintf ("%d", c.Year),
26 Expiration : fmt. Sprintf ("%d / %d", c.Month ,
c.Year),
27 Brand: string (c. Brand ),
28 })
29 }
30 }
31
32 engine . Respond (w, r, http.StatusOK , ov)

After successfully retrieving the Stripe customer we make sure we get their plan.
We will see a little later how we handle multiple pricing sets.
We then retrieve the active credit cards for this Stripe customer and return the
struct to the caller.

1.3 Per-account and per-user billing


We briefly discussed the Seats and Logins fields of our billing struct in the
previous sub-chapter. It’s time to explore how we will handle per-user billing.
It’s worth clarifying what kind of per-user billing we will be handling and what
we’re not going to support.
All paid users of an account cannot have different plans.
Supporting multiple plans per account would be out of the scope of this chapter.
We will instead implement an easy way for a paid account to add and remove
users, named seats.
We will be using the qty field of Stripe’s subscription object for that. I know
what you’re saying:
I thought all users would have had their own subscription.
That would be too much for this book, and it involves lots of downsides:
1. Stripe do multiple charges for each subscription, so a big account with 25
paid users could have 15-20 charges potentially on the same day. That’s
problematic with some banks and charges could be refused.
2. The data model to handle such a scenario would be kill of the idea of
supporting both per-account or per-user in the same billing module.
I would recommend reading this Stripe article - https://stripe.com/docs/subscriptions/multiple
- should you need to cover per-account and per-user billing in your application.

7
Let’s keep things simple; it’s already complicated.
The idea is to have our API update the Stripe subscription when an account
adds or removes paid users. We will use Stripe prorating feature to calculate
additional or removal of fees for the current billing period.
Using the quantity field is the most straightforward and cleanest way to handle
this problem without having to mess with the billing anniversary, changing
billing period or manually charging extra.
The first thing we will do is calculate the Seats in our overview function. We
will replace the line where we wrote this comment:
1 // TODO: getting logins and calculating paying seats

With the following code.


1 ov. Logins = account .Users
2
3 // get all logins for user roles
4 for _, l := range account .Users {
5 if l.Role < model . RoleFree {
6 ov.Seats ++
7 }
8 }

We’re iterating over our users and checking if the user is a paid one. The UI will
be able to display or multiply Seats with the CostPerNewUser to inform the
account admin that there will be charges when adding/promoting a paid user.
We will now need an easy way to +1/-1 the qty field of the subscription. When
we add a new paid user, when we promote a free user to a paid one, and when
we remove or demote a paid user. All of those actions will need to modify the
subscription.
We need to import the following Stripe package in our billing.go file:
1 " github .com/ stripe /stripe -go/sub"

1 func (b Billing ) changeQuantity (stripeID , subID string ,


qty int) error {
2 p := & stripe . SubParams { Customer : stripeID , Quantity :
uint64 (qty)}
3 _, err := sub. Update (subID , p)
4 return err
5 }

This changeQuantity function can be called from the User controllers that
we’ve created in chapter 6: Authorization middleware removeUser.

8
1 func (u User) removeUser (w http. ResponseWriter , r
*http. Request ) {
2 ...
3 // do we need to lower quantity
4 if acct. IsPaid () && l.Role != data. RoleFree {
5 acct.Seats --
6
7 b := Billing {}
8 if err := b. changeQuantity (acct.StripeID ,
acct.SubID , acct.Seats ); err != nil {
9 engine . Respond (w, r,
http. StatusInternalServerError , err)
10 return
11 }
12
13 if err := db. Users . SetSeats (acct.ID ,
acct.Seats); err != nil {
14 engine . Respond (w, r,
http. StatusInternalServerError , err)
15 return
16 }
17 }
18 ...
19 }

When we are removing a paid user, one that had a Role higher than the free
RoleFree, we call the changeQuantity function of our Billing controller.
On the next billing date for this account, they will have the credit for unused
paid time. For example, imagine this scenario:
1. An account started its paid subscription on October 10th with one paid
user.
2. On October 15th they added another paid user, so they paid for Oct-15
to Nov-2. We will see this code next by the way.
3. On Oct 20th they remove the user added on Oct 15th, on their next billing
date Nov-2 they will need to pay for one user for Nov-2 to Dec-2, but their
total will be credited for the unused paid time from Oct 20th to Nov 2nd
for the second user.
Those are great benefits we can leverage from Stripe and from using the qty
field of their subscription object.
Our last task to handle is when a user is upgraded from a free to paid role or
downgraded from a paid to a free role.
For that, we will create a new function in our Billing controller named
userRoleChanged.

9
1 func (b Billing ) userRoleChanged (db data.DB , accountID
model .Key , oldRole , newRole model . Roles ) (paid bool ,
err error) {
2 acct , err := db.Users.Get( accountID )
3 if err != nil {
4 return false , err
5 }
6
7 // if this is a paid account
8 if acct. IsPaid () {
9 // if they were a free user
10 if oldRole == model. RoleFree {
11 // and are now a paid user , we need to +1
qty and prepare the invoice
12 if newRole == model . RoleAdmin || newRole ==
model. RoleUser {
13 paid = true
14
15 // we increase the seats number for this
account
16 acct. Seats ++
17
18 // try to change their subscription (+1
qty)
19 if err = b. changeQuantity (acct.StripeID ,
acct.SubID , acct. Seats ); err != nil {
20 return
21 }
22
23 // TODO: something missing here :)
24
25 if err = db.Users . SetSeats (acct.ID ,
acct.Seats); err != nil {
26 return false , err
27 }
28 }
29 } else {
30 // they were a paid user , now they are set
as free
31 if newRole == model . RoleFree {
32 acct.Seats --
33
34 if err = b. changeQuantity (acct.StripeID ,
acct.SubID , acct. Seats ); err != nil {
35 return
36 }

10
37
38 if err = db.Users . SetSeats (acct.ID ,
acct.Seats); err != nil {
39 return false , err
40 }
41 }
42 }
43 }
44 return false , nil
45 }

Hopefully, the inline comments are clear enough here. We simply +1 or -1 the
Stripe subscription according to the old/new roles.
This function can be called from our User controller in the invite and
changeRole functions.
1 func (ctrl * membership ) changeRole (w
http. ResponseWriter , r *http. Request ) {
2 ...
3
4 b := Billing {}
5 _, err = b. userRoleChanged (db , keys.AccountID ,
l.Role , data.Role)
6 if err != nil {
7 respond (w, r, http. StatusInternalServerError ,
err)
8 return
9 }
10 ...
11 }

Please refer to the full source code for the invite function code.
There’s one thing that’s not working properly in our implementation
at the moment.
It’s not that it is a bug or anything. Maybe more a preference choice. At this
moment, when we invite or promote a user there are no charges from Stripe.
Let’s take our scenario above. On step 2 when they added a new paid user
on October 15th. The current code would simply require them to pay for the
Oct-15 to Nov-2 on their next billing date on Nov-2. So if they do not remove
this new user, on Nov-2, they would pay for two users for Nov-2 to Dec-2 and
extra for one user for Oct-15 to Nov-2.
Personally, I prefer to trigger a charge when someone is added or promoted.
That way, if you have to pay for resources for additional users, you cover the
fees by having the money already charged upfront.

11
Remember I used the following comment in the userRoleChanged function:
1 // TODO: something missing here :)

We will need to create a Stripe invoice if we want to trigger a Stripe charge


when a subscription quantity increases.
The following code uses packages that we’ve not seen yet like the Queue struct
and job package. We will be implementing them in chapter 9: Emails and
background tasks.
Think of these as a teaser for chapter 9, I trust you will get it.
This code would replace the comment above.
1 // ensure that the charges will be immediate and not on
next billing date
2 if err := queue. Enqueue ( queue . TaskCreateInvoice ,
acct. StripeID ); err != nil {
3 return paid , err
4 }

The trick is to make sure the account admin has enough time to add/promote
multiple users in a defined period without creating a Stripe invoice each time.
Wait, what?
We don’t want to bombard Stripe each time a new paid user is added or pro-
moted. We would prefer to group those into one call. So when an account admin
wants to add/promote user(s) we hope they will do this in a specific timeframe,
say 2 hours.
To handle that scenario we enqueue a task of type TaskCreateInvoice. If
there’s already a queue that has not been processed yet for this account, we do
not create another one. This queue will be processed 2 hours after its creation.
We will not see the implementation of the dequeue job package in this chapter.
We can at least look at the billing function that will be called when this
queued job is processing.
We will need to import this Stripe package:
1 " github .com/ stripe /stripe -go/ invoice "

1 func (b * Billing ) Run(qt QueueTask ) error {


2 id , ok := qt.Data .( string )
3 if !ok {
4 return fmt. Errorf (" the data should be a stripe
customer ID ")
5 }
6
7 // we delay execution for 2 hours to let add/ remove

12
8 // operations in between creating the invoice
9 // since we 're on a go routine we can use a
time.Sleep
10 time. Sleep (2 * time.Hour)
11
12 p := & stripe . InvoiceParams { Customer : id}
13 _, err := invoice .New(p)
14 return err
15 }

By calling Stripe New function on their invoice package we tell Stripe that it
is OK to bill the pending charges (the modified qty we’ve made) right now.
Like I said, this is more of a preference type feature. If you don’t want your
Stripe customer to be charged right away when they add/promote paid users,
you can just omit those functions.

1.4 How to simultaneously handle multiple ver-


sions of pricing
You can believe me when I tell you that this feature will save you tons of hard
work 1 or 2 years from now.
When we’re building the initial version of a SaaS, being the MVP or a beta
version, we’re generally not thinking about the fact that we will have to handle
multiple pricing sets at some point.
When you attend MicroConf, and Patrick McKenzie (patio11) tells you to double
your price today you’ll be happy I’ve covered your back with multi-pricing sets.
One easy way I’ve found to handle this is simply to have dates attached to Stripe
plan ID. For example, say you have a starter, pro and enterprise plans, the ID for
those plans on Stripe could be starter_171020, pro_171020, enterprise_171020.
Those are your launched plans on 2017-10-20.
If you were to change your plan in April of 2018, you would create a new set
of plans in Stripe with the following ids: starter_180410, pro_180410, and
enterprise_180410.
Let’s look at our Plan struct.
1 // BillingPlan defines what one plan to have access to
and set limitations
2 type BillingPlan struct {
3 ID string `json :" id"`
4 Name string `json :" name"`
5 Version string `json :" version "`
6 Price float32 `json :" price "`

13
7 YearlyPrice float32 `json :" yearly "`
8 AllowABC bool
`json :" allowABC "`
9 }

The ID field will match our id on Stripe, we also define the Price and
YearlyPrice as well as the Version. The Version will be used to group a
plan into a pricing set.
The AllowABC is where you would have your limitation and allowing certain
features or limits for a specific plan.
Inside our data package, we’ll create a BillingPlan.go file. We will handle
our plans via a map[string]BillingPlan where the key is the ID.
Inside the init function we will create our plans like this:
1 var plans map[ string ] BillingPlan
2
3 func init () {
4 plans = make(map[ string ] BillingPlan )
5
6 plans [" free "] = BillingPlan {
7 ID: "free",
8 Name: "Free",
9 Version : "201612" ,
10 }
11
12 plans [" starter -201612"] = BillingPlan {
13 ID: "starter -201612" ,
14 Name: " Starter ",
15 Version : "201612" ,
16 Price: 25,
17 YearlyPrice : 15,
18 }
19
20 plans ["pro -201612"] = BillingPlan {
21 ID: "pro -201612" ,
22 Name: "Pro",
23 Version : "201612" ,
24 Price: 55,
25 YearlyPrice : 35,
26 }
27
28 plans [" enterprise -201612"] = BillingPlan {
29 ID: " enterprise -201612" ,
30 Name: " Enterprise ",
31 Version : "201612" ,

14
32 Price: 95,
33 YearlyPrice : 65,
34 }
35
36 plans [" starter -201707"] = BillingPlan {
37 ID: "starter -201707" ,
38 Name: " Starter ",
39 Version : "201707" ,
40 Price: 39,
41 YearlyPrice : 29,
42 }
43
44 plans ["pro -201707"] = BillingPlan {
45 ID: "pro -201707" ,
46 Name: "Pro",
47 Version : "201707" ,
48 Price: 99,
49 YearlyPrice : 79,
50 }
51
52 plans [" enterprise -201707"] = BillingPlan {
53 ID: " enterprise -201707" ,
54 Name: " Enterprise ",
55 Version : "201707" ,
56 Price: 129 ,
57 YearlyPrice : 159 ,
58 }
59 }

We have two pricing sets in this example, 201612 and 201707. To make things
easier and so we don’t hard code those magic strings in our code base we will
create a const to match each pricing set version.
1 const (
2 // Plan201612 is for plans from Dec 2016 to Jul 2017
3 Plan201612 = "201612"
4 // Plan201707 is for current plans
5 Plan201707 = "201707"
6 )

The only thing left to cover here is some quick helper functions that will allow
us to get the plans for a specific version and get plans based on the current
version of the account.
Let’s start with a quick function to get a specific plan:
1 // GetPlan returns a specific plan by ID
2 func GetPlan (id string ) ( BillingPlan , bool) {

15
3 v, ok := plans[id]
4 return v, ok
5 }

We’re simply using Go’s map to find the requested plan and return it if we found
that plan.
The next function gets the list of all plans matching a version:
1 // GetPlans returns a slice of the desired version plans
2 func GetPlans (v string ) [] BillingPlan {
3 var list [] BillingPlan
4 for k, p := range plans {
5 if k == "free" {
6 // the free plan is available on all versions
7 list = append (list , p)
8 } else if p. Version == v {
9 // this is a plan for the requested version
10 list = append (list , p)
11 }
12 }
13 return list
14 }

As you can see here, the free plan is always part of each pricing sets. If you are
not planning on supporting a free plan, you may remove that entry.
The last helper function accepts a plan as an argument and returns the matching
plans for that version. This is useful if you have a page in your UI where the
user can upgrade/downgrade from plan to plan. They will see the plans for
their pricing set.
1 // GetPlansVersion returns a slice of the plans matching
a current plan
2 func GetPlansVersion (plan string ) [] BillingPlan {
3 if p, ok := plans[plan ]; ok {
4 return GetPlans (p. Version )
5 }
6 // we are returning current plan since we could not
find this plan
7 return GetPlans ( Plan201711 )
8 }

With this simple struct and functions, it would be easy to introduce new pricing
sets. Create the new plans in the init function, add a new const holding the
date for this new pricing set and you’re done.
What if one pricing set is to be discontinued?

16
You could update all your users that are still using that version say
starter_201612 to starter_201707 and optionally you can update them on
Stripe or not. But at least the next time they upgrade or downgrade, they
would be on the current pricing set.

1.5 From trial to a paid customer, with optional


coupon code
It’s time to make some money :).
At some point, you’ll want to have your trial account convert into a paid account.
Imagine we have a billing page for the trial user that allows them to pick one
of our three paid plans that we’ve seen above.
Once the user selects a plan, they will be asked to enter their credit card infor-
mation e.g. credit card number, expiration date, CVC number, etc.
From there the UI will call this route:
1 } else if r. Method == http. MethodPost {
2 if head == "start" {
3 b. start(w, r)
4 }
5 }

We will now investigate our start function of our Billing controller.


Here’s what we need to perform to create a new Stripe customer with an active
subscription:
1. Create the Stripe customer with the captured credit card as a payment
source.
2. Calculate the number of paying users (if we want a per-user billing model).
3. Get the desired plan and create the subscription.
4. Apply any coupon the user used to the subscription.
5. Cancel their trial and convert their account to a paid one.
6. Return the new BillingOverview if everything is fine.
Let’s start by looking at what we will receive as struct:
1 // BillingNewCustomer represents data sent to api for
creating a new customer
2 type BillingNewCustomer struct {
3 Plan string `json :" plan"`
4 Card BillingCardData `json :" card"`
5 Coupon string `json :" coupon "`
6 Zip string `json :" zip"`
7 IsYearly bool `json :" yearly "`
8 }

17
Quick reminder: We’ve seen the BillingCardData in our first sub-chapter.
We’ll start by creating the function and creating the Stripe customer:
We will need to import these Stripe packages in our billing.go file:
1 import (
2 ...
3 " github .com/ stripe /stripe -go/ coupon "
4 " github .com/ stripe /stripe -go/ customer "
5 )

1 func (ctrl * billing ) start(w http. ResponseWriter , r


*http. Request ) {
2 ctx := r. Context ()
3 keys := ctx.Value ( engine . ContextAuth ).( engine .Auth)
4 db := ctx.Value( engine . ContextDatabase ).(* data.DB)
5
6 var data BillingNewCustomer
7 if err := engine . ParseBody (r.Body , &data); err !=
nil {
8 engine . Respond (w, r, http. StatusBadRequest , err)
9 return
10 }
11
12 p := & stripe . CustomerParams {Email : keys. Email }
13 p. SetSource (& stripe . CardParams {
14 Name: data.Card.Name ,
15 Number : data.Card.Number ,
16 Month: data.Card.Month ,
17 Year: data.Card.Year ,
18 CVC: data.Card.CVC ,
19 Zip: data.Zip ,
20 })
21
22 c, err := customer .New(p)
23 if err != nil {
24 engine . Respond (w, r,
http. StatusInternalServerError , err)
25 return
26 }
27 ...
28 }

Note: The first lines where we get the database connection, API keys and parsing
the request body into our struct were defined in chapter 3: A 50 lines web
framework.

18
We’re creating the stripe.CustomerParams and attaching the credit card in-
formation we’ve received and trying to create the Stripe customer.
Should the coupon code apply to the customer instead of the sub-
scription?
That’s a great question. It might be another personal choice here. For me, I
prefer to have a coupon code attached to the subscription.
The reason is if the customer ever cancels their subscription, they lose their
coupon whilst if the coupon is applied to the customer they would keep it
forever.
I think having it tied to the subscription is another small way to reduce churn
for users that do have a coupon code.
Let’s create our subscription now:
1 acct , err := db.Users. GetDetail (keys. AccountID )
2 if err != nil {
3 engine . Respond (w, r, http. StatusInternalServerError ,
err)
4 return
5 }
6
7 seats := 0
8 for _, u := range acct.User {
9 if u.Role < model. RoleFree {
10 seats ++
11 }
12 }
13
14 plan := data.Plan
15 if data. IsYearly {
16 plan += " _yearly "
17 }
18
19 // Coupon : " PRELAUNCH11 ",
20 subp := & stripe . SubParams {
21 Customer : c.ID ,
22 Plan: plan ,
23 Quantity : uint64 ( seats ),
24 }
25
26 if len(data. Coupon ) > 0 {
27 subp. Coupon = data. Coupon
28 }
29
30 s, err := sub.New(subp)

19
31 if err != nil {
32 engine . Respond (w, r, http. StatusInternalServerError ,
err)
33 return
34 }

We start by counting how many paid users this account has inside our seats
variable.
In Stripe, I like to use _yearly at the end of my plan ID to flag them as being
a yearly billing cycle.
From there we create the stripe.SubParams, and we apply the coupon if we
received one. Look into the source code for the function to validate the coupon
from UI.
At this point, we have a new paying customer with an active subscription. It’s
time to change their plan and cancel their trial state.
1 acct. TrialInfo . IsTrial = false
2 if err := db.Users. ConvertToPaid (acct.ID , c.ID , s.ID ,
data.Plan , data.IsYearly , seats ); err != nil {
3 engine . Respond (w, r, http. StatusInternalServerError ,
err)
4 return
5 }
6
7 ov := BillingOverview {}
8 ov. StripeID = c.ID
9 ov.Plan = data.Plan
10 ov. IsYearly = data. IsYearly
11 ov. Seats = seats
12
13 acct. StripeID = c.ID
14 acct. SubscribedOn = time.Now ()
15 acct. SubID = s.ID
16 acct.Plan = data.Plan
17 acct. IsYearly = data. IsYearly
18 acct. Seats = seats
19
20 engine . Respond (w, r, http.StatusOK , ov)

As you can see, our ConvertToPaid function inside our dal package updates
the account and sets the ID we’ve received from the customer and subscription
creation.
We then return a new BillingOverview with all the information required to
flag the account as a paid one.
Congratulations, you can start receiving online payments from your customers.

20
1.6 Upgrading and downgrading from plan to
plan
Sometimes you’re able to up-sell your plans to existing users. It is, of course, a
great way to increase your MRR (monthly recurring revenue). Having them to
upgrade to higher plan is always a good sign.
Unfortunately, the opposite also happens. You might want to understand why
they downgraded by having a quick call with them. Who knows you might get
useful information and potentially get them back on their plan.
I choose to implement one single function when changing plan. The reason is
to make sure the UI does not handle too many scenarios.
The changePlan of our billing controller tries to detect if they are upgrading
or downgrading. This is one of the only places in the code base we’re building
that I’m using magic strings that you’ll need to change to fit your plan naming
scheme.
Make sure you change the hard-coded plan names above.
We’ll start by defining the function and what it is receiving as posted data.
1 ctx := r. Context ()
2 keys := ctx.Value ( engine . ContextAuth ).( engine .Auth)
3 db := ctx.Value( engine . ContextDatabase ).(* data.DB)
4
5 var data = new( struct {
6 Plan string `json :" plan"`
7 IsYearly bool `json :" isYearly "`
8 })
9 if err := engine . ParseBody (r.Body , &data); err !=
nil {
10 engine . Respond (w, r, http. StatusBadRequest , err)
11 return
12 }
13
14 account , err := db.Users . GetDetail (keys. AccountID )
15 if err != nil {
16 engine . Respond (w, r,
http. StatusInternalServerError , err)
17 return
18 }
19
20 plan := data.Plan
21 ... ...
22 }

21
The caller needs to send the selected plan and if it’s yearly or not (monthly).
We now determine if we’re upgrading or downgrading, that’s why we’re getting
the current account so we can compare the newly selected plan with the current
account plan saved in the database.
1 newLevel , currentLevel := 0, 0
2 if len(plan) == 0 || plan == "free" {
3 newLevel = 0
4 } else if strings . HasPrefix (plan , " starter ") {
5 newLevel = 1
6 } else if strings . HasPrefix (plan , "pro ") {
7 newLevel = 2
8 } else {
9 newLevel = 3
10 }
11
12 if strings . HasPrefix ( account .Plan , " starter ") {
13 currentLevel = 1
14 } else if strings . HasPrefix ( account .Plan , "pro ") {
15 currentLevel = 2
16 } else {
17 currentLevel = 3
18 }

It is where you would need to make this match your plan names and number of
plans you have.
The goal here is to assign an int value to the newly selected plan and the current
one. Comparing the levels will be enough to understand the context we’re in,
upgrading or downgrading.
1 // did they cancelled
2 if newLevel == 0 {
3 // we need to cancel their subscriptions
4 if _, err := sub. Cancel ( account .SubID , nil); err !=
nil {
5 engine . Respond (w, r,
http. StatusInternalServerError , err)
6 return
7 }
8
9 if err := accounts . Cancel ( account .ID); err != nil {
10 engine . Respond (w, r,
http. StatusInternalServerError , err)
11 return
12 }
13 } // an else will follow next here

22
We handle the fact that they could downgrade to the free plan. If you’re not
offering a free plan, you may ignore this code and leave it there.
However, if you do offer a free plan, this account needs to be canceled.
Note: Please refer to the full source code for details of the dal package Accounts
Cancel function.
We will examine the else block shortly. But first, we need to take a small pause
and discuss the implication of an upgrade.
Like we’ve seen in the sub-chapter: Per-account and per-user billing, when we
update a Stripe subscription, the charges will occur only on their next billing
date.
We will look at a new scenario here:
1. Bob Graton converts from trial to paid on October 20th and takes your
yearly starter plan.
2. His credit is charge October 20th for that new subscription.
3. On December 10th he upgrades his account to your pro plan. Woohoo,
nice.
4. Stripe would add the charge for Dec 10th to Oct 20th on their next billing
date.
Just like the per-user billing when we were adding a new paid user, upgrading
a plan also means that we need to create a Stripe invoice so the charges can be
executed when the user upgrades and not at their next anniversary billing date.
This is our else block handling upgrading and downgrading:
1 } else {
2 if data. IsYearly {
3 plan += " _yearly "
4 }
5
6 // calculate paid users
7 // skip for clarity , we have this same code already
at multiple
8 // places . Time for a refactor by the way to honor
DRY principle .
9
10 subParams := & stripe . SubParams { Customer :
account .StripeID ,
11 Plan: plan ,
12 Quantity : uint64 ( seats)}
13 // if we upgrade we need to change billing cycle date
14 upgraded := false
15 if newLevel > currentLevel {
16 upgraded = true

23
17 } else if account . IsYearly == false && data. IsYearly
{
18 upgraded = true
19 }
20
21 if upgraded {
22 // queue an invoice create for this upgrade
23 queue. Enqueue ( queue . TaskCreateInvoice ,
account . StripeID )
24 }
25
26 if _, err := sub. Update ( account .SubID , subParams );
err != nil {
27 engine . Respond (w, r,
http. StatusInternalServerError , err)
28 return
29 }
30 ...
31 }

It is almost identical to the add user process if we’re upgrading we queue the
creation of a Stripe invoice. For upgrading and downgrading, we update the
Stripe subscription to reflect the newly selected plan.
From there we need to update the account also to reflect the newly selected
plan. I’m skipping this as well since we’re starting to repeat ourselves.
From your UI point-of-view you have one route to call that handles upgrading
and downgrading:
1 } else if r. Method == http. MethodPost {
2 if head == "start" {
3 b. start(w, r)
4 } else if head == " changeplan " {
5 b. changePlan (w, r)
6 }
7 }

1.7 Add/update/remove credit cards


I’d say this is another aspect that we’re usually not thinking about on day 1
when we first launch, but this one is important. Letting your users upgrade
their credit cards to ensure they always have a billable source is essential.
One must have aspect of Stripe is that they have a partnership with banks that
ensure the credit card updates when your customer receives a new one or when

24
their expiration date changes.
It is already difficult to have them commit to giving us money and if we make it
hard for them to keep their account in a paid state, we’re not creating a smooth
customer experience. We need to make sure it’s easy to add and remove credit
cards.
The UI and UX of the entire billing process are as important than the core of
the app.
This sub-chapter will be short; we will have three simple functions in our
Billing controller to add, update and remove Stripe card.
Stripe would try an alternative source if the default one failed for some reason.
Having your users entering multiple credit cards is another easy way to prevent
payment failure.
This is the route we need:
1 router . Handle ("/ billing /card",
adapt (http. HandlerFunc (b. updateCard ),
roleAdmin ...)). Methods (" PUT ")
2 router . Handle ("/ billing /card",
adapt(http. HandlerFunc (b. addCard ),
roleAdmin ...)). Methods (" POST ")
3 router . Handle ("/ billing /card /{ id}",
adapt(http. HandlerFunc (b. deleteCard ),
roleAdmin ...)). Methods (" DELETE ")

The functions are self-explanatory:


1 func (b Billing ) updateCard (w http. ResponseWriter , r
*http. Request ) {
2 ctx := r. Context ()
3 keys := ctx.Value ( engine . ContextAuth ).( engine .Auth)
4 db := ctx.Value( engine . ContextDatabase ).(* data.DB)
5
6 account , err := db.Users . GetDetail (keys. AccountID )
7 if err != nil {
8 engine . Respond (w, r, http. StatusBadRequest , err)
9 return
10 }
11
12 var data BillingCardData
13 if err := engine . ParseBody (r.Body , &data); err !=
nil {
14 engine . Respond (w, r,
http. StatusInternalServerError , err)
15 return
16 }

25
17
18 if c, err := card. Update (data.ID ,
& stripe . CardParams { Customer : account .StripeID ,
Month: data.Month , Year: data.Month , CVC:
data.CVC }); err != nil {
19 engine . Respond (w, r,
http. StatusInternalServerError , err)
20 } else {
21 card := BillingCardData {
22 ID: c.ID ,
23 Name: c.Name ,
24 Number : c.LastFour ,
25 Month: fmt. Sprintf ("%d", c. Month ),
26 Year: fmt. Sprintf ("%d", c.Year),
27 Expiration : fmt. Sprintf ("%d / %d", c.Month ,
c.Year),
28 Brand: string (c. Brand ),
29 }
30 engine . Respond (w, r, http.StatusOK , card)
31 }
32 }
33
34 func (b Billing ) addCard (w http. ResponseWriter , r
*http. Request ) {
35 ctx := r. Context ()
36 keys := ctx.Value ( engine . ContextAuth ).( engine .Auth)
37 db := ctx.Value( engine . ContextDatabase ).(* data.DB)
38
39 account , err := db.Users . GetDetail (keys. AccountID )
40 if err != nil {
41 engine . Respond (w, r, http. StatusBadRequest , err)
42 return
43 }
44
45 var data BillingCardData
46 if err := engine . ParseBody (r.Body , &data); err !=
nil {
47 engine . Respond (w, r,
http. StatusInternalServerError , err)
48 return
49 }
50
51 if c, err := card.New (& stripe . CardParams { Customer :
account .StripeID , Name: data.Name , Number :
data.Number , Month : data.Month , Year: data.Year ,
CVC: data.CVC }); err != nil {

26
52 engine . Respond (w, r,
http. StatusInternalServerError , err)
53 } else {
54 card := BillingCardData {
55 ID: c.ID ,
56 Number : c.LastFour ,
57 Expiration : fmt. Sprintf ("%d / %d", c.Month ,
c.Year),
58 Brand: string (c. Brand ),
59 }
60 engine . Respond (w, r, http.StatusOK , card)
61 }
62 }
63
64 func (b Billing ) deleteCard (w http. ResponseWriter , r
*http. Request ) {
65 ctx := r. Context ()
66 keys := ctx.Value ( engine . ContextAuth ).( engine .Auth)
67 db := ctx.Value( engine . ContextDatabase ).(* data.DB)
68
69 account , err := db.Users . GetDetail (keys. AccountID )
70 if err != nil {
71 engine . Respond (w, r, http. StatusBadRequest , err)
72 return
73 }
74
75 cardID := mux.Vars(r)["id"]
76
77 if _, err := card.Del(cardID ,
& stripe . CardParams { Customer : account . StripeID });
err != nil {
78 engine . Respond (w, r,
http. StatusInternalServerError , err)
79 } else {
80 engine . Respond (w, r, http.StatusOK , true)
81 }
82 }

Here you go, three simple functions that you can now call from your UI to make
sure your users can manage their credit cards easily.

27
1.8 Previous invoices and upcoming invoice pre-
view
When we started getting paid customers at Roadmap the most frequent question
was “Where are my invoices?”.
A variation of the same question: “My company needs to see our address and
your address on our invoice, where can we print those invoices?”.
Nope, our friends at Stripe are not handling this for us. They are sending email
receipts. A receipt is NOT enough for a company to have your SaaS as a legit
business expense. In some countries, the invoice has to include both parties
addresses.
Also, keep in mind that the automated Stripe receipts are often caught in the
spam folder. So having a way for your user to view and print past invoices and
see the upcoming invoice is a great way to show them you’re serious about their
experience.
At the end of their fiscal year when they are in a rush and need to supply all
the invoices to their CPA, having a one-click download link for all their invoices
will be a nice touch that they will remember.
This is the route that will returns all their invoices:
1 router . Handle ("/ billing / invoices ",
adapt (http. HandlerFunc (b. invoices ), roleAdmin ...))

You’ll need to implement a nice invoice template in your UI but at least you’ll
have all the data available.
1 func (b Billing ) invoices (w http. ResponseWriter , r
*http. Request ) {
2 ctx := r. Context ()
3 keys := ctx.Value ( engine . ContextAuth ).( engine .Auth)
4 db := ctx.Value( engine . ContextDatabase ).(* data.DB)
5
6 account , err := db.Users . GetDetail (keys. AccountID )
7 if err != nil {
8 engine . Respond (w, r,
http. StatusInternalServerError , err)
9 return
10 }
11
12 var invoices []* stripe . Invoice
13
14 iter :=
invoice .List (& stripe . InvoiceListParams { Customer :
account . StripeID })

28
15 for iter.Next () {
16 invoices = append (invoices , iter. Invoice ())
17 }
18
19 engine . Respond (w, r, http.StatusOK , invoices )
20 }

It will return a slice of stripe.Invoice. Here are some useful fields for this
struct that you would want to use in your invoice UI:
• Amount: is the invoice total amount.
• Customer: Where you can get your customer’s address and information.
• Date and DueDate: Useful timestamps to output.
• Lines: Will contain the detail of this invoice charges.
• Paid: Indicate if the balance has been paid alredy.
• Start and End: Timestamps indicating period start and end.
There’s lot more you can output. Since the endpoint returns the
stripe.Invoice Stripe struct, you may refer to the Stripe API documentation
for details.
One other thing you can show to your user is a preview of their next invoice.
Here’s the function
1 func (b * billing ) getNextInvoice (w http. ResponseWriter ,
r *http. Request ) {
2 ...
3
4 i, err :=
invoice . GetNext (& stripe . InvoiceParams { Customer :
account . StripeID })
5 if err != nil {
6 engine . Respond (w, r,
http. StatusInternalServerError , err)
7 return
8 }
9 engine . Respond (w, r, http.StatusOK , i)
10 }

I skipped the database connection, and API key retrieval and account fetch
code, this function returns a stripe.Invoice struct containing information for
the upcoming invoice for this account.
What if they want to download a PDF version?
My recommendation is to create a static route in your UI that can render an
invoice as HTML and use a separate tool like wkhtmltopdf to transform this
rendered HTML invoice into a PDF.

29
We can change the invoice.GetNext() function’s main code above with the
following to get a specific Stripe invoice:
1 i, err := invoice .Get(id , nil)

I’ll leave that to you to create this HTML to PDF flow if you need that.

1.9 Handling key Stripe webhook actions


I’m a huge fan of webhooks. Getting a HTTP call rom another system when
something important your app cares about has happened is one compelling
aspect of the Web that I enjoy.
Stripe exposes a massive number of webhooks. Here are what I consider to
be the minimum you’ll need. The way we implement it will hopefully let you
handle more web hooks just as smoothly.
A refresher on webhooks
You tell a platform that you want to be called via a HTTP request when specified
events occur. For example with Stripe, we could be notified when a payment
failure happens and when Stripe cancels a subscription if too many payment
failures occur.
Another benefit of having built our SaaS using an API-first approach as we’ve
discussed in chapter 5: API-first my friend is that we’re already able to handle
external webhooks.
We’re going to handle one Stripe webhook in particular here and it’s when Stripe
cancels a subscription after too many payment failures.
It’s optional, you can customize how Stripe handles your payment failure steps
and what the outcome of not having a successful charge is.
I prefer to have the subscription canceled. Since we’ve built a credit card expi-
ration checker in sub-module: 6, the user already has been alerted and should
have had enough time to react and update their credit card.
The event type Stripe will post to our web app is customer.subscription.deleted.
1 func (b Billing ) stripe (w http. ResponseWriter , r
*http. Request ) {
2 ctx := r. Context ()
3 db := ctx.Value( engine . ContextDatabase ).(* data.DB)
4
5 // no matter what happen , Stripe wants us to send a
200
6 defer w.Write ([] byte ("ok"))
7
8 var data WebhookData

30
9 if err := engine . ParseBody (r.Body , &data); err !=
nil {
10 log. Println (err)
11 return
12 }
13
14 if data.Type == " customer . subscription . deleted " {
15 subID := data.Data. Object .ID
16 if len(subID) == 0 {
17 log. Println (fmt. Errorf (" no subscription
found to this
customer . subscription . deleted %s",
data.ID))
18 return
19 }
20
21 stripeID := data.Data. Object . Customer
22 if len( stripeID ) == 0 {
23 log. Println (fmt. Errorf (" no customer found to
this invoice . payment_succeeded %s",
data.ID))
24 return
25 }
26
27 // check if it 's a failed payment_succeeded
28 account , err := db. Users . GetByStripe ( stripeID )
29 if err != nil {
30 log. Println (fmt. Errorf (" no customer matches
stripe id", stripeID ))
31 return
32 }
33
34 if len( account . SubscriptionID ) > 0 {
35 // TODO: Send emails
36
37 if err := db.Users . Cancel ( account .ID); err
!= nil {
38 log. Println (fmt. Errorf (" unable to cancel
this account ", account .ID))
39 return
40 }
41 }
42 }
43 }

I have created the WebhookData struct which you can see from the full source

31
code.
Whenever Stripe reaches the end of its payment retry steps, the subscription
will be deleted and Stripe will post a webhook to your configured URL.
You can register your webhooks in your Stripe Settings and Webhooks tab.
We’re finding the specific account by its StripeID. The code to send the email
is commented for clarity, and we are canceling the account, primarily returning
them to a free account.

1.10 Smooth cancellation


The only positive thing you can get from a cancellation is the real reason behind
the cancellation.
The first SaaS I built was deleting the Stripe customer when they were cancelling,
that was a huge mistake.
The cancellation should only cancel the active subscription(s) for this Stripe
customer. That way you still have access to their past invoices, and you can
still let them print their invoices.
Also, it’s easier for them to pick a plan later on since they do not need to supply
their credit card. Adding a new subscription to an existing Stripe customer will
be enough to restart receiving money from them.
I’d suggest that you force the user to submit the reason why they’re canceling
on your UI. The function we will build will take that reason and send an email
to let you know someone has canceled.
We will delete the Stripe subscription. This is the route:
This is our cancel function in our billing controller:
1 func (b Billing ) cancel (w http. ResponseWriter , r
*http. Request ) {
2 ctx := r. Context ()
3 keys := ctx.Value ( engine . ContextAuth ).( engine .Auth)
4 db := ctx.Value( engine . ContextDatabase ).(* data.DB)
5
6 var data = new( struct {
7 Reason string `json :" reason "`
8 })
9 if err := engine . ParseBody (r.Body , &data); err !=
nil {
10 engine . Respond (w, r, http. StatusBadRequest , err)
11 return
12 }
13

32
14 // SendMail would be call here passing the reason
15
16 account , err := db.Users . GetDetail (keys. AccountID )
17 if err != nil {
18 engine . Respond (w, r,
http. StatusInternalServerError , err)
19 return
20 }
21
22 if _, err := sub. Cancel ( account . SubscriptionID ,
nil); err != nil {
23 engine . Respond (w, r,
http. StatusInternalServerError , err)
24 return
25 }
26
27 if err := db.Users . Cancel ( account .ID); err != nil {
28 engine . Respond (w, r,
http. StatusInternalServerError , err)
29 return
30 }
31
32 engine . Respond (w, r, http.StatusOK , true)
33 }

There’s another important place in our SaaS app that we will need to handle
the cancellation of a subscription. It’s in our membership controller that we’ve
seen in chapter 6: Authorization middleware.
Hopefully, with the reason gathered during that cancellation process, you’ll be
able to make adjustments and prevent others from churning for similar reasons.

33

You might also like