@oneblink/sdk-core 10.1.0-beta.2 → 10.1.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,30 @@
1
1
  import { FormTypes, SubmissionTypes } from '@oneblink/types';
2
2
  import { type ParseDayOnlyDate } from './conditionalLogicService/types.js';
3
3
  export type { ParseDayOnlyDate } from './conditionalLogicService/types.js';
4
+ export type EvaluateExpressionResult = {
5
+ type: 'RESULT';
6
+ value: number;
7
+ } | {
8
+ type: 'MISSING_VALUES';
9
+ } | {
10
+ type: 'INVALID_EXPRESSION';
11
+ error: Error;
12
+ };
13
+ /**
14
+ * Find `{ELEMENT:...}` references in an expression whose name paths do not
15
+ * exist in the form definition.
16
+ *
17
+ * Intended for server-side payment validation. Skip this on the client during
18
+ * interactive calculation evaluation — it walks the form definition for every
19
+ * referenced element and is unnecessary for display.
20
+ *
21
+ * @returns Missing element name paths (e.g. `Amount` or `Items|Amount`), or an
22
+ * empty array when every referenced element exists.
23
+ */
24
+ export declare function findMissingFormElementsInExpression({ expression, formElements, }: {
25
+ expression: string;
26
+ formElements: FormTypes.FormElement[];
27
+ }): string[];
4
28
  /**
5
29
  * Evaluate a OneBlink calculation expression against form submission data.
6
30
  *
@@ -72,7 +96,7 @@ export type { ParseDayOnlyDate } from './conditionalLogicService/types.js';
72
96
  * `parseFloat`
73
97
  * - Arrays of numeric strings (e.g. checkboxes) are summed
74
98
  * - Repeatable set entries sum the referenced nested numeric values
75
- * - Empty arrays yield `NaN` (calculation result becomes `undefined`)
99
+ * - Empty arrays yield `NaN` (result type `MISSING_VALUES`)
76
100
  * - Compliance element objects use their `value` property
77
101
  *
78
102
  * ##### Built-in functions
@@ -90,14 +114,14 @@ export type { ParseDayOnlyDate } from './conditionalLogicService/types.js';
90
114
  * ##### Examples
91
115
  *
92
116
  * ```js
93
- * const value = calculationService.evaluateExpression({
117
+ * const result = calculationService.evaluateExpression({
94
118
  * expression: '{ELEMENT:Quantity} * {ELEMENT:Price}',
95
119
  * submission: { Quantity: 3, Price: 12.5 },
96
120
  * formElements: form.elements,
97
121
  * // Client: local timezone. Server: organisation timezone.
98
122
  * parseDayOnlyDate: (value) => new Date(`${value}T00:00:00.000Z`),
99
123
  * })
100
- * // value === 37.5
124
+ * // result === { type: 'RESULT', value: 37.5 }
101
125
  * ```
102
126
  *
103
127
  * ```js
@@ -107,7 +131,7 @@ export type { ParseDayOnlyDate } from './conditionalLogicService/types.js';
107
131
  * formElements: form.elements,
108
132
  * parseDayOnlyDate: (value) => new Date(`${value}T00:00:00.000Z`),
109
133
  * })
110
- * // 110
134
+ * // { type: 'RESULT', value: 110 }
111
135
  * ```
112
136
  *
113
137
  * ```js
@@ -117,13 +141,14 @@ export type { ParseDayOnlyDate } from './conditionalLogicService/types.js';
117
141
  * formElements: form.elements,
118
142
  * parseDayOnlyDate: (value) => new Date(`${value}T00:00:00.000Z`),
119
143
  * })
120
- * // 5
144
+ * // { type: 'RESULT', value: 5 }
121
145
  * ```
122
146
  *
123
147
  * @param options
124
- * @returns The evaluated numeric result, or `undefined` when evaluation does
125
- * not yield a valid number (including `NaN`). Throws when the expression
126
- * cannot be parsed.
148
+ * @returns A discriminated result describing the outcome of evaluation. Empty
149
+ * expressions and parse failures return
150
+ * `{ type: 'INVALID_EXPRESSION', error }`. Unexpected runtime errors are
151
+ * rethrown.
127
152
  */
128
153
  export declare function evaluateExpression({ expression, submission, formElements, parseDayOnlyDate, }: {
129
154
  /** The calculation expression string to evaluate */
@@ -132,7 +157,7 @@ export declare function evaluateExpression({ expression, submission, formElement
132
157
  submission: SubmissionTypes.S3SubmissionData['submission'];
133
158
  /**
134
159
  * Form elements definition. Used to resolve nested `form` element values
135
- * referenced in the expression.
160
+ * referenced in the expression and to validate `{ELEMENT:...}` references.
136
161
  */
137
162
  formElements: FormTypes.FormElement[];
138
163
  /**
@@ -141,4 +166,4 @@ export declare function evaluateExpression({ expression, submission, formElement
141
166
  * `new Date(value)`.
142
167
  */
143
168
  parseDayOnlyDate: ParseDayOnlyDate;
144
- }): number | undefined;
169
+ }): EvaluateExpressionResult;
@@ -1,5 +1,5 @@
1
1
  import ExpressionParser from 'morph-expressions';
2
- import { findFormElement, matchElementsTagRegex, } from './formElementsService.js';
2
+ import { findFormElement, flattenFormElements, matchElementsTagRegex, } from './formElementsService.js';
3
3
  import { DATE_ONLY_PATTERN, } from './conditionalLogicService/types.js';
4
4
  const isUnenteredValue = (value) => {
5
5
  return !value && value !== 0;
@@ -22,6 +22,59 @@ function roundToFixed(number, decimals) {
22
22
  const roundedNumber = Math.round(number * multiplier) / multiplier;
23
23
  return roundedNumber.toFixed(decimals);
24
24
  }
25
+ function findFormElementByNamePath(formElements, elementName) {
26
+ const parts = elementName.split('|');
27
+ let searchElements = formElements;
28
+ let found;
29
+ for (let i = 0; i < parts.length; i++) {
30
+ const name = parts[i];
31
+ // Match flattenFormElements: page/section are transparent for naming, but
32
+ // form/repeatableSet/infoPage require an explicit parent path segment.
33
+ found = flattenFormElements(searchElements).find((element) => 'name' in element && element.name === name);
34
+ if (!found) {
35
+ return;
36
+ }
37
+ if (i < parts.length - 1) {
38
+ if (!('elements' in found) || !found.elements) {
39
+ return;
40
+ }
41
+ searchElements = found.elements;
42
+ }
43
+ }
44
+ return found;
45
+ }
46
+ /**
47
+ * Find `{ELEMENT:...}` references in an expression whose name paths do not
48
+ * exist in the form definition.
49
+ *
50
+ * Intended for server-side payment validation. Skip this on the client during
51
+ * interactive calculation evaluation — it walks the form definition for every
52
+ * referenced element and is unnecessary for display.
53
+ *
54
+ * @returns Missing element name paths (e.g. `Amount` or `Items|Amount`), or an
55
+ * empty array when every referenced element exists.
56
+ */
57
+ export function findMissingFormElementsInExpression({ expression, formElements, }) {
58
+ if (!expression) {
59
+ return [];
60
+ }
61
+ const elementNames = [];
62
+ matchElementsTagRegex(expression, ({ elementName }) => {
63
+ elementNames.push(elementName);
64
+ });
65
+ const missingFormElementNames = [];
66
+ const seen = new Set();
67
+ for (const elementName of elementNames) {
68
+ if (seen.has(elementName)) {
69
+ continue;
70
+ }
71
+ seen.add(elementName);
72
+ if (!findFormElementByNamePath(formElements, elementName)) {
73
+ missingFormElementNames.push(elementName);
74
+ }
75
+ }
76
+ return missingFormElementNames;
77
+ }
25
78
  function resolveElementValue({ submission, nestedElementNames, formElements, parseDayOnlyDate, }) {
26
79
  const defaultAccumulator = submission[nestedElementNames[0]];
27
80
  return nestedElementNames.reduce((elementValue, elementName, index) => {
@@ -192,7 +245,7 @@ function resolveElementValue({ submission, nestedElementNames, formElements, par
192
245
  * `parseFloat`
193
246
  * - Arrays of numeric strings (e.g. checkboxes) are summed
194
247
  * - Repeatable set entries sum the referenced nested numeric values
195
- * - Empty arrays yield `NaN` (calculation result becomes `undefined`)
248
+ * - Empty arrays yield `NaN` (result type `MISSING_VALUES`)
196
249
  * - Compliance element objects use their `value` property
197
250
  *
198
251
  * ##### Built-in functions
@@ -210,14 +263,14 @@ function resolveElementValue({ submission, nestedElementNames, formElements, par
210
263
  * ##### Examples
211
264
  *
212
265
  * ```js
213
- * const value = calculationService.evaluateExpression({
266
+ * const result = calculationService.evaluateExpression({
214
267
  * expression: '{ELEMENT:Quantity} * {ELEMENT:Price}',
215
268
  * submission: { Quantity: 3, Price: 12.5 },
216
269
  * formElements: form.elements,
217
270
  * // Client: local timezone. Server: organisation timezone.
218
271
  * parseDayOnlyDate: (value) => new Date(`${value}T00:00:00.000Z`),
219
272
  * })
220
- * // value === 37.5
273
+ * // result === { type: 'RESULT', value: 37.5 }
221
274
  * ```
222
275
  *
223
276
  * ```js
@@ -227,7 +280,7 @@ function resolveElementValue({ submission, nestedElementNames, formElements, par
227
280
  * formElements: form.elements,
228
281
  * parseDayOnlyDate: (value) => new Date(`${value}T00:00:00.000Z`),
229
282
  * })
230
- * // 110
283
+ * // { type: 'RESULT', value: 110 }
231
284
  * ```
232
285
  *
233
286
  * ```js
@@ -237,18 +290,26 @@ function resolveElementValue({ submission, nestedElementNames, formElements, par
237
290
  * formElements: form.elements,
238
291
  * parseDayOnlyDate: (value) => new Date(`${value}T00:00:00.000Z`),
239
292
  * })
240
- * // 5
293
+ * // { type: 'RESULT', value: 5 }
241
294
  * ```
242
295
  *
243
296
  * @param options
244
- * @returns The evaluated numeric result, or `undefined` when evaluation does
245
- * not yield a valid number (including `NaN`). Throws when the expression
246
- * cannot be parsed.
297
+ * @returns A discriminated result describing the outcome of evaluation. Empty
298
+ * expressions and parse failures return
299
+ * `{ type: 'INVALID_EXPRESSION', error }`. Unexpected runtime errors are
300
+ * rethrown.
247
301
  */
248
302
  export function evaluateExpression({ expression, submission, formElements, parseDayOnlyDate, }) {
249
303
  if (!expression) {
250
- throw new Error('Expression is required.');
304
+ return {
305
+ type: 'INVALID_EXPRESSION',
306
+ error: new Error('Expression is required.'),
307
+ };
251
308
  }
309
+ const elementNames = [];
310
+ matchElementsTagRegex(expression, ({ elementName }) => {
311
+ elementNames.push(elementName);
312
+ });
252
313
  const exprParser = new ExpressionParser();
253
314
  exprParser.registerFunction('ROUND', (value, precision) => {
254
315
  if (!Number.isNaN(value) && Number.isFinite(value)) {
@@ -274,10 +335,6 @@ export function evaluateExpression({ expression, submission, formElements, parse
274
335
  }
275
336
  return value;
276
337
  });
277
- const elementNames = [];
278
- matchElementsTagRegex(expression, ({ elementName }) => {
279
- elementNames.push(elementName);
280
- });
281
338
  const code = elementNames.reduce((code, elementName, index) => {
282
339
  const regex = new RegExp(escapeRegExp(`{ELEMENT:${elementName}}`), 'g');
283
340
  const replacement = `a${index}`;
@@ -289,9 +346,25 @@ export function evaluateExpression({ expression, submission, formElements, parse
289
346
  }));
290
347
  return code.replace(regex, replacement);
291
348
  }, expression);
292
- const result = exprParser.parse(code.trim()).eval(submission);
349
+ let parsed;
350
+ try {
351
+ parsed = exprParser.parse(code.trim());
352
+ }
353
+ catch (error) {
354
+ return {
355
+ type: 'INVALID_EXPRESSION',
356
+ error: error instanceof Error ? error : new Error(String(error)),
357
+ };
358
+ }
359
+ const result = parsed.eval(submission);
360
+ // Match historical behaviour: any non-NaN number (including Infinity) is a
361
+ // usable calculation result for display.
293
362
  if (typeof result === 'number' && !Number.isNaN(result)) {
294
- return result;
363
+ return { type: 'RESULT', value: result };
295
364
  }
365
+ // Successfully parsed expressions that do not yield a number (e.g. `true`,
366
+ // non-numeric element values) previously returned `undefined` with no error
367
+ // in the form UI. Treat them the same as missing submission values.
368
+ return { type: 'MISSING_VALUES' };
296
369
  }
297
370
  //# sourceMappingURL=calculationService.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"calculationService.js","sourceRoot":"","sources":["../src/calculationService.ts"],"names":[],"mappings":"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","sourcesContent":["import ExpressionParser from 'morph-expressions'\nimport { FormTypes, SubmissionTypes } from '@oneblink/types'\nimport {\n findFormElement,\n matchElementsTagRegex,\n} from './formElementsService.js'\nimport {\n DATE_ONLY_PATTERN,\n type ParseDayOnlyDate,\n} from './conditionalLogicService/types.js'\n\nexport type { ParseDayOnlyDate } from './conditionalLogicService/types.js'\n\nconst isUnenteredValue = (value: unknown | undefined) => {\n return !value && value !== 0\n}\n\nconst isObjectWithValue = (obj: unknown): obj is { value: unknown } => {\n return typeof obj === 'object' && obj !== null && 'value' in obj\n}\n\n/**\n * Escape special RegExp characters in a string so it can be used as a literal\n * pattern.\n */\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\n/**\n * Workaround for `.toFixed()` not rounding floating point numbers correctly.\n */\nfunction roundToFixed(number: number, decimals: number) {\n const multiplier = Math.pow(10, decimals)\n const roundedNumber = Math.round(number * multiplier) / multiplier\n return roundedNumber.toFixed(decimals)\n}\n\nfunction resolveElementValue({\n submission,\n nestedElementNames,\n formElements,\n parseDayOnlyDate,\n}: {\n submission: SubmissionTypes.S3SubmissionData['submission']\n nestedElementNames: string[]\n formElements: FormTypes.FormElement[]\n parseDayOnlyDate: ParseDayOnlyDate\n}): unknown {\n const defaultAccumulator = submission[nestedElementNames[0]]\n return nestedElementNames.reduce(\n (elementValue: unknown | undefined, elementName: string, index: number) => {\n // Numbers can just be returned as is\n if (typeof elementValue === 'number') {\n return elementValue\n }\n\n // Attempt to get a number from the element value as a string.\n // NaN is accounted for in the calculation so we can return that from here.\n if (typeof elementValue === 'string') {\n // Day-only (`YYYY-MM-DD`) strings are parsed via parseDayOnlyDate so\n // callers control timezone. Other date strings use `new Date(value)`.\n // Numeric strings (e.g. `\"10\"`) fall through to parseFloat — `Date`\n // can otherwise treat them as valid dates.\n const isDayOnly = DATE_ONLY_PATTERN.test(elementValue)\n if (isDayOnly) {\n const parsedDate = parseDayOnlyDate(elementValue)\n if (!Number.isNaN(parsedDate.getTime())) {\n return parsedDate.getTime()\n }\n } else if (Number.isNaN(Number(elementValue))) {\n const parsedDate = new Date(elementValue)\n if (!Number.isNaN(parsedDate.getTime())) {\n return parsedDate.getTime()\n }\n }\n\n return parseFloat(elementValue)\n }\n\n if (Array.isArray(elementValue)) {\n // If there are no entries, return NaN to prevent the calculation from\n // running.\n if (!elementValue.length) {\n return NaN\n }\n\n // An array could be an element that allows multiple values e.g.\n // checkboxes. If that is the case, add them all together and move on.\n const elementValues = elementValue.map((entry) => parseFloat(entry))\n if (elementValues.every((entry) => !Number.isNaN(entry))) {\n return elementValues.reduce((number, entry) => number + entry, 0)\n }\n\n // Otherwise attempt to process it as a repeatable set. If we found\n // another repeatable set to process, pass it to the next element name\n // to iterate over the entries. If we are processing the entries in a\n // repeatable set, we can sum the number elements in the entries.\n const nextElementName = nestedElementNames[index + 1]\n\n let isNestedRepeatableSet = false\n const nestedElementValues = elementValue.reduce(\n (nestedElementValues, entry) => {\n if (entry) {\n const nextElementValue = entry[nextElementName]\n if (Array.isArray(nextElementValue)) {\n if (nextElementValue.length) {\n nestedElementValues.push(...nextElementValue)\n isNestedRepeatableSet = true\n }\n } else {\n nestedElementValues.push(nextElementValue)\n }\n }\n return nestedElementValues\n },\n [] as unknown[],\n )\n\n // If the nested element values are all arrays, pass them on to the\n // next iteration.\n if (isNestedRepeatableSet) {\n return nestedElementValues\n }\n\n return nestedElementValues.reduce(\n (total: number, nestedElementValue: unknown | undefined) => {\n if (Number.isNaN(total)) {\n return NaN\n }\n const value = parseFloat(nestedElementValue as string)\n if (Number.isNaN(value)) {\n return NaN\n }\n return total + value\n },\n 0,\n )\n }\n\n // If the value is an object, take the element name and check to see if\n // this element is a nested form element. If so, take the next nested\n // element name, find its value in the object and return it for the next\n // iteration to handle.\n if (typeof elementValue === 'object') {\n const formFormElement = findFormElement(\n formElements,\n (e) => e.type === 'form' && e.name === nestedElementNames[index],\n )\n const nextElementName = nestedElementNames[index + 1]\n if (formFormElement && nextElementName) {\n return (elementValue as Record<string, unknown>)[nextElementName]\n }\n }\n\n // \"compliance\" form element has an object value with a \"value\" property.\n if (\n isObjectWithValue(elementValue) &&\n typeof elementValue.value === 'string'\n ) {\n return parseFloat(elementValue.value)\n }\n\n // We did not find a number value from the known elements, assume we are\n // at the end of the line.\n return NaN\n },\n defaultAccumulator,\n )\n}\n\n/**\n * Evaluate a OneBlink calculation expression against form submission data.\n *\n * Day-only (`YYYY-MM-DD`) date parsing is injected via `parseDayOnlyDate` so\n * callers control timezone-aware behaviour (client vs server), matching the\n * pattern used by `conditionalLogicService`. Other date strings use\n * `new Date(value)`.\n *\n * #### Expression syntax\n *\n * Expressions are mathematical / logical formulas. Form element values are\n * referenced with `{ELEMENT:<elementName>}` tags (including nested paths via\n * `|`, e.g. `{ELEMENT:Children|Age}`).\n *\n * ##### Literals\n *\n * - **Numbers** — e.g. `0`, `5`, `5.4`\n * - **Strings** — single or double quoted, e.g. `\"abc\"`, `'abc'`\n * - **Booleans** — `true`, `false`\n *\n * ##### Arithmetic operators\n *\n * | Operator | Description | Example |\n * | -------- | ------------------------------------ | -------------- |\n * | `+` | Addition (also unary plus) | `1 + 2` → `3` |\n * | `-` | Subtraction (also unary minus) | `1 - 2` → `-1` |\n * | `*` | Multiplication | `4 * 2` → `8` |\n * | `/` | Division | `4 / 2` → `2` |\n * | `%` | Remainder | `8 % 3` → `2` |\n * | `++` | Unary increment | `++2` → `3` |\n * | `--` | Unary decrement | `--2` → `1` |\n *\n * Standard operator precedence applies. Use parentheses to override it, e.g.\n * `3 * (2 + 1)` → `9`.\n *\n * ##### Comparison operators\n *\n * | Operator | Description | Example |\n * | -------- | --------------------- | ------------------- |\n * | `==` | Equal | `2 == 2` → `true` |\n * | `!=` | Not equal | `2 != 3` → `true` |\n * | `>` | Greater than | `2 > 1` → `true` |\n * | `>=` | Greater than or equal | `2 >= 2` → `true` |\n * | `<` | Less than | `2 < 3` → `true` |\n * | `<=` | Less than or equal | `2 <= 2` → `true` |\n *\n * ##### Logical operators\n *\n * | Operator | Description | Example |\n * | -------- | ------------------------------------ | -------------------------- |\n * | `&&` | Logical AND (returns last operand) | `true && false` → `false` |\n * | `\\|\\|` | Logical OR (returns first truthy) | `false \\|\\| true` → `true` |\n * | `!` | Logical NOT | `!false` → `true` |\n *\n * ##### Element references\n *\n * - `{ELEMENT:Number}` — value of the root element named `Number`\n * - `{ELEMENT:Set\\|Child}` — value of `Child` within repeatable set / nested\n * form element `Set`\n * - Nested paths may include multiple segments:\n * `{ELEMENT:Parents\\|Children\\|Age}`\n *\n * How values are coerced for evaluation:\n *\n * - Numbers are used as-is\n * - Day-only (`YYYY-MM-DD`) strings are parsed via `parseDayOnlyDate`; other\n * non-numeric date strings use `new Date(value)`. Valid dates use\n * `Date#getTime()`, otherwise the string is parsed as a float via\n * `parseFloat`\n * - Arrays of numeric strings (e.g. checkboxes) are summed\n * - Repeatable set entries sum the referenced nested numeric values\n * - Empty arrays yield `NaN` (calculation result becomes `undefined`)\n * - Compliance element objects use their `value` property\n *\n * ##### Built-in functions\n *\n * | Function | Description | Example |\n * | -------------------------------- | --------------------------------------------------------------------------- | ------------------------------- |\n * | `ROUND(value, precision)` | Round `value` to `precision` decimal places (correct floating-point rounds) | `ROUND(1.255, 2)` → `1.26` |\n * | `ROUND_DOWN(value)` | Round down to the nearest integer (`Math.floor`) | `ROUND_DOWN(1.9)` → `1` |\n * | `ROUND_UP(value)` | Round up to the nearest integer (`Math.ceil`) | `ROUND_UP(1.1)` → `2` |\n * | `ISNULL(value, defaultValue)` | If `value` is unentered (`null`/`undefined`/`\"\"`), return `defaultValue` or `0`; otherwise return `value` | `ISNULL({ELEMENT:A}, 10)` |\n *\n * `ROUND`, `ROUND_DOWN`, and `ROUND_UP` return `null` when `value` is `NaN` or\n * not finite.\n *\n * ##### Examples\n *\n * ```js\n * const value = calculationService.evaluateExpression({\n * expression: '{ELEMENT:Quantity} * {ELEMENT:Price}',\n * submission: { Quantity: 3, Price: 12.5 },\n * formElements: form.elements,\n * // Client: local timezone. Server: organisation timezone.\n * parseDayOnlyDate: (value) => new Date(`${value}T00:00:00.000Z`),\n * })\n * // value === 37.5\n * ```\n *\n * ```js\n * calculationService.evaluateExpression({\n * expression: 'ROUND({ELEMENT:Amount} * 1.1, 2)',\n * submission: { Amount: 100 },\n * formElements: form.elements,\n * parseDayOnlyDate: (value) => new Date(`${value}T00:00:00.000Z`),\n * })\n * // 110\n * ```\n *\n * ```js\n * calculationService.evaluateExpression({\n * expression: 'ISNULL({ELEMENT:Optional}, 0) + {ELEMENT:Required}',\n * submission: { Required: 5 },\n * formElements: form.elements,\n * parseDayOnlyDate: (value) => new Date(`${value}T00:00:00.000Z`),\n * })\n * // 5\n * ```\n *\n * @param options\n * @returns The evaluated numeric result, or `undefined` when evaluation does\n * not yield a valid number (including `NaN`). Throws when the expression\n * cannot be parsed.\n */\nexport function evaluateExpression({\n expression,\n submission,\n formElements,\n parseDayOnlyDate,\n}: {\n /** The calculation expression string to evaluate */\n expression: string\n /** Form submission data used to resolve `{ELEMENT:...}` references */\n submission: SubmissionTypes.S3SubmissionData['submission']\n /**\n * Form elements definition. Used to resolve nested `form` element values\n * referenced in the expression.\n */\n formElements: FormTypes.FormElement[]\n /**\n * Parse `YYYY-MM-DD` strings when resolving date element values. sdk-core\n * only calls this for day-only values; other date strings use\n * `new Date(value)`.\n */\n parseDayOnlyDate: ParseDayOnlyDate\n}): number | undefined {\n if (!expression) {\n throw new Error('Expression is required.')\n }\n\n const exprParser = new ExpressionParser<\n SubmissionTypes.S3SubmissionData['submission']\n >()\n exprParser.registerFunction('ROUND', (value: number, precision: number) => {\n if (!Number.isNaN(value) && Number.isFinite(value)) {\n return parseFloat(roundToFixed(value, precision))\n }\n return null\n })\n exprParser.registerFunction('ROUND_DOWN', (value: number) => {\n if (!Number.isNaN(value) && Number.isFinite(value)) {\n return Math.floor(value)\n }\n return null\n })\n exprParser.registerFunction('ROUND_UP', (value: number) => {\n if (!Number.isNaN(value) && Number.isFinite(value)) {\n return Math.ceil(value)\n }\n return null\n })\n exprParser.registerFunction(\n 'ISNULL',\n (value: unknown | undefined, defaultValue: number) => {\n if (isUnenteredValue(value)) {\n return defaultValue || 0\n }\n return value\n },\n )\n\n const elementNames: string[] = []\n matchElementsTagRegex(expression, ({ elementName }) => {\n elementNames.push(elementName)\n })\n\n const code = elementNames.reduce((code, elementName, index) => {\n const regex = new RegExp(escapeRegExp(`{ELEMENT:${elementName}}`), 'g')\n const replacement = `a${index}`\n exprParser.registerProperty(\n replacement,\n (submissionScope: SubmissionTypes.S3SubmissionData['submission']) =>\n resolveElementValue({\n submission: submissionScope,\n nestedElementNames: elementName.split('|'),\n formElements,\n parseDayOnlyDate,\n }),\n )\n return code.replace(regex, replacement)\n }, expression)\n\n const result = exprParser.parse(code.trim()).eval(submission)\n if (typeof result === 'number' && !Number.isNaN(result)) {\n return result\n }\n}\n"]}
1
+ {"version":3,"file":"calculationService.js","sourceRoot":"","sources":["../src/calculationService.ts"],"names":[],"mappings":"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","sourcesContent":["import ExpressionParser from 'morph-expressions'\nimport { FormTypes, SubmissionTypes } from '@oneblink/types'\nimport {\n findFormElement,\n flattenFormElements,\n matchElementsTagRegex,\n} from './formElementsService.js'\nimport {\n DATE_ONLY_PATTERN,\n type ParseDayOnlyDate,\n} from './conditionalLogicService/types.js'\n\nexport type { ParseDayOnlyDate } from './conditionalLogicService/types.js'\n\nexport type EvaluateExpressionResult =\n | { type: 'RESULT'; value: number }\n | { type: 'MISSING_VALUES' }\n | { type: 'INVALID_EXPRESSION'; error: Error }\n\nconst isUnenteredValue = (value: unknown | undefined) => {\n return !value && value !== 0\n}\n\nconst isObjectWithValue = (obj: unknown): obj is { value: unknown } => {\n return typeof obj === 'object' && obj !== null && 'value' in obj\n}\n\n/**\n * Escape special RegExp characters in a string so it can be used as a literal\n * pattern.\n */\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\n/**\n * Workaround for `.toFixed()` not rounding floating point numbers correctly.\n */\nfunction roundToFixed(number: number, decimals: number) {\n const multiplier = Math.pow(10, decimals)\n const roundedNumber = Math.round(number * multiplier) / multiplier\n return roundedNumber.toFixed(decimals)\n}\n\nfunction findFormElementByNamePath(\n formElements: FormTypes.FormElement[],\n elementName: string,\n): FormTypes.FormElement | undefined {\n const parts = elementName.split('|')\n let searchElements = formElements\n let found: FormTypes.FormElement | undefined\n\n for (let i = 0; i < parts.length; i++) {\n const name = parts[i]\n // Match flattenFormElements: page/section are transparent for naming, but\n // form/repeatableSet/infoPage require an explicit parent path segment.\n found = flattenFormElements(searchElements).find(\n (element) => 'name' in element && element.name === name,\n )\n if (!found) {\n return\n }\n\n if (i < parts.length - 1) {\n if (!('elements' in found) || !found.elements) {\n return\n }\n searchElements = found.elements\n }\n }\n\n return found\n}\n\n/**\n * Find `{ELEMENT:...}` references in an expression whose name paths do not\n * exist in the form definition.\n *\n * Intended for server-side payment validation. Skip this on the client during\n * interactive calculation evaluation — it walks the form definition for every\n * referenced element and is unnecessary for display.\n *\n * @returns Missing element name paths (e.g. `Amount` or `Items|Amount`), or an\n * empty array when every referenced element exists.\n */\nexport function findMissingFormElementsInExpression({\n expression,\n formElements,\n}: {\n expression: string\n formElements: FormTypes.FormElement[]\n}): string[] {\n if (!expression) {\n return []\n }\n\n const elementNames: string[] = []\n matchElementsTagRegex(expression, ({ elementName }) => {\n elementNames.push(elementName)\n })\n\n const missingFormElementNames: string[] = []\n const seen = new Set<string>()\n for (const elementName of elementNames) {\n if (seen.has(elementName)) {\n continue\n }\n seen.add(elementName)\n if (!findFormElementByNamePath(formElements, elementName)) {\n missingFormElementNames.push(elementName)\n }\n }\n return missingFormElementNames\n}\n\nfunction resolveElementValue({\n submission,\n nestedElementNames,\n formElements,\n parseDayOnlyDate,\n}: {\n submission: SubmissionTypes.S3SubmissionData['submission']\n nestedElementNames: string[]\n formElements: FormTypes.FormElement[]\n parseDayOnlyDate: ParseDayOnlyDate\n}): unknown {\n const defaultAccumulator = submission[nestedElementNames[0]]\n return nestedElementNames.reduce(\n (elementValue: unknown | undefined, elementName: string, index: number) => {\n // Numbers can just be returned as is\n if (typeof elementValue === 'number') {\n return elementValue\n }\n\n // Attempt to get a number from the element value as a string.\n // NaN is accounted for in the calculation so we can return that from here.\n if (typeof elementValue === 'string') {\n // Day-only (`YYYY-MM-DD`) strings are parsed via parseDayOnlyDate so\n // callers control timezone. Other date strings use `new Date(value)`.\n // Numeric strings (e.g. `\"10\"`) fall through to parseFloat — `Date`\n // can otherwise treat them as valid dates.\n const isDayOnly = DATE_ONLY_PATTERN.test(elementValue)\n if (isDayOnly) {\n const parsedDate = parseDayOnlyDate(elementValue)\n if (!Number.isNaN(parsedDate.getTime())) {\n return parsedDate.getTime()\n }\n } else if (Number.isNaN(Number(elementValue))) {\n const parsedDate = new Date(elementValue)\n if (!Number.isNaN(parsedDate.getTime())) {\n return parsedDate.getTime()\n }\n }\n\n return parseFloat(elementValue)\n }\n\n if (Array.isArray(elementValue)) {\n // If there are no entries, return NaN to prevent the calculation from\n // running.\n if (!elementValue.length) {\n return NaN\n }\n\n // An array could be an element that allows multiple values e.g.\n // checkboxes. If that is the case, add them all together and move on.\n const elementValues = elementValue.map((entry) => parseFloat(entry))\n if (elementValues.every((entry) => !Number.isNaN(entry))) {\n return elementValues.reduce((number, entry) => number + entry, 0)\n }\n\n // Otherwise attempt to process it as a repeatable set. If we found\n // another repeatable set to process, pass it to the next element name\n // to iterate over the entries. If we are processing the entries in a\n // repeatable set, we can sum the number elements in the entries.\n const nextElementName = nestedElementNames[index + 1]\n\n let isNestedRepeatableSet = false\n const nestedElementValues = elementValue.reduce(\n (nestedElementValues, entry) => {\n if (entry) {\n const nextElementValue = entry[nextElementName]\n if (Array.isArray(nextElementValue)) {\n if (nextElementValue.length) {\n nestedElementValues.push(...nextElementValue)\n isNestedRepeatableSet = true\n }\n } else {\n nestedElementValues.push(nextElementValue)\n }\n }\n return nestedElementValues\n },\n [] as unknown[],\n )\n\n // If the nested element values are all arrays, pass them on to the\n // next iteration.\n if (isNestedRepeatableSet) {\n return nestedElementValues\n }\n\n return nestedElementValues.reduce(\n (total: number, nestedElementValue: unknown | undefined) => {\n if (Number.isNaN(total)) {\n return NaN\n }\n const value = parseFloat(nestedElementValue as string)\n if (Number.isNaN(value)) {\n return NaN\n }\n return total + value\n },\n 0,\n )\n }\n\n // If the value is an object, take the element name and check to see if\n // this element is a nested form element. If so, take the next nested\n // element name, find its value in the object and return it for the next\n // iteration to handle.\n if (typeof elementValue === 'object') {\n const formFormElement = findFormElement(\n formElements,\n (e) => e.type === 'form' && e.name === nestedElementNames[index],\n )\n const nextElementName = nestedElementNames[index + 1]\n if (formFormElement && nextElementName) {\n return (elementValue as Record<string, unknown>)[nextElementName]\n }\n }\n\n // \"compliance\" form element has an object value with a \"value\" property.\n if (\n isObjectWithValue(elementValue) &&\n typeof elementValue.value === 'string'\n ) {\n return parseFloat(elementValue.value)\n }\n\n // We did not find a number value from the known elements, assume we are\n // at the end of the line.\n return NaN\n },\n defaultAccumulator,\n )\n}\n\n/**\n * Evaluate a OneBlink calculation expression against form submission data.\n *\n * Day-only (`YYYY-MM-DD`) date parsing is injected via `parseDayOnlyDate` so\n * callers control timezone-aware behaviour (client vs server), matching the\n * pattern used by `conditionalLogicService`. Other date strings use\n * `new Date(value)`.\n *\n * #### Expression syntax\n *\n * Expressions are mathematical / logical formulas. Form element values are\n * referenced with `{ELEMENT:<elementName>}` tags (including nested paths via\n * `|`, e.g. `{ELEMENT:Children|Age}`).\n *\n * ##### Literals\n *\n * - **Numbers** — e.g. `0`, `5`, `5.4`\n * - **Strings** — single or double quoted, e.g. `\"abc\"`, `'abc'`\n * - **Booleans** — `true`, `false`\n *\n * ##### Arithmetic operators\n *\n * | Operator | Description | Example |\n * | -------- | ------------------------------------ | -------------- |\n * | `+` | Addition (also unary plus) | `1 + 2` → `3` |\n * | `-` | Subtraction (also unary minus) | `1 - 2` → `-1` |\n * | `*` | Multiplication | `4 * 2` → `8` |\n * | `/` | Division | `4 / 2` → `2` |\n * | `%` | Remainder | `8 % 3` → `2` |\n * | `++` | Unary increment | `++2` → `3` |\n * | `--` | Unary decrement | `--2` → `1` |\n *\n * Standard operator precedence applies. Use parentheses to override it, e.g.\n * `3 * (2 + 1)` → `9`.\n *\n * ##### Comparison operators\n *\n * | Operator | Description | Example |\n * | -------- | --------------------- | ------------------- |\n * | `==` | Equal | `2 == 2` → `true` |\n * | `!=` | Not equal | `2 != 3` → `true` |\n * | `>` | Greater than | `2 > 1` → `true` |\n * | `>=` | Greater than or equal | `2 >= 2` → `true` |\n * | `<` | Less than | `2 < 3` → `true` |\n * | `<=` | Less than or equal | `2 <= 2` → `true` |\n *\n * ##### Logical operators\n *\n * | Operator | Description | Example |\n * | -------- | ------------------------------------ | -------------------------- |\n * | `&&` | Logical AND (returns last operand) | `true && false` → `false` |\n * | `\\|\\|` | Logical OR (returns first truthy) | `false \\|\\| true` → `true` |\n * | `!` | Logical NOT | `!false` → `true` |\n *\n * ##### Element references\n *\n * - `{ELEMENT:Number}` — value of the root element named `Number`\n * - `{ELEMENT:Set\\|Child}` — value of `Child` within repeatable set / nested\n * form element `Set`\n * - Nested paths may include multiple segments:\n * `{ELEMENT:Parents\\|Children\\|Age}`\n *\n * How values are coerced for evaluation:\n *\n * - Numbers are used as-is\n * - Day-only (`YYYY-MM-DD`) strings are parsed via `parseDayOnlyDate`; other\n * non-numeric date strings use `new Date(value)`. Valid dates use\n * `Date#getTime()`, otherwise the string is parsed as a float via\n * `parseFloat`\n * - Arrays of numeric strings (e.g. checkboxes) are summed\n * - Repeatable set entries sum the referenced nested numeric values\n * - Empty arrays yield `NaN` (result type `MISSING_VALUES`)\n * - Compliance element objects use their `value` property\n *\n * ##### Built-in functions\n *\n * | Function | Description | Example |\n * | -------------------------------- | --------------------------------------------------------------------------- | ------------------------------- |\n * | `ROUND(value, precision)` | Round `value` to `precision` decimal places (correct floating-point rounds) | `ROUND(1.255, 2)` → `1.26` |\n * | `ROUND_DOWN(value)` | Round down to the nearest integer (`Math.floor`) | `ROUND_DOWN(1.9)` → `1` |\n * | `ROUND_UP(value)` | Round up to the nearest integer (`Math.ceil`) | `ROUND_UP(1.1)` → `2` |\n * | `ISNULL(value, defaultValue)` | If `value` is unentered (`null`/`undefined`/`\"\"`), return `defaultValue` or `0`; otherwise return `value` | `ISNULL({ELEMENT:A}, 10)` |\n *\n * `ROUND`, `ROUND_DOWN`, and `ROUND_UP` return `null` when `value` is `NaN` or\n * not finite.\n *\n * ##### Examples\n *\n * ```js\n * const result = calculationService.evaluateExpression({\n * expression: '{ELEMENT:Quantity} * {ELEMENT:Price}',\n * submission: { Quantity: 3, Price: 12.5 },\n * formElements: form.elements,\n * // Client: local timezone. Server: organisation timezone.\n * parseDayOnlyDate: (value) => new Date(`${value}T00:00:00.000Z`),\n * })\n * // result === { type: 'RESULT', value: 37.5 }\n * ```\n *\n * ```js\n * calculationService.evaluateExpression({\n * expression: 'ROUND({ELEMENT:Amount} * 1.1, 2)',\n * submission: { Amount: 100 },\n * formElements: form.elements,\n * parseDayOnlyDate: (value) => new Date(`${value}T00:00:00.000Z`),\n * })\n * // { type: 'RESULT', value: 110 }\n * ```\n *\n * ```js\n * calculationService.evaluateExpression({\n * expression: 'ISNULL({ELEMENT:Optional}, 0) + {ELEMENT:Required}',\n * submission: { Required: 5 },\n * formElements: form.elements,\n * parseDayOnlyDate: (value) => new Date(`${value}T00:00:00.000Z`),\n * })\n * // { type: 'RESULT', value: 5 }\n * ```\n *\n * @param options\n * @returns A discriminated result describing the outcome of evaluation. Empty\n * expressions and parse failures return\n * `{ type: 'INVALID_EXPRESSION', error }`. Unexpected runtime errors are\n * rethrown.\n */\nexport function evaluateExpression({\n expression,\n submission,\n formElements,\n parseDayOnlyDate,\n}: {\n /** The calculation expression string to evaluate */\n expression: string\n /** Form submission data used to resolve `{ELEMENT:...}` references */\n submission: SubmissionTypes.S3SubmissionData['submission']\n /**\n * Form elements definition. Used to resolve nested `form` element values\n * referenced in the expression and to validate `{ELEMENT:...}` references.\n */\n formElements: FormTypes.FormElement[]\n /**\n * Parse `YYYY-MM-DD` strings when resolving date element values. sdk-core\n * only calls this for day-only values; other date strings use\n * `new Date(value)`.\n */\n parseDayOnlyDate: ParseDayOnlyDate\n}): EvaluateExpressionResult {\n if (!expression) {\n return {\n type: 'INVALID_EXPRESSION',\n error: new Error('Expression is required.'),\n }\n }\n\n const elementNames: string[] = []\n matchElementsTagRegex(expression, ({ elementName }) => {\n elementNames.push(elementName)\n })\n\n const exprParser = new ExpressionParser<\n SubmissionTypes.S3SubmissionData['submission']\n >()\n exprParser.registerFunction('ROUND', (value: number, precision: number) => {\n if (!Number.isNaN(value) && Number.isFinite(value)) {\n return parseFloat(roundToFixed(value, precision))\n }\n return null\n })\n exprParser.registerFunction('ROUND_DOWN', (value: number) => {\n if (!Number.isNaN(value) && Number.isFinite(value)) {\n return Math.floor(value)\n }\n return null\n })\n exprParser.registerFunction('ROUND_UP', (value: number) => {\n if (!Number.isNaN(value) && Number.isFinite(value)) {\n return Math.ceil(value)\n }\n return null\n })\n exprParser.registerFunction(\n 'ISNULL',\n (value: unknown | undefined, defaultValue: number) => {\n if (isUnenteredValue(value)) {\n return defaultValue || 0\n }\n return value\n },\n )\n\n const code = elementNames.reduce((code, elementName, index) => {\n const regex = new RegExp(escapeRegExp(`{ELEMENT:${elementName}}`), 'g')\n const replacement = `a${index}`\n exprParser.registerProperty(\n replacement,\n (submissionScope: SubmissionTypes.S3SubmissionData['submission']) =>\n resolveElementValue({\n submission: submissionScope,\n nestedElementNames: elementName.split('|'),\n formElements,\n parseDayOnlyDate,\n }),\n )\n return code.replace(regex, replacement)\n }, expression)\n\n let parsed\n try {\n parsed = exprParser.parse(code.trim())\n } catch (error) {\n return {\n type: 'INVALID_EXPRESSION',\n error: error instanceof Error ? error : new Error(String(error)),\n }\n }\n\n const result = parsed.eval(submission)\n // Match historical behaviour: any non-NaN number (including Infinity) is a\n // usable calculation result for display.\n if (typeof result === 'number' && !Number.isNaN(result)) {\n return { type: 'RESULT', value: result }\n }\n\n // Successfully parsed expressions that do not yield a number (e.g. `true`,\n // non-numeric element values) previously returned `undefined` with no error\n // in the form UI. Treat them the same as missing submission values.\n return { type: 'MISSING_VALUES' }\n}\n"]}
@@ -64,7 +64,7 @@ export function checkForPaymentEvent({ definition, submission, submissionTimesta
64
64
  }
65
65
  if (typeof amount !== 'number') {
66
66
  console.log('Form has a payment submission event but the amount is not a number, throwing error');
67
- throw new Error('The configuration required to make a payment is incorrect. Please contact your administrator to ensure your application configuration has been completed successfully.');
67
+ throw new Error('[PAYMENT_AMOUNT_NOT_NUMBER] The configuration required to make a payment is incorrect. Please contact your administrator to ensure your application configuration has been completed successfully.');
68
68
  }
69
69
  return {
70
70
  paymentSubmissionEvent,
@@ -79,23 +79,48 @@ function resolvePaymentEventAmount({ configuration, definition, submission, pars
79
79
  }
80
80
  case 'EXPRESSION': {
81
81
  console.log('Evaluating payment calculation from payment submission event configuration', configuration.paymentCalculation);
82
- return calculationService.evaluateExpression({
82
+ const missingFormElementNames = calculationService.findMissingFormElementsInExpression({
83
+ expression: configuration.paymentCalculation,
84
+ formElements: definition.elements,
85
+ });
86
+ if (missingFormElementNames.length) {
87
+ console.log('Form has a payment submission event but the calculation references missing form elements, throwing error', missingFormElementNames);
88
+ throw new Error('[PAYMENT_EXPRESSION_MISSING_FORM_ELEMENTS] We could not find the configuration required to make a payment. Please contact your administrator to ensure your application configuration has been completed successfully.');
89
+ }
90
+ const expressionResult = calculationService.evaluateExpression({
83
91
  expression: configuration.paymentCalculation,
84
92
  submission,
85
93
  formElements: definition.elements,
86
94
  parseDayOnlyDate,
87
95
  });
96
+ switch (expressionResult.type) {
97
+ case 'RESULT': {
98
+ return expressionResult.value;
99
+ }
100
+ case 'MISSING_VALUES': {
101
+ console.log('Form has a payment submission event but the calculation is missing submission values, finishing as normal submission');
102
+ return;
103
+ }
104
+ case 'INVALID_EXPRESSION': {
105
+ console.log('Form has a payment submission event but the calculation expression is invalid, throwing error', expressionResult);
106
+ throw new Error('[PAYMENT_INVALID_EXPRESSION] We could not find the configuration required to make a payment. Please contact your administrator to ensure your application configuration has been completed successfully.');
107
+ }
108
+ default: {
109
+ const neverResult = expressionResult;
110
+ throw new Error(`[PAYMENT_UNEXPECTED_EXPRESSION_RESULT] Unexpected payment calculation result: ${JSON.stringify(neverResult)}`);
111
+ }
112
+ }
88
113
  }
89
114
  case 'FORM_ELEMENT':
90
115
  default: {
91
116
  if (!configuration.elementId) {
92
117
  console.log('Form has a payment submission event but the amount configuration is missing, throwing error');
93
- throw new Error('We could not find the configuration required to make a payment. Please contact your administrator to ensure your application configuration has been completed successfully.');
118
+ throw new Error('[PAYMENT_MISSING_ELEMENT_ID] We could not find the configuration required to make a payment. Please contact your administrator to ensure your application configuration has been completed successfully.');
94
119
  }
95
120
  const amountElement = formElementsService.findFormElement(definition.elements, (element) => element.id === configuration.elementId);
96
121
  if (!amountElement) {
97
122
  console.log('Form has a payment submission event but the amount element does not exist, throwing error');
98
- throw new Error('We could not find the configuration required to make a payment. Please contact your administrator to ensure your application configuration has been completed successfully.');
123
+ throw new Error('[PAYMENT_AMOUNT_ELEMENT_NOT_FOUND] We could not find the configuration required to make a payment. Please contact your administrator to ensure your application configuration has been completed successfully.');
99
124
  }
100
125
  console.log('Found form element for payment submission event', amountElement);
101
126
  return getRootElementValueById(amountElement.id, definition.elements, submission);
@@ -1 +1 @@
1
- {"version":3,"file":"paymentService.js","sourceRoot":"","sources":["../src/paymentService.ts"],"names":[],"mappings":"AAKA,OAAO,EACL,kBAAkB,EAClB,uBAAuB,EACvB,mBAAmB,GACpB,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAA;AAOhE,OAAO,kBAAkB,MAAM,wCAAwC,CAAA;AAevE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,UAAU,oBAAoB,CAAC,EACnC,UAAU,EACV,UAAU,EACV,mBAAmB,EACnB,gBAAgB,EAChB,aAAa,EACb,UAAU,EACV,QAAQ,GAcT;IAMC,MAAM,uBAAuB,GAAG,UAAU,CAAC,aAAa,IAAI,EAAE,CAAA;IAC9D,MAAM,sBAAsB,GAAG,uBAAuB,CAAC,IAAI,CACzD,CAAC,sBAAsB,EAAE,EAAE;QACzB,OAAO,CACL,sBAAsB;YACtB,uBAAuB,CAAC,6BAA6B,CAAC;gBACpD,aAAa,EAAE,CAAC,CAAC,sBAAsB,CAAC,oBAAoB;gBAC5D,gCAAgC,EAC9B,CAAC,CAAC,sBAAsB,CAAC,yCAAyC;gBACpE,qBAAqB,EACnB,sBAAsB,CAAC,8BAA8B,IAAI,EAAE;gBAC7D,UAAU,EAAE,UAAU;gBACtB,YAAY,EAAE,UAAU,CAAC,QAAQ;gBACjC,mBAAmB;gBACnB,gBAAgB;gBAChB,aAAa;gBACb,UAAU;gBACV,QAAQ;aACT,CAAC,CACH,CAAA;IACH,CAAC,CACF,CAAA;IAED,IAAI,CAAC,sBAAsB,EAAE,CAAC;QAC5B,OAAM;IACR,CAAC;IACD,OAAO,CAAC,GAAG,CACT,uEAAuE,CACxE,CAAA;IAED,MAAM,MAAM,GAAG,yBAAyB,CAAC;QACvC,aAAa,EAAE,sBAAsB,CAAC,aAAa;QACnD,UAAU;QACV,UAAU;QACV,gBAAgB;KACjB,CAAC,CAAA;IAEF,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,CAAC,GAAG,CACT,wHAAwH,CACzH,CAAA;QACD,OAAM;IACR,CAAC;IAED,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;QAC/B,OAAO,CAAC,GAAG,CACT,oFAAoF,CACrF,CAAA;QACD,MAAM,IAAI,KAAK,CACb,wKAAwK,CACzK,CAAA;IACH,CAAC;IAED,OAAO;QACL,sBAAsB;QACtB,MAAM;KACP,CAAA;AACH,CAAC;AAED,SAAS,yBAAyB,CAAC,EACjC,aAAa,EACb,UAAU,EACV,UAAU,EACV,gBAAgB,GAMjB;IACC,QAAQ,aAAa,CAAC,UAAU,EAAE,CAAC;QACjC,KAAK,QAAQ,CAAC,CAAC,CAAC;YACd,OAAO,CAAC,GAAG,CACT,wEAAwE,EACxE,aAAa,CAAC,aAAa,CAC5B,CAAA;YACD,OAAO,aAAa,CAAC,aAAa,CAAA;QACpC,CAAC;QACD,KAAK,YAAY,CAAC,CAAC,CAAC;YAClB,OAAO,CAAC,GAAG,CACT,4EAA4E,EAC5E,aAAa,CAAC,kBAAkB,CACjC,CAAA;YACD,OAAO,kBAAkB,CAAC,kBAAkB,CAAC;gBAC3C,UAAU,EAAE,aAAa,CAAC,kBAAkB;gBAC5C,UAAU;gBACV,YAAY,EAAE,UAAU,CAAC,QAAQ;gBACjC,gBAAgB;aACjB,CAAC,CAAA;QACJ,CAAC;QACD,KAAK,cAAc,CAAC;QACpB,OAAO,CAAC,CAAC,CAAC;YACR,IAAI,CAAC,aAAa,CAAC,SAAS,EAAE,CAAC;gBAC7B,OAAO,CAAC,GAAG,CACT,6FAA6F,CAC9F,CAAA;gBACD,MAAM,IAAI,KAAK,CACb,6KAA6K,CAC9K,CAAA;YACH,CAAC;YAED,MAAM,aAAa,GAAG,mBAAmB,CAAC,eAAe,CACvD,UAAU,CAAC,QAAQ,EACnB,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,KAAK,aAAa,CAAC,SAAS,CACpD,CAAA;YACD,IAAI,CAAC,aAAa,EAAE,CAAC;gBACnB,OAAO,CAAC,GAAG,CACT,2FAA2F,CAC5F,CAAA;gBACD,MAAM,IAAI,KAAK,CACb,6KAA6K,CAC9K,CAAA;YACH,CAAC;YAED,OAAO,CAAC,GAAG,CACT,iDAAiD,EACjD,aAAa,CACd,CAAA;YAED,OAAO,uBAAuB,CAC5B,aAAa,CAAC,EAAE,EAChB,UAAU,CAAC,QAAQ,EACnB,UAAU,CACX,CAAA;QACH,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,MAAM,0CAA0C,GAAG;AACxD,0DAA0D;AAC1D,qBAA4D,EAC5D,UAAoC,EACK,EAAE;IAC3C,OAAO,kBAAkB,CAAC,qBAAqB,CAAC,IAAI,CAAC,CAAC,iBAAiB,CACrE,qBAAqB,EACrB,UAAU,CACX,CAAA;AACH,CAAC,CAAA;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,4CAA4C,CAC1D,qBAA4D;IAE5D,OAAO,kBAAkB,CAAC,qBAAqB,CAAC,IAAI,CAAC,CAAC,mBAAmB,CACvE,qBAAqB,CACtB,CAAA;AACH,CAAC","sourcesContent":["import {\n FormTypes,\n SubmissionEventTypes,\n SubmissionTypes,\n} from '@oneblink/types'\nimport {\n calculationService,\n conditionalLogicService,\n formElementsService,\n} from './index.js'\nimport { getRootElementValueById } from './submissionService.js'\nimport {\n AddOffsetToDate,\n EndOfDay,\n ParseDayOnlyDate,\n StartOfDay,\n} from './conditionalLogicService/types.js'\nimport getPaymentProvider from './paymentService/getPaymentProvider.js'\nimport type {\n FormStorePayment,\n PaymentDisplayDetail,\n PaymentDisplayFormatters,\n PaymentDisplayDetailsResult,\n} from './paymentService/types.js'\n\nexport {\n FormStorePayment,\n PaymentDisplayDetail,\n PaymentDisplayDetailsResult,\n PaymentDisplayFormatters,\n}\n\n/**\n * Examine a submission and its form definition to validate whether a payment\n * workflow event needs to run.\n *\n * #### Example\n *\n * ```js\n * const result = paymentService.checkForPaymentEvent({\n * definition: form,\n * submission,\n * submissionTimestamp,\n * parseDayOnlyDate: (value) => new Date(`${value}T00:00:00.000Z`),\n * addDaysToDate: (date, days) => {\n * date.setUTCDate(date.getUTCDate() + days)\n * return date\n * },\n * startOfDay: (date) => {\n * date.setUTCHours(0, 0, 0, 0)\n * return date\n * },\n * endOfDay: (date) => {\n * date.setUTCHours(23, 59, 59, 999)\n * return date\n * },\n * })\n * ```\n *\n * @param options\n * @returns\n */\nexport function checkForPaymentEvent({\n definition,\n submission,\n submissionTimestamp,\n parseDayOnlyDate,\n addDaysToDate,\n startOfDay,\n endOfDay,\n}: {\n definition: FormTypes.Form\n submission: SubmissionTypes.S3SubmissionData['submission']\n /** ISO timestamp the form was submitted. When evaluating during submission, pass `new Date().toISOString()`. */\n submissionTimestamp: string\n /** Parse `YYYY-MM-DD` strings when evaluating date based predicates */\n parseDayOnlyDate: ParseDayOnlyDate\n /** Add days to a date when evaluating date based predicates */\n addDaysToDate: AddOffsetToDate\n /** Start of calendar day when evaluating date (not datetime) based predicates */\n startOfDay: StartOfDay\n /** End of calendar day when evaluating date (not datetime) based predicates */\n endOfDay: EndOfDay\n}):\n | {\n paymentSubmissionEvent: SubmissionEventTypes.FormPaymentEvent\n amount: number\n }\n | undefined {\n const paymentSubmissionEvents = definition.paymentEvents || []\n const paymentSubmissionEvent = paymentSubmissionEvents.find(\n (paymentSubmissionEvent) => {\n return (\n paymentSubmissionEvent &&\n conditionalLogicService.evaluateConditionalPredicates({\n isConditional: !!paymentSubmissionEvent.conditionallyExecute,\n requiresAllConditionalPredicates:\n !!paymentSubmissionEvent.requiresAllConditionallyExecutePredicates,\n conditionalPredicates:\n paymentSubmissionEvent.conditionallyExecutePredicates || [],\n submission: submission,\n formElements: definition.elements,\n submissionTimestamp,\n parseDayOnlyDate,\n addDaysToDate,\n startOfDay,\n endOfDay,\n })\n )\n },\n )\n\n if (!paymentSubmissionEvent) {\n return\n }\n console.log(\n 'Checking if submission with payment submission event needs processing',\n )\n\n const amount = resolvePaymentEventAmount({\n configuration: paymentSubmissionEvent.configuration,\n definition,\n submission,\n parseDayOnlyDate,\n })\n\n if (!amount) {\n console.log(\n 'Form has a payment submission event but the amount has been entered as 0 or not at all, finishing as normal submission',\n )\n return\n }\n\n if (typeof amount !== 'number') {\n console.log(\n 'Form has a payment submission event but the amount is not a number, throwing error',\n )\n throw new Error(\n 'The configuration required to make a payment is incorrect. Please contact your administrator to ensure your application configuration has been completed successfully.',\n )\n }\n\n return {\n paymentSubmissionEvent,\n amount,\n }\n}\n\nfunction resolvePaymentEventAmount({\n configuration,\n definition,\n submission,\n parseDayOnlyDate,\n}: {\n configuration: SubmissionEventTypes.FormPaymentEvent['configuration']\n definition: FormTypes.Form\n submission: SubmissionTypes.S3SubmissionData['submission']\n parseDayOnlyDate: ParseDayOnlyDate\n}): unknown {\n switch (configuration.amountType) {\n case 'NUMBER': {\n console.log(\n 'Using fixed payment amount from payment submission event configuration',\n configuration.paymentAmount,\n )\n return configuration.paymentAmount\n }\n case 'EXPRESSION': {\n console.log(\n 'Evaluating payment calculation from payment submission event configuration',\n configuration.paymentCalculation,\n )\n return calculationService.evaluateExpression({\n expression: configuration.paymentCalculation,\n submission,\n formElements: definition.elements,\n parseDayOnlyDate,\n })\n }\n case 'FORM_ELEMENT':\n default: {\n if (!configuration.elementId) {\n console.log(\n 'Form has a payment submission event but the amount configuration is missing, throwing error',\n )\n throw new Error(\n 'We could not find the configuration required to make a payment. Please contact your administrator to ensure your application configuration has been completed successfully.',\n )\n }\n\n const amountElement = formElementsService.findFormElement(\n definition.elements,\n (element) => element.id === configuration.elementId,\n )\n if (!amountElement) {\n console.log(\n 'Form has a payment submission event but the amount element does not exist, throwing error',\n )\n throw new Error(\n 'We could not find the configuration required to make a payment. Please contact your administrator to ensure your application configuration has been completed successfully.',\n )\n }\n\n console.log(\n 'Found form element for payment submission event',\n amountElement,\n )\n\n return getRootElementValueById(\n amountElement.id,\n definition.elements,\n submission,\n )\n }\n }\n}\n\n/**\n * Retrieve an array of detail items from a form submission payment.\n *\n * #### Example\n *\n * ```js\n * const detailItems =\n * paymentService.getDisplayDetailsFromFormSubmissionPayment(\n * formSubmissionPayment,\n * {\n * formatCurrency,\n * formatDateTime,\n * formatDate,\n * },\n * )\n * ```\n *\n * @param formSubmissionPayment\n * @param options\n * @returns\n */\nexport const getDisplayDetailsFromFormSubmissionPayment = (\n /** The form submission payment to get the details from */\n formSubmissionPayment: SubmissionTypes.FormSubmissionPayment,\n formatters: PaymentDisplayFormatters,\n): PaymentDisplayDetailsResult | undefined => {\n return getPaymentProvider(formSubmissionPayment.type).getDisplayDetails(\n formSubmissionPayment,\n formatters,\n )\n}\n\n/**\n * Map a form submission payment to the Form Store payment property.\n *\n * #### Example\n *\n * ```js\n * const formStorePayment =\n * paymentService.getFormStorePaymentFromFormSubmissionPayment(\n * formSubmissionPayment,\n * )\n * ```\n *\n * @param formSubmissionPayment\n * @returns\n */\nexport function getFormStorePaymentFromFormSubmissionPayment(\n formSubmissionPayment: SubmissionTypes.FormSubmissionPayment,\n): FormStorePayment {\n return getPaymentProvider(formSubmissionPayment.type).getFormStorePayment(\n formSubmissionPayment,\n )\n}\n"]}
1
+ {"version":3,"file":"paymentService.js","sourceRoot":"","sources":["../src/paymentService.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n FormTypes,\n SubmissionEventTypes,\n SubmissionTypes,\n} from '@oneblink/types'\nimport {\n calculationService,\n conditionalLogicService,\n formElementsService,\n} from './index.js'\nimport { getRootElementValueById } from './submissionService.js'\nimport {\n AddOffsetToDate,\n EndOfDay,\n ParseDayOnlyDate,\n StartOfDay,\n} from './conditionalLogicService/types.js'\nimport getPaymentProvider from './paymentService/getPaymentProvider.js'\nimport type {\n FormStorePayment,\n PaymentDisplayDetail,\n PaymentDisplayFormatters,\n PaymentDisplayDetailsResult,\n} from './paymentService/types.js'\n\nexport {\n FormStorePayment,\n PaymentDisplayDetail,\n PaymentDisplayDetailsResult,\n PaymentDisplayFormatters,\n}\n\n/**\n * Examine a submission and its form definition to validate whether a payment\n * workflow event needs to run.\n *\n * #### Example\n *\n * ```js\n * const result = paymentService.checkForPaymentEvent({\n * definition: form,\n * submission,\n * submissionTimestamp,\n * parseDayOnlyDate: (value) => new Date(`${value}T00:00:00.000Z`),\n * addDaysToDate: (date, days) => {\n * date.setUTCDate(date.getUTCDate() + days)\n * return date\n * },\n * startOfDay: (date) => {\n * date.setUTCHours(0, 0, 0, 0)\n * return date\n * },\n * endOfDay: (date) => {\n * date.setUTCHours(23, 59, 59, 999)\n * return date\n * },\n * })\n * ```\n *\n * @param options\n * @returns\n */\nexport function checkForPaymentEvent({\n definition,\n submission,\n submissionTimestamp,\n parseDayOnlyDate,\n addDaysToDate,\n startOfDay,\n endOfDay,\n}: {\n definition: FormTypes.Form\n submission: SubmissionTypes.S3SubmissionData['submission']\n /** ISO timestamp the form was submitted. When evaluating during submission, pass `new Date().toISOString()`. */\n submissionTimestamp: string\n /** Parse `YYYY-MM-DD` strings when evaluating date based predicates */\n parseDayOnlyDate: ParseDayOnlyDate\n /** Add days to a date when evaluating date based predicates */\n addDaysToDate: AddOffsetToDate\n /** Start of calendar day when evaluating date (not datetime) based predicates */\n startOfDay: StartOfDay\n /** End of calendar day when evaluating date (not datetime) based predicates */\n endOfDay: EndOfDay\n}):\n | {\n paymentSubmissionEvent: SubmissionEventTypes.FormPaymentEvent\n amount: number\n }\n | undefined {\n const paymentSubmissionEvents = definition.paymentEvents || []\n const paymentSubmissionEvent = paymentSubmissionEvents.find(\n (paymentSubmissionEvent) => {\n return (\n paymentSubmissionEvent &&\n conditionalLogicService.evaluateConditionalPredicates({\n isConditional: !!paymentSubmissionEvent.conditionallyExecute,\n requiresAllConditionalPredicates:\n !!paymentSubmissionEvent.requiresAllConditionallyExecutePredicates,\n conditionalPredicates:\n paymentSubmissionEvent.conditionallyExecutePredicates || [],\n submission: submission,\n formElements: definition.elements,\n submissionTimestamp,\n parseDayOnlyDate,\n addDaysToDate,\n startOfDay,\n endOfDay,\n })\n )\n },\n )\n\n if (!paymentSubmissionEvent) {\n return\n }\n console.log(\n 'Checking if submission with payment submission event needs processing',\n )\n\n const amount = resolvePaymentEventAmount({\n configuration: paymentSubmissionEvent.configuration,\n definition,\n submission,\n parseDayOnlyDate,\n })\n\n if (!amount) {\n console.log(\n 'Form has a payment submission event but the amount has been entered as 0 or not at all, finishing as normal submission',\n )\n return\n }\n\n if (typeof amount !== 'number') {\n console.log(\n 'Form has a payment submission event but the amount is not a number, throwing error',\n )\n throw new Error(\n '[PAYMENT_AMOUNT_NOT_NUMBER] The configuration required to make a payment is incorrect. Please contact your administrator to ensure your application configuration has been completed successfully.',\n )\n }\n\n return {\n paymentSubmissionEvent,\n amount,\n }\n}\n\nfunction resolvePaymentEventAmount({\n configuration,\n definition,\n submission,\n parseDayOnlyDate,\n}: {\n configuration: SubmissionEventTypes.FormPaymentEvent['configuration']\n definition: FormTypes.Form\n submission: SubmissionTypes.S3SubmissionData['submission']\n parseDayOnlyDate: ParseDayOnlyDate\n}): unknown {\n switch (configuration.amountType) {\n case 'NUMBER': {\n console.log(\n 'Using fixed payment amount from payment submission event configuration',\n configuration.paymentAmount,\n )\n return configuration.paymentAmount\n }\n case 'EXPRESSION': {\n console.log(\n 'Evaluating payment calculation from payment submission event configuration',\n configuration.paymentCalculation,\n )\n const missingFormElementNames =\n calculationService.findMissingFormElementsInExpression({\n expression: configuration.paymentCalculation,\n formElements: definition.elements,\n })\n if (missingFormElementNames.length) {\n console.log(\n 'Form has a payment submission event but the calculation references missing form elements, throwing error',\n missingFormElementNames,\n )\n throw new Error(\n '[PAYMENT_EXPRESSION_MISSING_FORM_ELEMENTS] We could not find the configuration required to make a payment. Please contact your administrator to ensure your application configuration has been completed successfully.',\n )\n }\n\n const expressionResult = calculationService.evaluateExpression({\n expression: configuration.paymentCalculation,\n submission,\n formElements: definition.elements,\n parseDayOnlyDate,\n })\n switch (expressionResult.type) {\n case 'RESULT': {\n return expressionResult.value\n }\n case 'MISSING_VALUES': {\n console.log(\n 'Form has a payment submission event but the calculation is missing submission values, finishing as normal submission',\n )\n return\n }\n case 'INVALID_EXPRESSION': {\n console.log(\n 'Form has a payment submission event but the calculation expression is invalid, throwing error',\n expressionResult,\n )\n throw new Error(\n '[PAYMENT_INVALID_EXPRESSION] We could not find the configuration required to make a payment. Please contact your administrator to ensure your application configuration has been completed successfully.',\n )\n }\n default: {\n const neverResult: never = expressionResult\n throw new Error(\n `[PAYMENT_UNEXPECTED_EXPRESSION_RESULT] Unexpected payment calculation result: ${JSON.stringify(neverResult)}`,\n )\n }\n }\n }\n case 'FORM_ELEMENT':\n default: {\n if (!configuration.elementId) {\n console.log(\n 'Form has a payment submission event but the amount configuration is missing, throwing error',\n )\n throw new Error(\n '[PAYMENT_MISSING_ELEMENT_ID] We could not find the configuration required to make a payment. Please contact your administrator to ensure your application configuration has been completed successfully.',\n )\n }\n\n const amountElement = formElementsService.findFormElement(\n definition.elements,\n (element) => element.id === configuration.elementId,\n )\n if (!amountElement) {\n console.log(\n 'Form has a payment submission event but the amount element does not exist, throwing error',\n )\n throw new Error(\n '[PAYMENT_AMOUNT_ELEMENT_NOT_FOUND] We could not find the configuration required to make a payment. Please contact your administrator to ensure your application configuration has been completed successfully.',\n )\n }\n\n console.log(\n 'Found form element for payment submission event',\n amountElement,\n )\n\n return getRootElementValueById(\n amountElement.id,\n definition.elements,\n submission,\n )\n }\n }\n}\n\n/**\n * Retrieve an array of detail items from a form submission payment.\n *\n * #### Example\n *\n * ```js\n * const detailItems =\n * paymentService.getDisplayDetailsFromFormSubmissionPayment(\n * formSubmissionPayment,\n * {\n * formatCurrency,\n * formatDateTime,\n * formatDate,\n * },\n * )\n * ```\n *\n * @param formSubmissionPayment\n * @param options\n * @returns\n */\nexport const getDisplayDetailsFromFormSubmissionPayment = (\n /** The form submission payment to get the details from */\n formSubmissionPayment: SubmissionTypes.FormSubmissionPayment,\n formatters: PaymentDisplayFormatters,\n): PaymentDisplayDetailsResult | undefined => {\n return getPaymentProvider(formSubmissionPayment.type).getDisplayDetails(\n formSubmissionPayment,\n formatters,\n )\n}\n\n/**\n * Map a form submission payment to the Form Store payment property.\n *\n * #### Example\n *\n * ```js\n * const formStorePayment =\n * paymentService.getFormStorePaymentFromFormSubmissionPayment(\n * formSubmissionPayment,\n * )\n * ```\n *\n * @param formSubmissionPayment\n * @returns\n */\nexport function getFormStorePaymentFromFormSubmissionPayment(\n formSubmissionPayment: SubmissionTypes.FormSubmissionPayment,\n): FormStorePayment {\n return getPaymentProvider(formSubmissionPayment.type).getFormStorePayment(\n formSubmissionPayment,\n )\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@oneblink/sdk-core",
3
3
  "description": "OneBlink SDK for JavaScript (works in Browsers and NodeJS)",
4
- "version": "10.1.0-beta.2",
4
+ "version": "10.1.0-beta.4",
5
5
  "author": "OneBlink <developers@oneblink.io> (https://oneblink.io)",
6
6
  "bugs": {
7
7
  "url": "https://github.com/oneblink/sdk-core-js/issues"