Build A Saas App in Go: Dominic St-Pierre 2018/09/02
Build A Saas App in Go: Dominic St-Pierre 2018/09/02
Build A Saas App in Go: Dominic St-Pierre 2018/09/02
Dominic St-Pierre
2018/09/02
Contents
1
Chapter 1
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.
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 )
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.
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
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"
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 :)
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 "
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.
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.
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 )
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 }
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 ")
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.
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.
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