Subscription Payments #2: Keeping track of Customer Billing information using Mongo and Stripe Webhooks

Subscription Payments #2: Keeping track of Customer Billing information using Mongo and Stripe Webhooks

Languages | Tools Used | Time Saved

Node.js, HTML Stripe 3 weeks → 40 mins

Goodies

Live Demo

This is the Part 2 in the series of guides on adding, managing, and keeping track of subscription payments using Stripe an Mongo for your SaaS app.

Subscription Payments #1: Adding Basic and Pro subscription plans using Stripe
Subscription Payments #3: Update and Cancel plans via a Manage Billing Screen using Stripe
Subscription Payments #4: Access premium content based on subscription plan
Subscription Payments #5: Deploy Stripe Application to Production using Heroku

Subscription Payments are the bread and butter of a SaaS application; it's how you start generating revenue. In this guide, we will:

  • Save customer data in a Mongo database
  • Display Account Dashboard view
  • Set up the Stripe Webhook to receive events from the Stripe Dashboard
  • Update plan details when customer payment goes through

Start Local Mongo DB

mongod

A local Mongo instance should start up on mongo://localhost:27017

Add MongoDB Connection to App

Add the MongoDB connection to app.js:

const mongoose = require('mongoose')

const db = 'mongodb://localhost:27017/users'
mongoose.connect(
  db,
  {
    useUnifiedTopology: true,
    useNewUrlParser: true,
    useFindAndModify: false,
    useCreateIndex: true
  },
  (error) => {
    if (error) console.log(error)
  }
)

Create User Schema

For each user, we will save:

  • email: The email of the user
  • billingID: The Stripe Customer ID
  • plan: The plan type the customer is currently subscribed to: None, Basic, or Pro
  • hasTrial: If the customer is currently on a trial
  • endDate: The date when the trial or the plan will renew

Create the User Model

Create a new file called, src/user/user.model.js

const mongoose = require('mongoose')
const Schema = mongoose.Schema

const userSchema = new Schema({
  email: String,
  billingID: String,
  plan: { type: String, enum: ['none', 'basic', 'pro'], default: 'none' },
  hasTrial: { type: Boolean, default: false },
  endDate: { type: Date, default: null }
})

const userModel = mongoose.model('user', userSchema, 'user')

module.exports = userModel

We will also add a User Service by creating src/user/user.service.js

const addUser = (User) => ({ email, billingID, plan, endDate }) => {
  if (!email || !billingID || !plan) { throw new Error('Missing Data. Please provide values for email, billingID, plan') }

  const user = new User({ email, billingID, plan, endDate })
  return user.save()
}

const getUsers = (User) => () => {
  return User.find({})
}

const getUserByEmail = (User) => async (email) => {
  return await User.findOne({ email })
}

const getUserByBillingID = (User) => async (billingID) => {
  return await User.findOne({ billingID })
}

const updatePlan = (User) => (email, plan) => {
  return User.findOneAndUpdate({ email, plan })
}

module.exports = (User) => {
  return {
    addUser: addUser(User),
    getUsers: getUsers(User),
    getUserByEmail: getUserByEmail(User),
    updatePlan: updatePlan(User),
    getUserByBillingID: getUserByBillingID(User)
  }
}

We can now inject the model into the service by adding src/user/index.js

const User = require('./user.model')
const UserService = require('./user.service')

module.exports = UserService(User)

Now when our user logs in using their email address, in addition to creating a Stripe Customer, we can save their information in a database.

We are now saving the email instead of the customer ID in the user session.

In app.js:

const UserService = require('./src/user')

app.post("/login", async function (req, res) {
  const { email } = req.body;
  console.log("email", email);

  let customer = await UserService.getUserByEmail(email);
  let customerInfo = {};

  if (!customer) {
    console.log(`email ${email} does not exist. Making one. `);
    try {
      customerInfo = await Stripe.addNewCustomer(email);

      customer = await UserService.addUser({
        email: customerInfo.email,
        billingID: customerInfo.id,
				hasTrial: false,
        plan: "none",
        endDate: null
      });

      console.log(
        `A new user signed up and addded to DB. The ID for ${email} is ${JSON.stringify(
          customerInfo
        )}`
      );

      console.log(`User also added to DB. Information from DB: ${customer}`);
    } catch (e) {
      console.log(e);
      res.status(200).json({ e });
      return;
    }
  }

  req.session.email = email;
  res.redirect('/account')
})

Display Dashboard View

A customer should be able to view their account information on the front end when they succesfully log in. Because we are saving everything in a database, it is trivial to create such a view.

Let's update our /account endpoint in app.js to get the customer information from the database.

app.get('/account', async function (req, res) {
  const { email } = req.session
  const customer = await UserService.getUserByEmail(email)
  res.render('account.ejs', { customer })
})

We can use EJS to render customer information when they are already subscribed. If not, we can present the usual Buy a Plan view. In our views/account.ejs

<html>

<head>
</head>

<body>
  <div>
    <h1>Account Dashboard</h1>
    <p>Hi <%- customer.email %>
    </p>

    <%if (customer.plan=="none" ) { %>
      <p>
        You are currently not on any plan. Purchase a subscription below.
      </p>
      <input type="radio" id="basic" name="product" value="basic" />
      <label for="basic">Basic for $10</label><br />
      <input type="radio" id="pro" name="product" value="pro" />
      <label for="pro">Pro for $12</label><br />
      <button id="checkout-button" type="submit">
        Buy now
      </button>
      <% } else{ %>
        <p>
          You are currently on the <%- customer.plan %> plan %>
        </p>
        <%if (customer.hasTrial) { %>

          <p>Trial active until <%- customer.endDate %>
          </p>
          <% } else{ %>

            <p>Trial inactive. Plan will end on <%- customer.endDate %>
            </p>
            <% } %>
              <% } %>
  </div>

  <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"
    integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"
    crossorigin="anonymous"></script>
  <script type="text/javascript" src="https://js.stripe.com/v3/"></script>
  <script>
    var customer = <%- JSON.stringify(customer) %>;
    console.log(customer);
  </script>
  <script type="text/javascript" src="./js/account.js"></script>
</body>
<html>

Once again, the view by itself does not look great so we made a better styled version and added to the Github project. Here is the HTML and the CSS.

We can also create our checkout session with the customer information pre-filled. Edit src/js/account.js and add customer Billing ID.

checkoutButton.click(function () {
    const product = $("input[name='product']:checked").val()

    fetch('/checkout', {
      // ...
      body: JSON.stringify({
        product,
        customerID: customer.billingID
      })
    })
      .then((result) => result.json())
      .then(({ sessionId }) => stripe.redirectToCheckout({ sessionId }))
  })

Edit app.js with,

app.post('/checkout', async (req, res) => {
  const { customerID, product } = req.body

  const price = productToPriceMap[product.toUpperCase()]

  const session = await Stripe.createCheckoutSession(customerID, price)

  res.send({ sessionId: session.id })
})

Configure local webhook

Great! We can now add subscription plans for our customers. But we also have to add this information to our database. It's better if Stripe tells us when a subscription has been created successfully than us trying to guess.

This is exactly what the Stripe webhook is used for. We subscribe our Stripe account's webhook and any time an event occurs on the platform, we get an event notification. We can then use the information in the event to trigger functionalities on our application i.e. updating the database.

A few useful events that we can access with Stripe are customer.subscription.created , invoice.paid and more.

Download Stripe CLI from the official Stripe website here.

We can set up the webhook locally to capture those events.

stripe login
stripe listen --forward-to localhost:4242/webhook

image

We will use the Webhook Secret in the next step to listen to events.

Add a createWebhook in src/connect/stripe.js with STRIPE_WEBHOOK_SECRET :

const STRIPE_WEBHOOK_SECRET = 'wh_xx'

// ..
const createWebhook = (rawBody, sig) => {
  const event = Stripe.webhooks.constructEvent(
    rawBody,
    sig,
    STRIPE_WEBHOOK_SECRET
  )
  return event
}

module.exports = {
  getCustomerByID,
  addNewCustomer,
  createWebhook
}

Let's edit our app.js

app.use("/webhook", bodyParser.raw({ type: "application/json" }))

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }))

app.post('/webhook', async (req, res) => {
  let event

  try {
    event = Stripe.createWebhook(req.body, req.header('Stripe-Signature'))
  } catch (err) {
    console.log(err)
    return res.sendStatus(400)
  }

  const data = event.data.object
  console.log(event.type, data)

  res.sendStatus(200)
})

Notice the bodyParser.raw line needs to placed before the /webhook endpoint otherwise Stripe will throw an error.

Start the server.

npm start

Go to the Stripe dashboard. A webhook event will be fired when you Add a New Customer. The event will be caught by our application and printed in the terminal.

customer.subscription.created { id: 'sub_IahKmOJSUeCJ5b',
  object: 'subscription',
  application_fee_percent: null,
  billing_cycle_anchor: 1608247822,
  billing_thresholds: null,
  cancel_at: null,
  cancel_at_period_end: false,
  canceled_at: null,
  collection_method: 'send_invoice',
  created: 1608247822,
  current_period_end: 1610926222,
  current_period_start: 1608247822,
  customer: 'cus_IV5wklBSZWztsG',
  days_until_due: 30,
  default_payment_method: null,
  default_source: null,
  default_tax_rates: [],
  discount: null,
  ended_at: null,
  items:
   { object: 'list',
     data: [ [Object] ],
     has_more: false,
     total_count: 1,
     url: '/v1/subscription_items?subscription=sub_IahKmOJSUeCJ5b' },
  latest_invoice: 'in_1HzVvOKelOFxScryYDXXoBEj',
  livemode: false,
  metadata: {},
  next_pending_invoice_item_invoice: null,
  pause_collection: null,
  pending_invoice_item_interval: null,
  pending_setup_intent: null,
  pending_update: null,
  plan:
   { id: 'price_1Hu1OhKelOFxScryyLwJXiWV',
     object: 'plan',
     active: true,
     aggregate_usage: null,
     amount: 1200,
     amount_decimal: '1200',
     billing_scheme: 'per_unit',
     created: 1606938835,
     currency: 'cad',
     interval: 'month',
     interval_count: 1,
     livemode: false,
     metadata: {},
     nickname: null,
     product: 'prod_IV1RHfJZ3sQ8GM',
     tiers_mode: null,
     transform_usage: null,
     trial_period_days: null,
     usage_type: 'licensed' },
  quantity: 1,
  schedule: null,
  start_date: 1608247822,
  status: 'active',
  transfer_data: null,
  trial_end: null,
  trial_start: null }

Update customer plan

When the customer buys a new subscription, we will see a customer.subscription.created. We can use this information to update the customer plan in the database.

app.post('/webhook', async (req, res) => {
  let event

  try {
    event = Stripe.createWebhook(req.body, req.header('Stripe-Signature'))
  } catch (err) {
    console.log(err)
    return res.sendStatus(400)
  }

  const data = event.data.object
  console.log(event.type, data)

  switch (event.type) {
    case 'customer.subscription.created': {
      console.log('new subscription added' + JSON.stringify(data))
      const user = await UserService.getUserByBillingID(data.customer)

      if (data.plan.id === productToPriceMap.BASIC) {
        console.log('You are talking about basic product')
        user.plan = 'basic'
      }

      if (data.plan.id === productToPriceMap.PRO) {
        console.log('You are talking about pro product')
        user.plan = 'pro'
      }

      const isOnTrial = data.status === 'trialing'

      if (isOnTrial) {
        user.hasTrial = true
        user.endDate = new Date(data.current_period_end * 1000)
      } else if (data.status === 'active') {
        user.hasTrial = false
        user.endDate = new Date(data.current_period_end * 1000)
      }

      await user.save()

      break
    }
    default:
  }

  res.sendStatus(200)
})

We also need to check if the trial should be over. We don't have to check it asynchronously in the background by cron processes, that would be overkill. A better way to do it would be the customer logs in, we check if the trial should stay active or not.

In app.js

app.post("/login", async function (req, res) {
  const { email } = req.body;
  console.log("email", email);

  let customer = await UserService.getUserByEmail(email);
  let customerInfo = {};

  if (!customer) {
	  // ..
  } else {
    const isTrialExpired =
      customer.plan !== 'none' && customer.endDate < new Date().getTime()

    if (isTrialExpired) {
      console.log('trial expired')
      customer.hasTrial = false
      customer.save()
    } else {
      console.log(
        'no trial information',
        customer.hasTrial,
        customer.plan !== 'none',
        customer.endDate < new Date().getTime()
      )
    }
  }
  req.session.email = email
  res.send(customerInfo)
})

Done!

In this guide we learned how to:

  • Save customer data in a Mongo database
  • Set up the Stripe Webhook to receive events from the Stripe Dashboard
  • Update plan details when customer payment goes through

Our customers can buy subscriptions but they cannot cancel or update their plans. We will look inot implementing that in the next guide.