Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
141 lines (109 sloc) 7.75 KB
---
title: How To: Add a Tax Service
---

Prerequisite Reading

Overview

In general, to add a tax service you must do the following:

  • Create a plugin or modify an existing one
  • Create and register a tax calculation function
  • Optionally create and register a function that returns a list of valid item tax codes
  • If necessary, extend the GraphQL API with mutations, queries, and types needed for operator UI
  • If necessary, create a React component for operators to enter and edit settings for your tax service

There is one included plugin that provides a "Custom Rates" tax service (taxes-rates in /imports/plugins/included). Examine the files in this plugin if you are confused by any of the steps in this article.

Register a tax service

Tax services are registered by passing an array of them to the taxServices property of the Reaction.registerPackage options.

import Reaction from "/imports/plugins/core/core/server/Reaction";
import calculateOrderTaxes from "./server/no-meteor/util/calculateOrderTaxes";
import getTaxCodes from "./server/no-meteor/util/getTaxCodes";

Reaction.registerPackage({
  label: "Custom Rates",
  name: "reaction-taxes-rates",
  autoEnable: true,
  taxServices: [
    {
      displayName: "Custom Rates",
      name: "custom-rates",
      functions: {
        calculateOrderTaxes,
        getTaxCodes
      }
    }
  ]
  // ...
});

See below for how to create the two functions.

Create a tax calculation function

Every tax service is expected to register a function that calculates taxes for a single CommonOrder. The core taxes plugin calls it like this:

const taxServiceResult = await primaryTaxService.functions.calculateOrderTaxes({ context, order });

One or more plugins can provide one or more tax services. Each shop can choose one service as a primary tax service and another as a fallback service. This is done by an operator in the General Tax Settings panel.

A fallback service is used to calculate the tax when the primary service returns a null result. This could be due to errors from the plugin configuration or a network failure. The tax plugin is expected to handle such errors and returns a null result when appropriate to allow the fallback service to kick in.

The return from a calculateOrderTaxes function is expected to be similar to this:

{
  // Taxes grouped by item
  itemTaxes: [
    {
      customFields: {}, // Optionally, anything else you need to store per item
      itemId: "abc", // Must match `_id` from one of the `order.items`
      tax: 1, // Total amount due for this item, for all types of tax combined, in `order.currencyCode`
      taxableAmount: 1, // Amount of `item.subtotal` that was deemed subject to taxation, in `order.currencyCode`
      taxes: [] // Breakdown of all taxes that apply to this item
    }
  ],
  // Combined taxes
  taxSummary: {
    calculatedAt: new Date(), // The time at which this calculation happened
    calculatedByTaxServiceName: "my-tax-service", // optionally, provide a name that will be stored on the final order, allowing you to identify which orders this service calculated taxes for and mark the order complete in your external tax service if required
    customFields: {}, // Optionally, anything else you need to store per order fulfillment group
    referenceId: "123", // Optional ID to tie this calculation back to an external system
    tax: 1, // Total amount due for all items, for all types of tax combined, in `order.currencyCode`
    taxableAmount: 1, // Amount of the order total that was deemed subject to taxation, in `order.currencyCode`
    taxes: [] // Breakdown of all taxes that apply to any item, with combined values for all items they applied to
  }
}

Where each item in the taxes arrays has this shape:

{
  _id: "123", // Generate a random unique ID if you don't have one
  jurisdictionId: "123", // Optionally provide an ID for the jurisdiction this tax is for. Not currently used by core.
  sourcing: "destination", // Either "destination" or "origin" depending on which address triggered this tax
  tax: 1, // Total amount due for this type of tax only, in `order.currencyCode`
  taxableAmount: 1, // Total deemed taxable for this type of tax only, in `order.currencyCode`
  taxName: "CA Sales Tax", // A human-readable string for showing to operators and customers in the UI
  taxRate: 3.5, // The tax rate for this type of tax, used for this calculation
  customFields: {} // Optionally, anything else you need to store per tax line item
}

If you are integrating with a third-party tax service, you will typically get back a result similar to this. You simply need to map the result to the return shape expected.

The calculateOrderTaxes function is called frequently, every time a cart changes and whenever an order is placed. Your function should return null if it's called with a CommonOrder that does not yet have enough information on it to calculate taxes. However, if order.sourceType is "order" and you still do not have enough information to calculate, then something is wrong. It may be wise to throw an error in this case, and in a production system you'll want to track such events and get alerts if this happens.

External tax APIs often require various addresses with various names. Do your best to provide what they require, but the shipping address is usually the most important. Reaction does not guarantee that all orders will have shipping or billing addresses. If you have one or the other, it is usually fine to substitute billing for shipping or vice versa in order to at least get a calculation and allow the order to be placed. If an order has neither address, you'll need to decide what action is proper based on the third-party API. There should always be an originAddress provided, and that may be enough to do a calculation in some cases.

customFields

Anything you store on customFields is not exposed through GraphQL by default. If you need any of the fields available on Cart or Order through GraphQL, you can create a custom plugin to extend type CalculatedTax with your properly typed custom fields and add resolvers as necessary.

Primarily, though, this is intended to be used to store extra data that the third-party tax integrations need for later API calls.

Create a tax codes function

Tax services need not provide a tax codes function, but if you don't, shop operators will need to enter free text tax codes without a selection list. A tax codes function simply returns the current list of tax codes, with a code and label for each.

export default async function getTaxCodes() {
  return [{
    code: "RC_TAX",
    label: "Taxable (RC_TAX)"
  }];
}

This is a simple example, but you will likely need to query the third-party tax service and return that list mapped to this expected shape.

Extend the GraphQL API

This is necessary only for any operator actions you need to support, such as getting and modifying settings. Review How To: Create a new GraphQL query and How To: Create a new GraphQL mutation. Example queries and mutations:

  • updateMyTaxServiceSettings (mutation)
  • createTaxRule (mutation)
  • updateTaxRule (mutation)
  • deleteTaxRule (mutation)
  • myTaxServiceSettings (query)
  • taxRules (query)

Settings React Component

We recommend that you use the Form component from the ReactoForm package along with Reaction Design System fields and inputs. Use a container pattern for injecting data using higher order components that do GraphQL requests with Apollo Client. Example: /imports/plugins/core/taxes/client/containers/GeneralTaxSettings.js

You can’t perform that action at this time.