The ServiceTrade Accounting Connector uses "recipes" -- customized templates that apply business rules to ServiceTrade data to create a import file in the format that the target accounting system requires.  This article describes how to create a recipe to represent those business rules.

Recipe format

An Accounting Connector recipe is a JavaScript object that defines the input, internal processing rules, and outputs of that recipe. The basic rules are:

  • A recipe must have exactly one input, which may be a file or an API request response.
  • A recipe can have internal processing rules (text manipulations, API requests, etc) that apply to the whole input dataset, to the whole output dataset, or to each row of the dataset.
  • A recipe must have one or more outputs, which can be text files or API requests.

By convention, properties added by internal processing rules are named with a leading underscore, such as _purchase_order_object. This makes it easy to distinguish between properties that were part of the original dataset ("normal" names) and properties added by recipe processing (underscored names). This convention isn't required, and isn't enforced or respected by the Accounting Connector.

The recipe processing workflow is:

input -> preProcess ->
[ FOR EACH ROW:   eachPreFetch -> eachFetch -> eachPreProcess -> output mapping -> eachPostProcess ] ->
postProcess -> output

Referring to nested properties

Recipe fetch URLs and recipe outputs can refer to nested properties within non-scalars.

References to properties within objects are made with dot-naming. For instance, given the following object:

{id: 456, purchase_order: {number: 123}, status: 'pending'}

...a URL like /api/purchase_order={{purchase_order.number}} would resolve to /api/purchase_order=123.

References to properties within arrays are made with zero-indexed dot-naming: the first member of an array is named 0, the second is named 1, and so forth. For instance, given the following object:

{purchase_orders: [
{id: 456, poNumber: 123, status: 'pending'},
{id: 789, poNumber: 321, status: 'closed'}
]}

...a URL like /api/purchase_order={{purchase_orders.0.id}} would resolve to /api/purchase_order=456.

Recipe contents

input (REQUIRED)

An object containing a type. Supported types are: file, fetch.

The file input type has no additional parameters. The input format is expected to be CSV, with the first row providing headers and all subsequent rows providing values. Each row of values is treated as an input row.

Example:

input: {
type: 'file'
}

The fetch input type has the following additional parameters:

  • api: The API to which the request should be made. Currently supported APIs are: 'partsledger', 'servicetrade', 'quickbooks'
  • request: The relative URL of the API request. Handlebars-style variables are supported in the request URL.
  • params: An array of parameters that will be used to compose the request. All parameters should be presented to the user as required fields. Each parameter has the following properties:
  • type: One of: text, date
  • name: The name of the field (will be used for creating the variable for the request URL)
  • display: The display name of the field. If not provided, a pretty-cased version of the name property should be displayed.
  • default: If provided, the default value of the input field. Dates must be formatted in ISO 8601 format (example: 2018-05-01).
  • options: If provided for a text field, this defines the set of possible values for this field (which should be displayed as a select box or radio buttons to the user). Ignored if provided for any other type of field. Options can be either an array of values, or an array of objects with the following properties:
  • value: The option value
  • name: The option name. If not provided, the value should be displayed.
  • preFetch: An array containing zero or more functions that will be applied to input object BEFORE the fetch is performed, but AFTER input parameters are applied. The functions are processed in order, and the input to each function is the output of the preceding function. (The input to the first function is the original value of the input object, with the user-supplied input parameter values interleaved into the params array as the value property of each array member). Prefetch steps can be used to convert relative date strings (example: 'yesterday', 'today') into date values, etc.

Fetch URLs can contain Handlebars-style variables, which it will resolve to actual values before performing the API request, using the user-inputted params data.

Fetch input type requests return an array of results, each of which is treated as an input row.

Example:

input: {
type: 'fetch',
api: 'servicetrade',
request: '/job?number={{job_number}}&status={{job_status}}&appointmentBeginsAfter={{date_start}}&appointmentBeginsBefore={{date_end}}',
params: [
{type: 'text', name: 'job_number', display: 'Job No'},
{type: 'date', name: 'date_start', default: '2019-05-01'},
{type: 'date', name: 'date_end'},
{type: 'text', name: 'job_completed', display: 'Job Completed', options: ['Yes', 'No']},
{type: 'text', name: 'job_status', display: 'Job Status', default: 'open', options: [
{name: 'Completed', value: 'complete'},
{value: 'open'}
]}
],
preFetch: [
function convertStartDateToTimestamp(data) {
data.params.forEach(p => {
if (p.name == 'date_start') {
p.value = Moment(p.value + ' 00:00:00').format('X');
}

})
return data;
}
]

preProcess

An array containing zero or more functions that will be applied to the entire dataset. The functions are processed in order, and the input to each function is the output of the preceding function. (The input to the first function is the original contents of the input).

Each function takes a single input parameter that contains the row of data, and should return the (transformed) row as output.

Example:

preProcess: [
function filterEvens(input) {
let output = input.filter(function(i) {
return i.rowNumber % 2 == 0;
});
return output;
}
]

eachPreFetch

An array containing zero or more functions that will be applied to the row of data BEFORE fetches are performed. The functions are processed in order, and the input to each function is the output of the preceding function. (The input to the first function is the contents of the row after preProcess steps are completed.)

Each function takes a single input parameter that contains the whole dataset, and should return the entire (transformed) dataset as output.

Example:

eachPreFetch: [
function lowercaseName(row) {
row.name = row.name.toLowerCase();
return row;
}
]

eachFetch

An array containing zero or more API request definitions. Only GET API requests are supported (hence 'fetch'). Each fetch definition has the following properties:

  • api: The API to which the request should be made. Currently supported APIs are: 'partsledger', 'servicetrade', 'quickbooks'
  • request: The relative URL of the API request. Handlebars-style variables are supported in the request URL.
  • output: The name of the object that will contain the API request's output.

Recipe fetch URLs can contain Handlebars-style variables, which it will resolve to actual values before performing the API request, using the current row's data.

Fetches are processed in order, and the input to each fetch is the row after the previous fetch was applied to it. This allows you to chain together API calls so that later requests can use the responses of earlier requests.

Example:

eachFetch: [
{
api: 'partsledger',
request: '/v1/order?order_type=2&order_number={{_purchase_order_number}}',
output: '_purchase_order_object'
},
{
api: 'servicetrade',
request: '/job?number={{_purchase_order_object.order.order_items.0.order_item.work_orders.0.work_order.internal_reference_number}}',
output: '_job_object'
}
]

In this example, the output of the first API request (to PartsLedger) is placed to a property on the row named _purchase_order_object, and that row (which now includes the newly added property) is given as input to the second API request, which then uses the contents of _purchase_order_object to resolve the second API request's URL.

eachPreProcess

An array containing zero or more functions that will be applied to the row of data AFTER fetches are performed, but BEFORE output mapping is performed. The functions are processed in order, and the input to each function is the output of the preceding function. (The input to the first function is the contents of the row after all fetches are performed).

Each function takes a single input parameter that contains the whole dataset, and should return the entire (transformed) dataset as output.

Example:

eachPreProcess: [
function capitalizeName(row) {
row.name = row.name.toUpperCase();
return row;
}
]

outputs (REQUIRED)

An object containing one or more output mapping definitions. Each output definition has the following properties:

  • type: Currently file, quickbooks, and servicetrade are supported. For file outputs, the output format is assumed to be key/value-ish (such as CSV). You can transform key/value-style outputs to other formats (such as XML) with postProcess steps.
  • name: The name of the output file.
  • request: The output API request URL (servicetrade and quickbooks outputs only)
  • method: The output API request method. Valid options are: 'put', 'post'. (servicetrade output only; quickbooks method is always 'post')
  • contents: An object containing key/value pairs, where the keys are the names of the output columns, and the values are the values in those columns for each row.

Each contents element value can be one of:

  • A value from the row's data object. See 'Referring to nested properties' above for details on referring to nested properties of objects and arrays. Unlike fetch URLs, you do NOT need to wrap property references in Handlebars-style curly braces in output content definitions.
  • A literal string. Literal strings are wrapped in backticks (`).

Example:

outputs: {
main: {
name: 'output-1.csv',
type: 'file',
contents: {
"Description": '_purchase_order_object.order.order_items.0.order_item.work_orders.0.work_order.internal_reference_number',
"Job Type": '_job_object.type',
"Account": '`Expenses`',
"GL Period": '_glPeriod',
}
}
}

The result of the output mapping for each row is an object containing subobjects for each of the defined outputs.

eachPostProcess

An array containing zero or more functions that will be applied to the row of data AFTER fetches and output mapping are performed. The functions are processed in order, and the input to each function is the output of the preceding function. (The input to the first function is the output of the row from the outputs mapping).

Each function takes a single input parameter that contains the output object for that row (with subobjects for each of the recipe's defined outputs), and should return the entire (transformed) row output object as output.

Example:

eachPostProcess: [
function randomizeName(rowObject) {
rowObject.main.name = rowObject.main.name + (Math.random() * 1000);
return rowObject;
}
]

postProcess

An array containing zero or more functions that will be applied to the entire output object. The functions are processed in order, and the input to each function is the output of the preceding function. (The input to the first function is output object after all rows' postprocessing is complete).

The output object is an object containing subobjects, one subobject for each output defined in outputs. The value of each subobject is an array of rows representing the data for that output.

Each function takes a single input parameter that contains the output object, and should return the (transformed) output object as output.

Example:

postProcess: {
function filterHighNumbers(outputs) {
outputs.main.contents = outputs.main.contents.filter(function(o) {
return o.transaction_id < 500;
});
return outputs;
}
},

Postprocessing functions should be use to remove unneeded output rows, transform key/value output into another format (XML, headerless text file such as IIF), etc.

resources

An object containing resources that can be used in any step of the processing workflow. These resources can be scalar values, arrays, objects, or functions. Any resources that need to be re-used across several different steps the processing workflow should be placed here, to eliminate code duplication and make the recipe simpler. Resources are available as properties of the recipe.resources object.

Recipe functions also have access to the following JavaScript libraries:

Example:

resources: {
validStatuses: ['open', 'closed'],
capitalize: function(string) {
return string.toUpperCase();
}
},
eachPreProcess: [
function capitalizeName(row) {
row.name = recipe.resources.capitalize(row.name);
return row;
},
function checkValidStatus(row) {
row._isStatusValid = recipe.resources.validStatuses.indexOf(row.status) !== -1;
return row;
},
function convertDate(row) {
row._prettyDate = Moment(row.transaction_date).format("YYYY-MM-DD");
return row;
}
]

Logging

Recipes can record log entries by calling the included RecipeLogger function, which takes a log string value as input. Example:

eachPreProcess: [
function capitalizeName(row) {
row.name = recipe.resources.capitalize(row.name);
RecipeLogger("Name after capitalization: " + row.name);
return row;
}
]
Did this answer your question?