kalisio/kBilling

View on GitHub
src/services/billing/billing.service.js

Summary

Maintainability
D
1 day
Test Coverage
A
98%
import makeDebug from 'debug'
import _ from 'lodash'
import { disallow } from 'feathers-hooks-common'
import { Customer, Card, Subscription } from 'feathers-stripe'
import { BadRequest } from '@feathersjs/errors'

const debug = makeDebug('kalisio:kBilling:billing:service')

const propertiesToOmit = ['billingObject', 'billingObjectService', 'billingPerspective', 'action']

function toStripeCustomerData (data) {
  if (_.isNil(data.email)) throw new BadRequest('createCustomer: missing \'email\' parameter')
  return {
    email: data.email,
    description: _.get(data, 'description', ''),
    business_vat_id: _.get(data, 'vatNumber', '')
  }
}

export default function (name, app, options) {
  const config = app.get('billing')
  return {
    async createStripeSubscription (customerId, planId, billingMethod) {
      debug('create Stripe subscription to ' + planId + ' for ' + customerId + ' [billing method: ' + billingMethod + ']')
      const subscriptionService = app.service('billing/subscription')
      const stripeSubscriptionData = {
        customer: customerId,
        plan: planId,
        billing: billingMethod
      }
      if (billingMethod === 'send_invoice') stripeSubscriptionData.days_until_due = config.daysUntilInvoiceDue
      const stripeSubscription = await subscriptionService.create(stripeSubscriptionData)
      return stripeSubscription.id
    },
    async removeStripeSubscription (customerId, subscriptionId) {
      debug('remove Stripe subscription ' + subscriptionId + ' from ' + customerId)
      const subscriptionService = app.service('billing/subscription')
      await subscriptionService.remove(subscriptionId, { customer: customerId, stripe: {} })
    },
    async createStripeCard (customerId, token) {
      debug('create card ' + token + ' for customer ' + customerId)
      const cardService = app.service('billing/card')
      const card = await cardService.create({ source: token }, { customer: customerId })
      return { stripeId: card.id, brand: card.brand, last4: card.last4 }
    },
    async removeStripeCard (customerId, cardId) {
      debug('remove card from customer ' + customerId)
      const cardService = app.service('billing/card')
      await cardService.remove(cardId, { customer: customerId })
    },
    async updateStripeBillingMethod (subscriptionId, billingMethod) {
      debug('update billing for subscription ' + subscriptionId + ' with method ' + billingMethod)
      const subscriptionData = { billing: billingMethod }
      if (billingMethod === 'send_invoice') subscriptionData.days_until_due = config.daysUntilInvoiceDue
      const subscriptionService = app.service('billing/subscription')
      await subscriptionService.update(subscriptionId, subscriptionData)
    },
    async createCustomer (billingObject, data) {
      const billingObjectId = billingObject._id
      const stripeCustomerData = toStripeCustomerData(data)
      debug('create customer ' + stripeCustomerData.email)
      const customerService = app.service('billing/customer')
      const stripeCustomer = await customerService.create(stripeCustomerData)
      let customerObject = Object.assign({ stripeId: stripeCustomer.id }, _.omit(data, propertiesToOmit))
      if (!_.isNil(_.get(data, 'token', null))) {
        const card = await this.createStripeCard(stripeCustomer.id, data.token)
        customerObject = Object.assign(customerObject, { card: card })
      }
      const billingObjectService = app.getService(data.billingObjectService)
      await billingObjectService.patch(billingObjectId, { 'billing.customer': customerObject })
      return customerObject
    },
    async updateCustomer (billingObject, data) {
      const billingObjectId = billingObject._id
      const customerId = _.get(billingObject, 'billing.customer.stripeId')
      const stripeCustomerData = toStripeCustomerData(data)
      debug('update customer ' + customerId)
      const customerService = app.service('billing/customer')
      const stripeCustomer = await customerService.update(customerId, stripeCustomerData)
      let customerObject = Object.assign({ stripeId: customerId }, _.omit(data, propertiesToOmit))
      if (stripeCustomer.sources.total_count > 0) {
        // do we need to remove the card
        if (_.isNil(data.card)) {
          // Card has been removed
          await this.removeStripeCard(customerId, stripeCustomer.sources.data[0].id)
          // Update subscription if needed
          if (stripeCustomer.subscriptions.total_count > 0) {
            if (_.isNil(data.token)) await this.updateStripeBillingMethod(stripeCustomer.subscriptions.data[0].id, 'send_invoice')
          }
        }
        if (!_.isNil(data.token)) {
          // Card has been replaced
          if (!_.isNil(data.card)) await this.removeStripeCard(customerId, stripeCustomer.sources.data[0].id)
          const card = await this.createStripeCard(customerId, data.token)
          customerObject = Object.assign(customerObject, { card: card })
        }
      } else if (!_.isNil(data.token)) {
        // Card has been added
        const card = await this.createStripeCard(customerId, data.token)
        customerObject = Object.assign(customerObject, { card: card })
        // Update subscription if needed
        if (stripeCustomer.subscriptions.total_count > 0) {
          await this.updateStripeBillingMethod(stripeCustomer.subscriptions.data[0].id, 'charge_automatically')
        }
      }
      const billingObjectService = app.getService(data.billingObjectService)
      await billingObjectService.patch(billingObjectId, { 'billing.customer': customerObject })
      return customerObject
    },
    async removeCustomer (billingObject, query, patch) {
      const billingObjectId = billingObject._id
      const customerId = _.get(billingObject, 'billing.customer.stripeId')
      debug('remove customer: ' + customerId)
      const customerService = app.service('billing/customer')
      await customerService.remove(customerId)
      if (patch) {
        const billingObjectService = app.getService(query.billingObjectService)
        await billingObjectService.patch(billingObjectId, { 'billing.customer': null, 'billing.subscription': null })
      }
    },
    async createSubscription (billingObject, data) {
      const billingObjectId = billingObject._id
      const plan = _.get(data, 'plan')
      if (_.isNil(plan)) throw new BadRequest('createSubscription: missing \'plan\' parameter')
      debug('create subscripton for ' + billingObjectId + ' to plan ' + plan)
      const subscription = { plan: data.plan }
      if (!_.isNil(config.plans[data.plan].stripeId)) {
        const customerId = _.get(billingObject, 'billing.customer.stripeId')
        const customerCard = _.get(billingObject, 'billing.customer.card')
        if (_.isNil(customerId)) throw new BadRequest('updateSubscription: you must create a customer before subscribing to a product')
        let billingMethod = 'send_invoice'
        if (!_.isNil(customerCard)) billingMethod = 'charge_automatically'
        subscription.stripeId = await this.createStripeSubscription(customerId, config.plans[plan].stripeId, billingMethod)
      }
      const billingObjectService = app.getService(data.billingObjectService)
      await billingObjectService.patch(billingObjectId, { 'billing.subscription': subscription })
      return subscription
    },
    async updateSubscription (billingObject, data) {
      const billingObjectId = billingObject._id
      const plan = _.get(data, 'plan')
      if (_.isNil(plan)) throw new BadRequest('updateSubscription: missing \'plan\' parameter')
      debug('update subscripton for ' + billingObjectId + ' to plan ' + plan)
      const customerId = _.get(billingObject, 'billing.customer.stripeId')
      const customerCard = _.get(billingObject, 'billing.customer.card')
      const subscriptionId = _.get(billingObject, 'billing.subscription.stripeId')
      if (!_.isNil(subscriptionId)) {
        if (_.isNil(customerId)) throw new BadRequest('updateSubscription: inconsistent billing perspective')
        await this.removeStripeSubscription(customerId, subscriptionId)
      }
      const subscription = { plan: plan }
      if (!_.isNil(config.plans[plan].stripeId)) {
        if (_.isNil(customerId)) throw new BadRequest('updateSubscription: you must create a customer before subscribing to a product')
        let billingMethod = 'send_invoice'
        if (!_.isNil(customerCard)) billingMethod = 'charge_automatically'
        subscription.stripeId = await this.createStripeSubscription(customerId, config.plans[plan].stripeId, billingMethod)
      }
      const billingObjectService = app.getService(data.billingObjectService)
      await billingObjectService.patch(billingObjectId, { 'billing.subscription': subscription })
      return subscription
    },
    async removeSubscription (billingObject, query, patch) {
      const billingObjectId = billingObject._id
      debug('remove subscription from ' + billingObjectId)
      const customerId = _.get(billingObject, 'billing.customer.stripeId')
      const subscriptionId = _.get(billingObject, 'billing.subscription.stripeId')
      if (!_.isNil(subscriptionId)) {
        if (_.isNil(customerId)) throw new BadRequest('removeSubscription: inconsistent billing perspective')
        await this.removeStripeSubscription(customerId, subscriptionId)
      }
      if (patch) {
        const billingObjectService = app.getService(query.billingObjectService)
        await billingObjectService.patch(billingObjectId, { 'billing.subscription': null })
      }
    },
    setup (app) {
      app.use('/billing/customer', new Customer({ secretKey: config.secretKey }))
      app.service('billing/customer').hooks({ before: { all: disallow('external') } })
      app.use('/billing/card', new Card({ secretKey: config.secretKey }))
      app.service('billing/card').hooks({ before: { all: disallow('external') } })
      app.use('/billing/subscription', new Subscription({ secretKey: config.secretKey }))
      app.service('billing/subscription').hooks({ before: { all: disallow('external') } })
    },
    // Used to perform service actions such as create a customer/subscription.
    async create (data, params) {
      debug(`billing service called for create action=${data.action}`)
      switch (data.action) {
        case 'customer': {
          const customer = await this.createCustomer(params.billingObject, data)
          return customer
        }
        case 'subscription': {
          const subscription = await this.createSubscription(params.billingObject, data)
          return subscription
        }
      }
    },
    // Used to perform service actions such as update a customer/subscription
    async update (id, data, params) {
      debug(`billing service called for update action=${data.action}`)
      switch (data.action) {
        case 'customer': {
          const customer = await this.updateCustomer(params.billingObject, data)
          return customer
        }
        case 'subscription': {
          const subscription = await this.updateSubscription(params.billingObject, data)
          return subscription
        }
      }
    },
    // Used to perform service actions such as remove a customer/subscription
    async remove (id, params) {
      const query = params.query
      debug(`billing service called for remove action=${query.action}`)
      switch (query.action) {
        case 'customer':
          await this.removeCustomer(params.billingObject, query, _.get(params, 'patch', true))
          break
        case 'subscription':
          await this.removeSubscription(params.billingObject, query, _.get(params, 'patch', true))
          break
      }
    }
  }
}