Bundle JSON
The JSON structure of a bundle (version 1), for imports and for anyone who builds a bundle outside the screen.
Last updated on
The Extra invoice lines card saves a bundle as JSON on the product, under the key invoiceBundle. For imports you use the same structure. For normal use you do not need this page: the fields and their explanation are on Bundle steps.
Structure
| Field | Meaning |
|---|---|
invoiceBundle.version | Always 1 |
invoiceBundle.steps | List of steps, at most 40. The order is the order of the child lines on the invoice |
Every step is either a product step or an info step.
Product step
| Field | Meaning |
|---|---|
productId | An existing product in your organisation. Required |
quantity | A number greater than 0, default 1. The quantity of the line without a formula, or when the formula returns true |
quantityScale | Optional. "anchor_line_quantity" multiplies every calculated quantity by the quantity of the anchor line |
description | Optional. Text for the invoice line instead of the product name; a formula can give each line its own description |
style | Optional. DEFAULT or INFO |
billOncePerInvoice | Optional. true keeps the step with the first anchor line when an invoice has several anchor lines |
extendAccessMethod | Optional. { "daysAfterPeriodEnd": <integer, 0 to 3660> }, see NFC extension |
formula | Optional. TypeScript that decides which lines the step produces, see Formula |
Info step
{
"stepKind": "info",
"description": "Inclusief toeristen-, gemeente- en provincietaksen"
}description is required and an info step has no productId. With style (DEFAULT or INFO) you choose the line style; the default is INFO. A formula that returns false leaves out the info line.
Formula
formula is TypeScript that, when an invoice is saved, returns true (one line with quantity), false (no line), a number (the quantity) or a list of lines { "quantity", "description", "unitPrice", "accessMethodValue" }. Which fields and lists you can read and what each form does is described under Formula per step. A formula that does not compile is rejected when the product is saved. In JSON the formula is a single string with \n for line breaks.
"formula": "return {customer.customFields.exemptFromSurcharges} !== true;"NFC extension
Only useful if the main product has Extend Occupation Period as an action.
"extendAccessMethod": { "daysAfterPeriodEnd": 20 }When a line from such a step is paid, Tillor sets the validity of the customer's access methods to the end date of the anchor line plus daysAfterPeriodEnd calendar days. By default this runs until 23:59 in the organisation's time zone, or until the weekday and time of Extend access method on the main product. Which types and statuses count is also set there. Without an end date on the anchor line, nothing happens. With Do not shorten on extend switched on, Tillor skips methods whose validity is already on or after the new date. An accessMethodValue on the line names the method of that line; the extension itself follows the settings of the main product.
After a successful extension, the invoice timeline shows a line that refers to the payment.
Example: explanation, NFC surcharge, meter rental and dogs
{
"invoiceBundle": {
"version": 1,
"steps": [
{
"stepKind": "info",
"description": "Inclusief toeristen-, gemeente- en provincietaksen"
},
{
"productId": "prd_nfc_toeslag",
"quantity": 1,
"quantityScale": "anchor_line_quantity",
"extendAccessMethod": { "daysAfterPeriodEnd": 20 },
"style": "INFO",
"billOncePerInvoice": true,
"formula": "const nfc = {accessMethods}.filter((a) => a.type === \"NFC\" && a.status === \"ACTIVE\");\nreturn nfc.slice(0, 1).map((a) => ({ quantity: 0, unitPrice: 0, accessMethodValue: a.value, description: `Toegangsmethode inclusief: ${a.label}` }));"
},
{
"productId": "prd_nfc_toeslag",
"quantity": 1,
"quantityScale": "anchor_line_quantity",
"extendAccessMethod": { "daysAfterPeriodEnd": 20 },
"billOncePerInvoice": true,
"formula": "const nfc = {accessMethods}.filter((a) => a.type === \"NFC\" && a.status === \"ACTIVE\");\nreturn nfc.slice(1).map((a) => ({ quantity: 1, accessMethodValue: a.value, description: `Toeslag extra toegangsmethode: ${a.label}` }));"
},
{
"productId": "prd_meterhuur",
"quantity": 1,
"quantityScale": "anchor_line_quantity",
"formula": "return {meters}.filter((m) => m.provider === \"ICY\" && m.type === \"ELECTRICITY\" && m.icyMetadata?.switchState === true).map((m) => ({ quantity: 1, description: `Huur digitale elektriciteitsmeter ${m.name}` }));"
},
{
"productId": "prd_hond",
"quantity": 1,
"quantityScale": "anchor_line_quantity",
"billOncePerInvoice": true,
"formula": "const dogs = Math.max(0, Math.floor(Number({customer.customFields.animals.dogs}) || 0));\nreturn dogs === 0 ? false : [{ quantity: dogs, description: `Honden (${dogs})` }];"
}
]
}
}Note
Check that all productId values on product steps exist in your catalogue. Tillor skips steps with an unknown product.