Automated payments are fundamental to scale any tech startup. That is why at Teacode.io, we established a flow for delivering Stripe payments to your product faster than ever. In this post, I will explain this flow to you, so that you can incorporate it to your app as well. First, we will explore the Stripe API and then we will discuss how it fits in with the Feathers.js framework.

Stripe Installation with NPM or Yarn

You can install stripe using your favourite package manager (npm or yarn). It is as simple as running the following commands in your console.

npm install stripe --save
# or
yarn add stripe

Stripe API

Stripe is the industry standard for automating payments. It provides you with an API, which removes the burden of managing your clients' sensitive data. We have to first get familiar with some key concepts before we can start integrating Stripe, as a Feathers service, with our app.

Customers

The name of this object is rather self-explanatory. A customer is equivalent to your user; this will be the object you refer to when communicating with the Stripe API.

Invoices

These are created by Stripe for the customer as statements of the amount that is owed to you. They can be generated one-off or in a periodic fashion. These are important to you as they are the actual means through which the customer executes a payment.

Subscriptions

Stripe uses this object manages recurrent payments that include paying an invoice via generating periodic ones. It takes away the problem of remembering to invoice your customer regularly. It is assigned to a customer.

Plans

This is an object which resides at a higher level than subscriptions supported by Stripe. It tells Stripe how the app should generate a subscription. Such cases include paying both a yearly or a monthly subscription for your product. This would be carried out by creating two plans: monthly and annual. When a customer selects a plan, a subscription will be generated using the plan object's information.

Events

Stripe uses these to inform you about anything important that has happened. For example, it will notify you when a payment fails, allowing your app to act accordingly. This is carried out using webhooks.

Integrating with Feathers.js

Having the Stripe API's basic concepts spelt out, we can now dive into the integration with Feathers.js. We will begin by looking at how we can turn our clients into customers, but before we dive in, we first need to set up the stripeClient.

Stripe Client

To set up the Stripe Client, we create a new file called stripe.js in the src. The content is as follows.

import { Application } from './declarations'
import Stripe from 'stripe'

export default function (app) {
  const stripeConf = app.get(`stripe`) # Your stripe config

  const stripe = new Stripe(stripeConf.secret, {
    apiVersion: '2020-03-02'
  })

  app.set('stripeClient', stripe)
}

This will set up your stripe client using the stripe config you have defined, which is then called from your app.js as a configuration.

import stripe from './stripe'

const app = express(feathers())

app.configure(stripe)

Now with this setup, we can start building the payments system!

Turning clients into customers

From the client model view, this is relatively simple. We need to add two new fields to our already existing user schema: the customerId and subscriptionPlanId.

customerId: {
      type: String
},
subscriptionPlanId: {
      type: String,
      required: true
}

Note that the subscriptionPlanId is set to be required.  Whether the customer is supposed to pay or not, you want to have a subscription associated. This allows for easier transitions between subscriptions later on.

Now with the model setup, we want to add some logic to it. Let's create a createStripeCustomer hook function.

function createStripeCustomer () {
  return async (context) => {
    try {
      const {
      	_id,
        email
      } = context.result

      const stripeCustomer = await context.app.service('stripeCustomers').create({
        clientId: _id,
        email
      })

      await context.app.service('clients')._patch(context.result._id, {
        customerId: stripeCustomer.id
      })

      return context
    } catch (e) {
      // remove client to free up the username
      await context.app.service('clients').remove(context.result._id)

      throw e
    }
  }
}

For this function, we extract the email that the user-provided on signup and the assigned _id. We create a customer using the stripeCustomers service that we will define a bit later on. Then we patch the client that we just created with the customerId. In the case that something fails we remove the entire user and return the error. This function is then attached to the clients create.after hook.

The following services are supported by Stripe as well:

Stripe Customer

Now let's look at the stripe customer service. We have a dedicated model for storing information about the customer. The schema is defined as follows.

const schema = new mongooseClient.Schema({
    clientId: {
      type: mongooseClient.Schema.Types.ObjectId,
      required: true,
      ref: 'clients',
      index: true
    },
    id: {
      type: String
    },
    address: {
      city: String,
      country: String,
      line1: String,
      line2: String,
      postal_code: String,
      state: String
    },
    email: String,
    name: String,
    phone: String,
    taxId: {
      type: String
    }

  }, {
    timestamps: true,
    strict: false // IMPORTANT so we save all the fields from stripe
})

Here we store a copy of the customer object from Stripe. For this service, we also define a createStripeCustomer hook function. This function is purely responsible for sending over the required information to Stripe to create a Stripe customer.

const createStripeCustomer = () => async (context) => {
  try {
    const stripeClient: Stripe = context.app.get(`stripeClient`)

    // map fields for protection
    const createCustomerParams: Stripe.CustomerCreateParams = {
      email: context.result.email || undefined,
      name: context.result.name || undefined,
      phone: context.result.phone || undefined,
      preferred_locales: ['pl'],
      invoice_settings: {
        custom_fields: [{
          name: 'NIP',
          value: '118-138-18-96'
        }]
      } as Stripe.CustomerUpdateParams.InvoiceSettings
    }

    // need to test line1, because if address is present, line 1 needs to be as well
    if (
      context.result.address &&
      context.result.address.line1
    ) {
      createCustomerParams.address = {
        city: context.result.address.city || undefined,
        country: context.result.address.country || undefined,
        line1: context.result.address.line1,
        line2: context.result.address.line2 || undefined,
        postal_code: context.result.address.postal_code || undefined,
        state: context.result.address.state || undefined
      }
    }

    const customer = await stripeClient.customers.create(createCustomerParams)

    context.result = await context.app.service('stripeCustomers')._patch(context.result._id, customer)
  } catch (e) {
    logger.error(e)

    // cleanup
    await context.app.service('stripeCustomers').remove(context.result._id)

    throw e
  }
}

You then want to save the result of this request in your stripeCustomers service. In case this fails, remove the stripeCustomer. You are patching the stripeCustomer results in running the after.patch hook, where two methods are present: createTaxId and updateStripeCustomer. First, we will have a look at the createTaxId method.

const createTaxId = () => async (context) => {
  const stripeClient: Stripe = context.app.get(`stripeClient`)

  try {
    if (
      context.data.taxId &&
      context.result.taxId &&
      context.data.taxId === context.result.taxId
    ) {
      if (
        context.result.tax_ids.data &&
        context.result.tax_ids.data.length
      ) {
        await stripeClient.customers.deleteTaxId(
          context.result.id,
          context.result.tax_ids.data[0].id
        )
      }
      await stripeClient.customers.createTaxId(
        context.result.id,
        {
          type: 'eu_vat',
          value: `PL${context.result.taxId}`
        }
      )
    }

    if (
      context.data.taxId === '' &&
      context.data.taxId === context.result.taxId &&
      context.result.tax_ids.data &&
      context.result.tax_ids.data.length
    ) {
      await stripeClient.customers.deleteTaxId(
        context.result.id,
        context.result.tax_ids.data[0].id
      )
    }
  } catch (e) {
    throw new BadRequest(e.message)
  }
}

To create the taxId, we pass the data from the request and send it to stripe using the stripeClient. If the taxId is not present, but it was there earlier, we want to make sure that we remove it from the stripe by calling deleteTaxId. The updateStripeCustomer hook passes all the data that has changed to the stripeClient and requests an update.

const attachOriginalDoc = () => async (context: BeforePatchHookContext<IStripeCustomer>) => {
  context.params._originalDoc = await context.app.service('stripeCustomers').get(context.id)

  return context
}

const updateStripeCustomer = () => async (context: AfterCreateHookContext<IStripeCustomer>) => {
  try {
    const stripeClient: Stripe = context.app.get(`stripeClient`)

    // map fields for protection
    const updateCustomerParams: Stripe.CustomerUpdateParams = {
      email: context.result.email || undefined,
      name: context.result.name || undefined,
      phone: context.result.phone || undefined,
      invoice_settings: {
        default_payment_method: context.result.invoice_settings?.default_payment_method,
        custom_fields: [{
          name: 'NIP',
          value: '118-138-18-96'
        }]
      } as Stripe.CustomerUpdateParams.InvoiceSettings
    }

    // need to test line1, because if address is present, line 1 needs to be as well
    if (
      context.result.address &&
      context.result.address.line1
    ) {
      updateCustomerParams.address = {
        city: context.result.address.city || undefined,
        country: context.result.address.country || undefined,
        line1: context.result.address.line1,
        line2: context.result.address.line2 || undefined,
        postal_code: context.result.address.postal_code || undefined,
        state: context.result.address.state || undefined
      }
    }

    const customer = await stripeClient.customers.update(context.result.id, updateCustomerParams)

    context.result = await context.app.service('stripeCustomers')._patch(context.result._id, customer)
  } catch (e) {
    logger.error(e)

    await context.app.service('stripeCustomers')._patch(context.result._id, context.params._originalDoc)

    throw e
  }
}

Finally, for the customer service, we need to handle deleting the customer. This is done on an after.remove hook. This method is relatively straightforward since for removal just the id is required.

const deleteStripeCustomer = () => async (context) => {
  try {
    const stripeClient: Stripe = context.app.get(`stripeClient`)
    const customer = await context.app.service('stripeCustomers').find(context.params)

    if (customer?.data?.[0]?.id) {
      await stripeClient.customers.del(customer?.data?.[0]?.id)
    }

    return context

  } catch (e) {
    logger.error(e)

    throw e
  }
}

Thus the hooks for the stripeCustomer can be summarised as follows.

{
  before: {
    remove: [
      deleteStripeCustomer()
    ]
  },

  after: {
    create: [
      createStripeCustomer()
    ],
    patch: [
      createTaxId(),
      updateStripeCustomer()
    ]
  }
}

This concludes the customers part. Now we can move on to some more exciting details, which are the stripeEvents!

Handling Stripe Events

As mentioned at the beginning Stripe Events are how Stripe send information to your app. The model for this service is pretty simple.

const schema = new Schema({
    id: {
      type: String,
      required: true,
      unique: true // IMPORTANT - secures so that the same event does not get processed more then once
    }
  }, {
    timestamps: true,
    strict: false // so anything sent in stripe event can be saved in db - this collection is mostly for logging the events
  })

We only store a generated id for this event, making sure that we don't process the event more than once. This service has only one hook on after.create. There we create an Agenda job, which is a lightweight job scheduling library. Using a job scheduler allows you to quickly respond to the webhook and leave the event's processing to some later time when the app has some free resources.

Agenda Processors

We define the agenda jobs as follows.

agenda.define('stripe', async (job) => {
    const jobData = job.attrs.data

    const handle = _.get(events, jobData.type)

    if (handle) {
      return handle(app)(jobData.data.object)
    }

    throw new Error(`No handle specified for '${jobData.type}' WebHook event type`)
  })

Here for each event, you define a processor. Here we will look at the most basic three processors that you will need to get yourself going. Let's define an events/index.js which contains a mapping from stripe events to the processor methods.

const events = {
  'invoice.payment_succeeded': invoicePaymentSucceeded,
  'customer.subscription.updated': customerSubscriptionUpdated,
  'customer.subscription.deleted': customerSubscriptionDeleted
}

These processors manage three types of events. The success of a payment, updating a customers subscription and deleting a customers subscription. When such an event is sent to you from Stripe, Agenda will create a job with the corresponding processor, and it will be queued for later. Let's now look at the payment succeeded processor.

const invoicePaymentSucceeded = (app) => async (data) => {
  const stripeClient: Stripe = app.get(`stripeClient`)

  if (!data.subscription) {
    throw new Error('subscription not specified')
  }

  let subscription

  if (typeof data.subscription === 'string') {
    subscription = await stripeClient.subscriptions.retrieve(data.subscription)
  } else {
    subscription = data.subscription
  }

  if (!subscription) {
    throw new Error('subscription not found')
  }

  let customer

  if (typeof data.customer === 'string') {
    customer = await stripeClient.customers.retrieve(data.customer)
  } else {
    customer = data.customer
  }

  if (!customer) {
    throw new Error('customer not found')
  }

  const clientRes = await app.service('clients').find({
    query: {
      customerId: customer.id
    }
  })

  if (clientRes.total !== 1) {
    throw new Error('Client not found')
  }
  
  // Update user access
  if (
    data.lines.data[0] &&
    data.lines.data[0].plan
  ) {
    const interval = data.lines.data[0].plan.interval
    let accessExpiryDate = null

    switch (interval) {
      case 'month': {
        accessExpiryDate = addDays(subscription.current_period_end * 1000, 3)
        break
      }
      case 'year': {
        accessExpiryDate = addDays(subscription.current_period_end * 1000, 14)
        break
      }
    }

    if (accessExpiryDate) {
      await app.service('clients').patch(
        clientRes.data[0]._id,
        {
          accessExpiryDate
        }
      )
    }
  }
  
  // Send Email Notification
}

The function above first verifies that all the necessary data is provided if everything is ok. The user's accessExpiryDate is extended to a later period. If the user fails to pay, the accessExpiryDate is not updated and thus the access is revoked. The app can similarly handle the remaining events.

Conclusion

This concludes the process of setting up payments using Stripe.js in Feathers.js. We have explored the Stripe API's key concepts and looked into how they can integrate into the Feathers.js framework. Using this approach, it is straightforward to allow automated payments in your app, allowing you to scale faster! Be witty and for the payment service use the best Feathers service for Stripe!