ProCoders

Paying {in instalments through Stripe with Nodejs 8.10 and Serverless}


Posted: April 20th 2018
« Back to All Posts

Async + Await has never felt so good!

Background

I love using Stripe. Not only is it a sleek affordable service, but it also has a great API, simple UI and doesn't force it's branding on to end-users. It's therefore no wonder that Stripe is used by 100,000+ businesses from hobbyists up to the likes of Lyft, Asos and Shopify.

At {Pro}Coders we offer our development bootcamp for £3,500, paid over seven monthly instalments of £500 per month. We do this purely to reduce the barrier to entry onto our course, as few of us are able to pay such a sum up front and we believe that everyone should have the opportunity to become a developer.

However, Stripe doesn't support taking payments in instalments out of the box. But you can create subscriptions, which are recurring payments for a fixed amount which continuously run on a monthly basis. With minimal set up though, we can harness subscriptions and use it to pay for large items in instalments through the use of Stripe webhooks and API.

Process

To achieve this, we will need to do something along the lines of the following:

  • Create a subscription in Stripe with an identifier to how much it costs in total;
  • Set up a webhook in Stripe that gets triggered when a charge has been successful;
  • Use the customer ID from the webhook payload to query for the subscriptions the customer belongs to;
  • Get the first subscription and determine the total amount to be paid*;
  • Use the customer ID from the webhook payload to query for all the charges the customer has made;
  • Filter the charges down to those that have been paid and not refunded;
  • Total the amount paid for each charge;
  • Compare this total to the cost of the subscription; and
  • If it's the same, then cancel the subscription. If it is less do nothing. If it is more, then raise an error so that it can be investigated, the subscription cancelled and a refund made to the customer for any over paid charges.

Implementation

For our implementation we are going to be using Serverless configured to use AWS Lambda and NodeJS. Serverless have great documentation and the beauty of using Serverless is that it will configure all of the AWS infrastucture needed for your Lambda function to work for you. Now that AWS Lambda has added support for Node 8.10, we are going to take advantage of Async+Await for our functions.

Install serverless and initialise the project, the documentation for this process is really simple and explained clearly so I won't discuss how to do it here.

Edit your serverless.yml file to look something like this:

service: ProCoders

provider:
  name: aws
  runtime: nodejs8.10
  stage: dev
  region: eu-west-1
  environment:
    STRIPE_SECRET_KEY: ${file(./.env.${opt:stage}.yml):STRIPE_SECRET_KEY}

functions:
  checkForFinishedPayments:
    handler: handler.call
    events:
      - http:
          path: /
          method: post

Now we can start to write some code! First we will begin with our main function that accepts the webhook, pulls out the customerID from the payload, makes calls to other functions to calculate whether or not we need to cancel a users subscription and returns the response back to Lambda.

const checkForFinishedPayments = async (event, context) => {
  const body = JSON.parse(event.body)

  // Set the customer id
  const customerID = {
    customer: body.data.object.customer,
    limit: 100
  }

  try {
    // Get the subscription plan and ID
    const [subscriptionID, subscriptionPlan] = await getSubscriptionPlan(customerID)

    // Work out the total for all successful payments made
    const totalPaid = await getTotalPaid(customerID)

    // Work out the total subscription cost from the subscription
    const totalDue = calculateTotalDue(subscriptionPlan)

    // Cancel the subscription if they have finished paying in full
    const result = await attemptToCancelSubscription(totalPaid, totalDue, subscriptionID)

    // Return response payload
    return {
      statusCode: 200,
      body: JSON.stringify({
        message: result,
        input: event
      })
    }
  } catch (error) {
    return respondWithError(event, error)
  }
}

In the above code, I have used the limit of 100 so that we get up to 100 results back from any Stripe API calls, the default being 10. This is useful in cases where a customer has made more than 10 instalments. Currently, we don't envisage that a customer could ever make over 100 charges. However, your application may vary, in which case you will need to cater for paginating the results in order to catch all charges.

Now we'll look at our function to return the subscription ID and plan. The ID is needed to cancel a subscription through the Stripe API; the plan will be the ID we used to set up the subscription with. This will be used to determine how much the user has left to pay.

const getSubscriptionPlan = async (customerID) => {
  const subscriptions = await stripe.subscriptions.list(customerID)

  if (subscriptions.data.length === 0) {
    throw new Error('Customer does not have any subscriptions')
  }

  const subscriptionID = subscriptions.data[0].id
  const subscriptionPlan = subscriptions.data[0].items.data[0].plan.id

  return [subscriptionID, subscriptionPlan]
}

Notice here that we do not have a try/catch block, this is because we have one within the main function that was used to call this one. Any errors will be caught within the try/catch block in our main function.

We also throw an error here if the customer has no subscriptions. This should never happen, so it's best we are alerted about it.

Next we get all the payments a user has made, but limit them to those that have been paid and not refunded.

const getTotalPaid = async (customerID) => {
  const query = Object.assign({}, customerID, {paid: true, refunded: false})
  const paidCharges = await stripe.charges.list(query)
  let totalPaid

  // Add together all payments made and return total
  if (paidCharges.data.length > 0) {
    totalPaid = paidCharges.data.map(item => item.amount).reduce((accumulator, item) => {
      return accumulator + item
    })
  } else {
    totalPaid = 0
  }

  return totalPaid
}

In order to calculate the total amount that they are due to pay, we need to determine which subscription plan the user is on.

const calculateTotalDue = (subscription) => {
  let totalDue

  // Define the old subscription prices that no longer exist in Stripe
  const subscriptionPrices = {
    Instalments: 350000,
    Parttime: 100000
  }

  // Work out how much they need to pay in total
  if (subscriptionPrices[subscription]) {
    totalDue = subscriptionPrices[subscription]
  } else {
    totalDue = subscription.split('_')[0]
  }

  return Number(totalDue)
}

In my example above, I have added a list of deprecated subscription plans that are no longer offered and have been removed from Stripe. We check if the user is subscribed to those first and if not take the first value of the subscription plan ID, e.g. 350000 from the original 350000_50000 ({TOTAL}_{INSTALMENT}) value.

With all of the above data in place, we are now in a position to determine if the user has paid in full and if so, cancel their subscription so that no more payments will be taken.

const attemptToCancelSubscription = async (totalPaid, totalDue, subscriptionID) => {
  if (totalDue === totalPaid) {
    await stripe.subscriptions.del(subscriptionID)
    return 'Cancelled Subscription'
  } else if (totalDue > totalPaid) {
    return `${totalDue - totalPaid} remaining`
  } else {
    throw new Error('Total paid is more than total due')
  }
}

We also have a sanity check in place here. We throw an error if the user has over paid, to alert us that something has gone wrong and to issue them a refund.

Finally, we have a function that is called when any errors are thrown which responds with a 502 and the error message. This can then be flagged within the alerts section of AWS Cloudwatch to email relevant parties that an error has occured and to investigate further.

Deploy to serverless and make note of the endpoint that is returned e.g. https://sdvgskjvbn5.execute-api.eu-west-1.amazonaws.com/dev/. This will be needed when we add the webhook to Stripe.

serverless deploy

Stripe Configuration

With the code implementation out of the way, we can proceed with setting up Stripe subscription plans and webhooks.

Within the subscription section add a plan and set the name and id using the format {TOTAL}_{INSTALMENT} e.g. 350000_50000. Please note that these values are in pennies.

Adding a subscription

Now add a webhook for charge.succeeded, as we only want to check if all payments have been made when a successful charge has been captured, and enter the endpoint URL that serverless returned after you deployed the Lambda function.

Adding a webhook

If you want to make sure that everything is working as intended, then you can send a test webhook from Stripe. Alternatively, use the body that Stripe sent in the test webhook and modify it to use different customer IDs for checking different payment scenarios. I love to use Insomnia for QAing API endpoints.

Usage

All of the code discussed in this article is available opensource on our Github account.

Footnotes

* We are assuming that the customer will only ever have one subscription. If your customers are signed up to more than one subscription now, in the past or at any time in the future then you will need to filter the subscriptions accordingly, otherwise you will not be able to accurately compare the total due.