@oneblink/sdk-core 10.1.0-beta.1 → 10.1.0-beta.3
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.
- package/dist/calculationService.d.ts +169 -0
- package/dist/calculationService.js +370 -0
- package/dist/calculationService.js.map +1 -0
- package/dist/conditionalLogicService/evaluateConditionalSubmissionTimestampPredicate.js +1 -1
- package/dist/conditionalLogicService/evaluateConditionalSubmissionTimestampPredicate.js.map +1 -1
- package/dist/conditionalLogicService/types.d.ts +5 -0
- package/dist/conditionalLogicService/types.js +5 -1
- package/dist/conditionalLogicService/types.js.map +1 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/paymentService.js +65 -11
- package/dist/paymentService.js.map +1 -1
- package/package.json +4 -1
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { FormTypes, SubmissionTypes } from '@oneblink/types';
|
|
2
|
+
import { type ParseDayOnlyDate } from './conditionalLogicService/types.js';
|
|
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[];
|
|
28
|
+
/**
|
|
29
|
+
* Evaluate a OneBlink calculation expression against form submission data.
|
|
30
|
+
*
|
|
31
|
+
* Day-only (`YYYY-MM-DD`) date parsing is injected via `parseDayOnlyDate` so
|
|
32
|
+
* callers control timezone-aware behaviour (client vs server), matching the
|
|
33
|
+
* pattern used by `conditionalLogicService`. Other date strings use
|
|
34
|
+
* `new Date(value)`.
|
|
35
|
+
*
|
|
36
|
+
* #### Expression syntax
|
|
37
|
+
*
|
|
38
|
+
* Expressions are mathematical / logical formulas. Form element values are
|
|
39
|
+
* referenced with `{ELEMENT:<elementName>}` tags (including nested paths via
|
|
40
|
+
* `|`, e.g. `{ELEMENT:Children|Age}`).
|
|
41
|
+
*
|
|
42
|
+
* ##### Literals
|
|
43
|
+
*
|
|
44
|
+
* - **Numbers** — e.g. `0`, `5`, `5.4`
|
|
45
|
+
* - **Strings** — single or double quoted, e.g. `"abc"`, `'abc'`
|
|
46
|
+
* - **Booleans** — `true`, `false`
|
|
47
|
+
*
|
|
48
|
+
* ##### Arithmetic operators
|
|
49
|
+
*
|
|
50
|
+
* | Operator | Description | Example |
|
|
51
|
+
* | -------- | ------------------------------------ | -------------- |
|
|
52
|
+
* | `+` | Addition (also unary plus) | `1 + 2` → `3` |
|
|
53
|
+
* | `-` | Subtraction (also unary minus) | `1 - 2` → `-1` |
|
|
54
|
+
* | `*` | Multiplication | `4 * 2` → `8` |
|
|
55
|
+
* | `/` | Division | `4 / 2` → `2` |
|
|
56
|
+
* | `%` | Remainder | `8 % 3` → `2` |
|
|
57
|
+
* | `++` | Unary increment | `++2` → `3` |
|
|
58
|
+
* | `--` | Unary decrement | `--2` → `1` |
|
|
59
|
+
*
|
|
60
|
+
* Standard operator precedence applies. Use parentheses to override it, e.g.
|
|
61
|
+
* `3 * (2 + 1)` → `9`.
|
|
62
|
+
*
|
|
63
|
+
* ##### Comparison operators
|
|
64
|
+
*
|
|
65
|
+
* | Operator | Description | Example |
|
|
66
|
+
* | -------- | --------------------- | ------------------- |
|
|
67
|
+
* | `==` | Equal | `2 == 2` → `true` |
|
|
68
|
+
* | `!=` | Not equal | `2 != 3` → `true` |
|
|
69
|
+
* | `>` | Greater than | `2 > 1` → `true` |
|
|
70
|
+
* | `>=` | Greater than or equal | `2 >= 2` → `true` |
|
|
71
|
+
* | `<` | Less than | `2 < 3` → `true` |
|
|
72
|
+
* | `<=` | Less than or equal | `2 <= 2` → `true` |
|
|
73
|
+
*
|
|
74
|
+
* ##### Logical operators
|
|
75
|
+
*
|
|
76
|
+
* | Operator | Description | Example |
|
|
77
|
+
* | -------- | ------------------------------------ | -------------------------- |
|
|
78
|
+
* | `&&` | Logical AND (returns last operand) | `true && false` → `false` |
|
|
79
|
+
* | `\|\|` | Logical OR (returns first truthy) | `false \|\| true` → `true` |
|
|
80
|
+
* | `!` | Logical NOT | `!false` → `true` |
|
|
81
|
+
*
|
|
82
|
+
* ##### Element references
|
|
83
|
+
*
|
|
84
|
+
* - `{ELEMENT:Number}` — value of the root element named `Number`
|
|
85
|
+
* - `{ELEMENT:Set\|Child}` — value of `Child` within repeatable set / nested
|
|
86
|
+
* form element `Set`
|
|
87
|
+
* - Nested paths may include multiple segments:
|
|
88
|
+
* `{ELEMENT:Parents\|Children\|Age}`
|
|
89
|
+
*
|
|
90
|
+
* How values are coerced for evaluation:
|
|
91
|
+
*
|
|
92
|
+
* - Numbers are used as-is
|
|
93
|
+
* - Day-only (`YYYY-MM-DD`) strings are parsed via `parseDayOnlyDate`; other
|
|
94
|
+
* non-numeric date strings use `new Date(value)`. Valid dates use
|
|
95
|
+
* `Date#getTime()`, otherwise the string is parsed as a float via
|
|
96
|
+
* `parseFloat`
|
|
97
|
+
* - Arrays of numeric strings (e.g. checkboxes) are summed
|
|
98
|
+
* - Repeatable set entries sum the referenced nested numeric values
|
|
99
|
+
* - Empty arrays yield `NaN` (result type `MISSING_VALUES`)
|
|
100
|
+
* - Compliance element objects use their `value` property
|
|
101
|
+
*
|
|
102
|
+
* ##### Built-in functions
|
|
103
|
+
*
|
|
104
|
+
* | Function | Description | Example |
|
|
105
|
+
* | -------------------------------- | --------------------------------------------------------------------------- | ------------------------------- |
|
|
106
|
+
* | `ROUND(value, precision)` | Round `value` to `precision` decimal places (correct floating-point rounds) | `ROUND(1.255, 2)` → `1.26` |
|
|
107
|
+
* | `ROUND_DOWN(value)` | Round down to the nearest integer (`Math.floor`) | `ROUND_DOWN(1.9)` → `1` |
|
|
108
|
+
* | `ROUND_UP(value)` | Round up to the nearest integer (`Math.ceil`) | `ROUND_UP(1.1)` → `2` |
|
|
109
|
+
* | `ISNULL(value, defaultValue)` | If `value` is unentered (`null`/`undefined`/`""`), return `defaultValue` or `0`; otherwise return `value` | `ISNULL({ELEMENT:A}, 10)` |
|
|
110
|
+
*
|
|
111
|
+
* `ROUND`, `ROUND_DOWN`, and `ROUND_UP` return `null` when `value` is `NaN` or
|
|
112
|
+
* not finite.
|
|
113
|
+
*
|
|
114
|
+
* ##### Examples
|
|
115
|
+
*
|
|
116
|
+
* ```js
|
|
117
|
+
* const result = calculationService.evaluateExpression({
|
|
118
|
+
* expression: '{ELEMENT:Quantity} * {ELEMENT:Price}',
|
|
119
|
+
* submission: { Quantity: 3, Price: 12.5 },
|
|
120
|
+
* formElements: form.elements,
|
|
121
|
+
* // Client: local timezone. Server: organisation timezone.
|
|
122
|
+
* parseDayOnlyDate: (value) => new Date(`${value}T00:00:00.000Z`),
|
|
123
|
+
* })
|
|
124
|
+
* // result === { type: 'RESULT', value: 37.5 }
|
|
125
|
+
* ```
|
|
126
|
+
*
|
|
127
|
+
* ```js
|
|
128
|
+
* calculationService.evaluateExpression({
|
|
129
|
+
* expression: 'ROUND({ELEMENT:Amount} * 1.1, 2)',
|
|
130
|
+
* submission: { Amount: 100 },
|
|
131
|
+
* formElements: form.elements,
|
|
132
|
+
* parseDayOnlyDate: (value) => new Date(`${value}T00:00:00.000Z`),
|
|
133
|
+
* })
|
|
134
|
+
* // { type: 'RESULT', value: 110 }
|
|
135
|
+
* ```
|
|
136
|
+
*
|
|
137
|
+
* ```js
|
|
138
|
+
* calculationService.evaluateExpression({
|
|
139
|
+
* expression: 'ISNULL({ELEMENT:Optional}, 0) + {ELEMENT:Required}',
|
|
140
|
+
* submission: { Required: 5 },
|
|
141
|
+
* formElements: form.elements,
|
|
142
|
+
* parseDayOnlyDate: (value) => new Date(`${value}T00:00:00.000Z`),
|
|
143
|
+
* })
|
|
144
|
+
* // { type: 'RESULT', value: 5 }
|
|
145
|
+
* ```
|
|
146
|
+
*
|
|
147
|
+
* @param options
|
|
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.
|
|
152
|
+
*/
|
|
153
|
+
export declare function evaluateExpression({ expression, submission, formElements, parseDayOnlyDate, }: {
|
|
154
|
+
/** The calculation expression string to evaluate */
|
|
155
|
+
expression: string;
|
|
156
|
+
/** Form submission data used to resolve `{ELEMENT:...}` references */
|
|
157
|
+
submission: SubmissionTypes.S3SubmissionData['submission'];
|
|
158
|
+
/**
|
|
159
|
+
* Form elements definition. Used to resolve nested `form` element values
|
|
160
|
+
* referenced in the expression and to validate `{ELEMENT:...}` references.
|
|
161
|
+
*/
|
|
162
|
+
formElements: FormTypes.FormElement[];
|
|
163
|
+
/**
|
|
164
|
+
* Parse `YYYY-MM-DD` strings when resolving date element values. sdk-core
|
|
165
|
+
* only calls this for day-only values; other date strings use
|
|
166
|
+
* `new Date(value)`.
|
|
167
|
+
*/
|
|
168
|
+
parseDayOnlyDate: ParseDayOnlyDate;
|
|
169
|
+
}): EvaluateExpressionResult;
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import ExpressionParser from 'morph-expressions';
|
|
2
|
+
import { findFormElement, flattenFormElements, matchElementsTagRegex, } from './formElementsService.js';
|
|
3
|
+
import { DATE_ONLY_PATTERN, } from './conditionalLogicService/types.js';
|
|
4
|
+
const isUnenteredValue = (value) => {
|
|
5
|
+
return !value && value !== 0;
|
|
6
|
+
};
|
|
7
|
+
const isObjectWithValue = (obj) => {
|
|
8
|
+
return typeof obj === 'object' && obj !== null && 'value' in obj;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Escape special RegExp characters in a string so it can be used as a literal
|
|
12
|
+
* pattern.
|
|
13
|
+
*/
|
|
14
|
+
function escapeRegExp(value) {
|
|
15
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Workaround for `.toFixed()` not rounding floating point numbers correctly.
|
|
19
|
+
*/
|
|
20
|
+
function roundToFixed(number, decimals) {
|
|
21
|
+
const multiplier = Math.pow(10, decimals);
|
|
22
|
+
const roundedNumber = Math.round(number * multiplier) / multiplier;
|
|
23
|
+
return roundedNumber.toFixed(decimals);
|
|
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
|
+
}
|
|
78
|
+
function resolveElementValue({ submission, nestedElementNames, formElements, parseDayOnlyDate, }) {
|
|
79
|
+
const defaultAccumulator = submission[nestedElementNames[0]];
|
|
80
|
+
return nestedElementNames.reduce((elementValue, elementName, index) => {
|
|
81
|
+
// Numbers can just be returned as is
|
|
82
|
+
if (typeof elementValue === 'number') {
|
|
83
|
+
return elementValue;
|
|
84
|
+
}
|
|
85
|
+
// Attempt to get a number from the element value as a string.
|
|
86
|
+
// NaN is accounted for in the calculation so we can return that from here.
|
|
87
|
+
if (typeof elementValue === 'string') {
|
|
88
|
+
// Day-only (`YYYY-MM-DD`) strings are parsed via parseDayOnlyDate so
|
|
89
|
+
// callers control timezone. Other date strings use `new Date(value)`.
|
|
90
|
+
// Numeric strings (e.g. `"10"`) fall through to parseFloat — `Date`
|
|
91
|
+
// can otherwise treat them as valid dates.
|
|
92
|
+
const isDayOnly = DATE_ONLY_PATTERN.test(elementValue);
|
|
93
|
+
if (isDayOnly) {
|
|
94
|
+
const parsedDate = parseDayOnlyDate(elementValue);
|
|
95
|
+
if (!Number.isNaN(parsedDate.getTime())) {
|
|
96
|
+
return parsedDate.getTime();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else if (Number.isNaN(Number(elementValue))) {
|
|
100
|
+
const parsedDate = new Date(elementValue);
|
|
101
|
+
if (!Number.isNaN(parsedDate.getTime())) {
|
|
102
|
+
return parsedDate.getTime();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return parseFloat(elementValue);
|
|
106
|
+
}
|
|
107
|
+
if (Array.isArray(elementValue)) {
|
|
108
|
+
// If there are no entries, return NaN to prevent the calculation from
|
|
109
|
+
// running.
|
|
110
|
+
if (!elementValue.length) {
|
|
111
|
+
return NaN;
|
|
112
|
+
}
|
|
113
|
+
// An array could be an element that allows multiple values e.g.
|
|
114
|
+
// checkboxes. If that is the case, add them all together and move on.
|
|
115
|
+
const elementValues = elementValue.map((entry) => parseFloat(entry));
|
|
116
|
+
if (elementValues.every((entry) => !Number.isNaN(entry))) {
|
|
117
|
+
return elementValues.reduce((number, entry) => number + entry, 0);
|
|
118
|
+
}
|
|
119
|
+
// Otherwise attempt to process it as a repeatable set. If we found
|
|
120
|
+
// another repeatable set to process, pass it to the next element name
|
|
121
|
+
// to iterate over the entries. If we are processing the entries in a
|
|
122
|
+
// repeatable set, we can sum the number elements in the entries.
|
|
123
|
+
const nextElementName = nestedElementNames[index + 1];
|
|
124
|
+
let isNestedRepeatableSet = false;
|
|
125
|
+
const nestedElementValues = elementValue.reduce((nestedElementValues, entry) => {
|
|
126
|
+
if (entry) {
|
|
127
|
+
const nextElementValue = entry[nextElementName];
|
|
128
|
+
if (Array.isArray(nextElementValue)) {
|
|
129
|
+
if (nextElementValue.length) {
|
|
130
|
+
nestedElementValues.push(...nextElementValue);
|
|
131
|
+
isNestedRepeatableSet = true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
nestedElementValues.push(nextElementValue);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return nestedElementValues;
|
|
139
|
+
}, []);
|
|
140
|
+
// If the nested element values are all arrays, pass them on to the
|
|
141
|
+
// next iteration.
|
|
142
|
+
if (isNestedRepeatableSet) {
|
|
143
|
+
return nestedElementValues;
|
|
144
|
+
}
|
|
145
|
+
return nestedElementValues.reduce((total, nestedElementValue) => {
|
|
146
|
+
if (Number.isNaN(total)) {
|
|
147
|
+
return NaN;
|
|
148
|
+
}
|
|
149
|
+
const value = parseFloat(nestedElementValue);
|
|
150
|
+
if (Number.isNaN(value)) {
|
|
151
|
+
return NaN;
|
|
152
|
+
}
|
|
153
|
+
return total + value;
|
|
154
|
+
}, 0);
|
|
155
|
+
}
|
|
156
|
+
// If the value is an object, take the element name and check to see if
|
|
157
|
+
// this element is a nested form element. If so, take the next nested
|
|
158
|
+
// element name, find its value in the object and return it for the next
|
|
159
|
+
// iteration to handle.
|
|
160
|
+
if (typeof elementValue === 'object') {
|
|
161
|
+
const formFormElement = findFormElement(formElements, (e) => e.type === 'form' && e.name === nestedElementNames[index]);
|
|
162
|
+
const nextElementName = nestedElementNames[index + 1];
|
|
163
|
+
if (formFormElement && nextElementName) {
|
|
164
|
+
return elementValue[nextElementName];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// "compliance" form element has an object value with a "value" property.
|
|
168
|
+
if (isObjectWithValue(elementValue) &&
|
|
169
|
+
typeof elementValue.value === 'string') {
|
|
170
|
+
return parseFloat(elementValue.value);
|
|
171
|
+
}
|
|
172
|
+
// We did not find a number value from the known elements, assume we are
|
|
173
|
+
// at the end of the line.
|
|
174
|
+
return NaN;
|
|
175
|
+
}, defaultAccumulator);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Evaluate a OneBlink calculation expression against form submission data.
|
|
179
|
+
*
|
|
180
|
+
* Day-only (`YYYY-MM-DD`) date parsing is injected via `parseDayOnlyDate` so
|
|
181
|
+
* callers control timezone-aware behaviour (client vs server), matching the
|
|
182
|
+
* pattern used by `conditionalLogicService`. Other date strings use
|
|
183
|
+
* `new Date(value)`.
|
|
184
|
+
*
|
|
185
|
+
* #### Expression syntax
|
|
186
|
+
*
|
|
187
|
+
* Expressions are mathematical / logical formulas. Form element values are
|
|
188
|
+
* referenced with `{ELEMENT:<elementName>}` tags (including nested paths via
|
|
189
|
+
* `|`, e.g. `{ELEMENT:Children|Age}`).
|
|
190
|
+
*
|
|
191
|
+
* ##### Literals
|
|
192
|
+
*
|
|
193
|
+
* - **Numbers** — e.g. `0`, `5`, `5.4`
|
|
194
|
+
* - **Strings** — single or double quoted, e.g. `"abc"`, `'abc'`
|
|
195
|
+
* - **Booleans** — `true`, `false`
|
|
196
|
+
*
|
|
197
|
+
* ##### Arithmetic operators
|
|
198
|
+
*
|
|
199
|
+
* | Operator | Description | Example |
|
|
200
|
+
* | -------- | ------------------------------------ | -------------- |
|
|
201
|
+
* | `+` | Addition (also unary plus) | `1 + 2` → `3` |
|
|
202
|
+
* | `-` | Subtraction (also unary minus) | `1 - 2` → `-1` |
|
|
203
|
+
* | `*` | Multiplication | `4 * 2` → `8` |
|
|
204
|
+
* | `/` | Division | `4 / 2` → `2` |
|
|
205
|
+
* | `%` | Remainder | `8 % 3` → `2` |
|
|
206
|
+
* | `++` | Unary increment | `++2` → `3` |
|
|
207
|
+
* | `--` | Unary decrement | `--2` → `1` |
|
|
208
|
+
*
|
|
209
|
+
* Standard operator precedence applies. Use parentheses to override it, e.g.
|
|
210
|
+
* `3 * (2 + 1)` → `9`.
|
|
211
|
+
*
|
|
212
|
+
* ##### Comparison operators
|
|
213
|
+
*
|
|
214
|
+
* | Operator | Description | Example |
|
|
215
|
+
* | -------- | --------------------- | ------------------- |
|
|
216
|
+
* | `==` | Equal | `2 == 2` → `true` |
|
|
217
|
+
* | `!=` | Not equal | `2 != 3` → `true` |
|
|
218
|
+
* | `>` | Greater than | `2 > 1` → `true` |
|
|
219
|
+
* | `>=` | Greater than or equal | `2 >= 2` → `true` |
|
|
220
|
+
* | `<` | Less than | `2 < 3` → `true` |
|
|
221
|
+
* | `<=` | Less than or equal | `2 <= 2` → `true` |
|
|
222
|
+
*
|
|
223
|
+
* ##### Logical operators
|
|
224
|
+
*
|
|
225
|
+
* | Operator | Description | Example |
|
|
226
|
+
* | -------- | ------------------------------------ | -------------------------- |
|
|
227
|
+
* | `&&` | Logical AND (returns last operand) | `true && false` → `false` |
|
|
228
|
+
* | `\|\|` | Logical OR (returns first truthy) | `false \|\| true` → `true` |
|
|
229
|
+
* | `!` | Logical NOT | `!false` → `true` |
|
|
230
|
+
*
|
|
231
|
+
* ##### Element references
|
|
232
|
+
*
|
|
233
|
+
* - `{ELEMENT:Number}` — value of the root element named `Number`
|
|
234
|
+
* - `{ELEMENT:Set\|Child}` — value of `Child` within repeatable set / nested
|
|
235
|
+
* form element `Set`
|
|
236
|
+
* - Nested paths may include multiple segments:
|
|
237
|
+
* `{ELEMENT:Parents\|Children\|Age}`
|
|
238
|
+
*
|
|
239
|
+
* How values are coerced for evaluation:
|
|
240
|
+
*
|
|
241
|
+
* - Numbers are used as-is
|
|
242
|
+
* - Day-only (`YYYY-MM-DD`) strings are parsed via `parseDayOnlyDate`; other
|
|
243
|
+
* non-numeric date strings use `new Date(value)`. Valid dates use
|
|
244
|
+
* `Date#getTime()`, otherwise the string is parsed as a float via
|
|
245
|
+
* `parseFloat`
|
|
246
|
+
* - Arrays of numeric strings (e.g. checkboxes) are summed
|
|
247
|
+
* - Repeatable set entries sum the referenced nested numeric values
|
|
248
|
+
* - Empty arrays yield `NaN` (result type `MISSING_VALUES`)
|
|
249
|
+
* - Compliance element objects use their `value` property
|
|
250
|
+
*
|
|
251
|
+
* ##### Built-in functions
|
|
252
|
+
*
|
|
253
|
+
* | Function | Description | Example |
|
|
254
|
+
* | -------------------------------- | --------------------------------------------------------------------------- | ------------------------------- |
|
|
255
|
+
* | `ROUND(value, precision)` | Round `value` to `precision` decimal places (correct floating-point rounds) | `ROUND(1.255, 2)` → `1.26` |
|
|
256
|
+
* | `ROUND_DOWN(value)` | Round down to the nearest integer (`Math.floor`) | `ROUND_DOWN(1.9)` → `1` |
|
|
257
|
+
* | `ROUND_UP(value)` | Round up to the nearest integer (`Math.ceil`) | `ROUND_UP(1.1)` → `2` |
|
|
258
|
+
* | `ISNULL(value, defaultValue)` | If `value` is unentered (`null`/`undefined`/`""`), return `defaultValue` or `0`; otherwise return `value` | `ISNULL({ELEMENT:A}, 10)` |
|
|
259
|
+
*
|
|
260
|
+
* `ROUND`, `ROUND_DOWN`, and `ROUND_UP` return `null` when `value` is `NaN` or
|
|
261
|
+
* not finite.
|
|
262
|
+
*
|
|
263
|
+
* ##### Examples
|
|
264
|
+
*
|
|
265
|
+
* ```js
|
|
266
|
+
* const result = calculationService.evaluateExpression({
|
|
267
|
+
* expression: '{ELEMENT:Quantity} * {ELEMENT:Price}',
|
|
268
|
+
* submission: { Quantity: 3, Price: 12.5 },
|
|
269
|
+
* formElements: form.elements,
|
|
270
|
+
* // Client: local timezone. Server: organisation timezone.
|
|
271
|
+
* parseDayOnlyDate: (value) => new Date(`${value}T00:00:00.000Z`),
|
|
272
|
+
* })
|
|
273
|
+
* // result === { type: 'RESULT', value: 37.5 }
|
|
274
|
+
* ```
|
|
275
|
+
*
|
|
276
|
+
* ```js
|
|
277
|
+
* calculationService.evaluateExpression({
|
|
278
|
+
* expression: 'ROUND({ELEMENT:Amount} * 1.1, 2)',
|
|
279
|
+
* submission: { Amount: 100 },
|
|
280
|
+
* formElements: form.elements,
|
|
281
|
+
* parseDayOnlyDate: (value) => new Date(`${value}T00:00:00.000Z`),
|
|
282
|
+
* })
|
|
283
|
+
* // { type: 'RESULT', value: 110 }
|
|
284
|
+
* ```
|
|
285
|
+
*
|
|
286
|
+
* ```js
|
|
287
|
+
* calculationService.evaluateExpression({
|
|
288
|
+
* expression: 'ISNULL({ELEMENT:Optional}, 0) + {ELEMENT:Required}',
|
|
289
|
+
* submission: { Required: 5 },
|
|
290
|
+
* formElements: form.elements,
|
|
291
|
+
* parseDayOnlyDate: (value) => new Date(`${value}T00:00:00.000Z`),
|
|
292
|
+
* })
|
|
293
|
+
* // { type: 'RESULT', value: 5 }
|
|
294
|
+
* ```
|
|
295
|
+
*
|
|
296
|
+
* @param options
|
|
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.
|
|
301
|
+
*/
|
|
302
|
+
export function evaluateExpression({ expression, submission, formElements, parseDayOnlyDate, }) {
|
|
303
|
+
if (!expression) {
|
|
304
|
+
return {
|
|
305
|
+
type: 'INVALID_EXPRESSION',
|
|
306
|
+
error: new Error('Expression is required.'),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
const elementNames = [];
|
|
310
|
+
matchElementsTagRegex(expression, ({ elementName }) => {
|
|
311
|
+
elementNames.push(elementName);
|
|
312
|
+
});
|
|
313
|
+
const exprParser = new ExpressionParser();
|
|
314
|
+
exprParser.registerFunction('ROUND', (value, precision) => {
|
|
315
|
+
if (!Number.isNaN(value) && Number.isFinite(value)) {
|
|
316
|
+
return parseFloat(roundToFixed(value, precision));
|
|
317
|
+
}
|
|
318
|
+
return null;
|
|
319
|
+
});
|
|
320
|
+
exprParser.registerFunction('ROUND_DOWN', (value) => {
|
|
321
|
+
if (!Number.isNaN(value) && Number.isFinite(value)) {
|
|
322
|
+
return Math.floor(value);
|
|
323
|
+
}
|
|
324
|
+
return null;
|
|
325
|
+
});
|
|
326
|
+
exprParser.registerFunction('ROUND_UP', (value) => {
|
|
327
|
+
if (!Number.isNaN(value) && Number.isFinite(value)) {
|
|
328
|
+
return Math.ceil(value);
|
|
329
|
+
}
|
|
330
|
+
return null;
|
|
331
|
+
});
|
|
332
|
+
exprParser.registerFunction('ISNULL', (value, defaultValue) => {
|
|
333
|
+
if (isUnenteredValue(value)) {
|
|
334
|
+
return defaultValue || 0;
|
|
335
|
+
}
|
|
336
|
+
return value;
|
|
337
|
+
});
|
|
338
|
+
const code = elementNames.reduce((code, elementName, index) => {
|
|
339
|
+
const regex = new RegExp(escapeRegExp(`{ELEMENT:${elementName}}`), 'g');
|
|
340
|
+
const replacement = `a${index}`;
|
|
341
|
+
exprParser.registerProperty(replacement, (submissionScope) => resolveElementValue({
|
|
342
|
+
submission: submissionScope,
|
|
343
|
+
nestedElementNames: elementName.split('|'),
|
|
344
|
+
formElements,
|
|
345
|
+
parseDayOnlyDate,
|
|
346
|
+
}));
|
|
347
|
+
return code.replace(regex, replacement);
|
|
348
|
+
}, expression);
|
|
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.
|
|
362
|
+
if (typeof result === 'number' && !Number.isNaN(result)) {
|
|
363
|
+
return { type: 'RESULT', value: result };
|
|
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' };
|
|
369
|
+
}
|
|
370
|
+
//# sourceMappingURL=calculationService.js.map
|
|
@@ -0,0 +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 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"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { DATE_ONLY_PATTERN, } from './types.js';
|
|
1
2
|
import { getElementAndValue } from './evaluateFormElementConditionalPredicate.js';
|
|
2
|
-
const DATE_ONLY_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
3
3
|
function resolveDateValue(dateValue, formElementsCtrl, parseDayOnlyDate, addDaysToDate, startOfDay, endOfDay, dayBoundary) {
|
|
4
4
|
let dateString;
|
|
5
5
|
if (dateValue.compareWith === 'ELEMENT') {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"evaluateConditionalSubmissionTimestampPredicate.js","sourceRoot":"","sources":["../../src/conditionalLogicService/evaluateConditionalSubmissionTimestampPredicate.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"evaluateConditionalSubmissionTimestampPredicate.js","sourceRoot":"","sources":["../../src/conditionalLogicService/evaluateConditionalSubmissionTimestampPredicate.ts"],"names":[],"mappings":"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","sourcesContent":["import { ConditionTypes } from '@oneblink/types'\nimport {\n AddOffsetToDate,\n DATE_ONLY_PATTERN,\n EndOfDay,\n FormElementsCtrl,\n ParseDayOnlyDate,\n StartOfDay,\n} from './types.js'\nimport { getElementAndValue } from './evaluateFormElementConditionalPredicate.js'\n\ntype DayBoundary = 'start' | 'end'\n\nfunction resolveDateValue(\n dateValue: ConditionTypes.ConditionalPredicateDateValue,\n formElementsCtrl: FormElementsCtrl,\n parseDayOnlyDate: ParseDayOnlyDate,\n addDaysToDate: AddOffsetToDate,\n startOfDay: StartOfDay,\n endOfDay: EndOfDay,\n dayBoundary: DayBoundary,\n): Date | undefined {\n let dateString: string | undefined\n\n if (dateValue.compareWith === 'ELEMENT') {\n const elementAndValue = getElementAndValue(\n formElementsCtrl,\n dateValue.elementId,\n )\n if (typeof elementAndValue.value === 'string' && elementAndValue.value) {\n dateString = elementAndValue.value\n } else {\n return undefined\n }\n } else {\n dateString = dateValue.value\n }\n\n const isDayOnly = DATE_ONLY_PATTERN.test(dateString)\n const date = isDayOnly ? parseDayOnlyDate(dateString) : new Date(dateString)\n if (Number.isNaN(date.getTime())) {\n return undefined\n }\n\n const dateWithOffset =\n typeof dateValue.daysOffset !== 'number' ||\n Number.isNaN(dateValue.daysOffset)\n ? date\n : addDaysToDate(date, dateValue.daysOffset)\n\n if (isDayOnly) {\n if (dayBoundary === 'start') {\n return startOfDay(dateWithOffset)\n } else {\n return endOfDay(dateWithOffset)\n }\n }\n\n return dateWithOffset\n}\n\n/**\n * Evaluate a `SUBMISSION_TIMESTAMP` conditional predicate against the form\n * submission timestamp.\n *\n * - `AFTER` — submission timestamp is after (exclusive) the comparison date\n * - `BEFORE` — submission timestamp is before (exclusive) the comparison date\n * - `BETWEEN` — submission timestamp is between `min` and `max` (inclusive)\n *\n * When the comparison value is a day-only (`YYYY-MM-DD`) string, day\n * boundaries are applied via the injected helpers:\n *\n * - `AFTER` — end of the comparison day\n * - `BEFORE` — start of the comparison day\n * - `BETWEEN` — start of `min` day through end of `max` day\n *\n * Full ISO datetime values compare at the exact instant.\n *\n * Day-only (`YYYY-MM-DD`) strings are parsed via `parseDayOnlyDate`. Other\n * date strings use `new Date(value)`. Day offsets are applied via\n * `addDaysToDate(date, offset)`.\n */\nexport default function evaluateConditionalSubmissionTimestampPredicate({\n predicate,\n formElementsCtrl,\n submissionTimestamp,\n parseDayOnlyDate,\n addDaysToDate,\n startOfDay,\n endOfDay,\n}: {\n predicate: ConditionTypes.ConditionalPredicateSubmissionTimestamp\n formElementsCtrl: FormElementsCtrl\n submissionTimestamp: string\n parseDayOnlyDate: ParseDayOnlyDate\n addDaysToDate: AddOffsetToDate\n startOfDay: StartOfDay\n endOfDay: EndOfDay\n}): boolean {\n const submissionDate = new Date(submissionTimestamp)\n if (Number.isNaN(submissionDate.getTime())) {\n return false\n }\n\n const submissionTime = submissionDate.getTime()\n\n switch (predicate.operator) {\n case 'BETWEEN': {\n const min = resolveDateValue(\n predicate.min,\n formElementsCtrl,\n parseDayOnlyDate,\n addDaysToDate,\n startOfDay,\n endOfDay,\n 'start',\n )\n const max = resolveDateValue(\n predicate.max,\n formElementsCtrl,\n parseDayOnlyDate,\n addDaysToDate,\n startOfDay,\n endOfDay,\n 'end',\n )\n if (!min || !max) {\n return false\n }\n return submissionTime >= min.getTime() && submissionTime <= max.getTime()\n }\n default: {\n const dayBoundary: DayBoundary =\n predicate.operator === 'AFTER' ? 'end' : 'start'\n const compareDate = resolveDateValue(\n predicate,\n formElementsCtrl,\n parseDayOnlyDate,\n addDaysToDate,\n startOfDay,\n endOfDay,\n dayBoundary,\n )\n if (!compareDate) {\n return false\n }\n\n switch (predicate.operator) {\n case 'AFTER':\n return submissionTime > compareDate.getTime()\n case 'BEFORE':\n return submissionTime < compareDate.getTime()\n default: {\n const n: never = predicate\n console.warn('Unhandled predicate operator', n)\n return false\n }\n }\n }\n }\n}\n"]}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { FormTypes, SubmissionTypes } from '@oneblink/types';
|
|
2
|
+
/**
|
|
3
|
+
* Matches day-only (`YYYY-MM-DD`) date strings. Used to decide when to call
|
|
4
|
+
* `parseDayOnlyDate` instead of `new Date(value)`.
|
|
5
|
+
*/
|
|
6
|
+
export declare const DATE_ONLY_PATTERN: RegExp;
|
|
2
7
|
/**
|
|
3
8
|
* Parse a day-only (`YYYY-MM-DD`) string into a `Date`. Callers supply
|
|
4
9
|
* timezone-aware parsing (e.g. start of day in an organisation timezone).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/conditionalLogicService/types.ts"],"names":[],"mappings":"","sourcesContent":["import { FormTypes, SubmissionTypes } from '@oneblink/types'\n\n/**\n * Parse a day-only (`YYYY-MM-DD`) string into a `Date`. Callers supply\n * timezone-aware parsing (e.g. start of day in an organisation timezone).\n * sdk-core only invokes this for values that match `YYYY-MM-DD`.\n */\nexport type ParseDayOnlyDate = (value: string) => Date\n\n/**\n * Add an offset to a date. Callers supply timezone-aware calendar arithmetic.\n */\nexport type AddOffsetToDate = (date: Date, offset: number) => Date\n\n/**\n * Move a date to the start of its calendar day. Callers supply timezone-aware\n * behaviour (e.g. organisation timezone on the server, local timezone in the\n * browser).\n */\nexport type StartOfDay = (date: Date) => Date\n\n/**\n * Move a date to the end of its calendar day (last millisecond). Callers supply\n * timezone-aware behaviour (e.g. organisation timezone on the server, local\n * timezone in the browser).\n */\nexport type EndOfDay = (date: Date) => Date\n\nexport type FormElementsCtrl = {\n model: SubmissionTypes.S3SubmissionData['submission'] | undefined\n flattenedElements: FormTypes.FormElement[]\n parentFormElementsCtrl?: FormElementsCtrl\n}\n"]}
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/conditionalLogicService/types.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,qBAAqB,CAAA","sourcesContent":["import { FormTypes, SubmissionTypes } from '@oneblink/types'\n\n/**\n * Matches day-only (`YYYY-MM-DD`) date strings. Used to decide when to call\n * `parseDayOnlyDate` instead of `new Date(value)`.\n */\nexport const DATE_ONLY_PATTERN = /^\\d{4}-\\d{2}-\\d{2}$/\n\n/**\n * Parse a day-only (`YYYY-MM-DD`) string into a `Date`. Callers supply\n * timezone-aware parsing (e.g. start of day in an organisation timezone).\n * sdk-core only invokes this for values that match `YYYY-MM-DD`.\n */\nexport type ParseDayOnlyDate = (value: string) => Date\n\n/**\n * Add an offset to a date. Callers supply timezone-aware calendar arithmetic.\n */\nexport type AddOffsetToDate = (date: Date, offset: number) => Date\n\n/**\n * Move a date to the start of its calendar day. Callers supply timezone-aware\n * behaviour (e.g. organisation timezone on the server, local timezone in the\n * browser).\n */\nexport type StartOfDay = (date: Date) => Date\n\n/**\n * Move a date to the end of its calendar day (last millisecond). Callers supply\n * timezone-aware behaviour (e.g. organisation timezone on the server, local\n * timezone in the browser).\n */\nexport type EndOfDay = (date: Date) => Date\n\nexport type FormElementsCtrl = {\n model: SubmissionTypes.S3SubmissionData['submission'] | undefined\n flattenedElements: FormTypes.FormElement[]\n parentFormElementsCtrl?: FormElementsCtrl\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -19,6 +19,16 @@ export * as formElementsService from './formElementsService.js';
|
|
|
19
19
|
* ```
|
|
20
20
|
*/
|
|
21
21
|
export * as conditionalLogicService from './conditionalLogicService.js';
|
|
22
|
+
/**
|
|
23
|
+
* ## Calculation Service
|
|
24
|
+
*
|
|
25
|
+
* Helper functions for evaluating form calculation expressions
|
|
26
|
+
*
|
|
27
|
+
* ```js
|
|
28
|
+
* import { calculationService } from '@oneblink/sdk-core'
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export * as calculationService from './calculationService.js';
|
|
22
32
|
/**
|
|
23
33
|
* ## Submission Service
|
|
24
34
|
*
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,16 @@ export * as formElementsService from './formElementsService.js';
|
|
|
19
19
|
* ```
|
|
20
20
|
*/
|
|
21
21
|
export * as conditionalLogicService from './conditionalLogicService.js';
|
|
22
|
+
/**
|
|
23
|
+
* ## Calculation Service
|
|
24
|
+
*
|
|
25
|
+
* Helper functions for evaluating form calculation expressions
|
|
26
|
+
*
|
|
27
|
+
* ```js
|
|
28
|
+
* import { calculationService } from '@oneblink/sdk-core'
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export * as calculationService from './calculationService.js';
|
|
22
32
|
/**
|
|
23
33
|
* ## Submission Service
|
|
24
34
|
*
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,uBAAuB,MAAM,iCAAiC,CAAA;AAC1E;;;;;;;;GAQG;AACH,OAAO,KAAK,mBAAmB,MAAM,0BAA0B,CAAA;AAC/D;;;;;;;;GAQG;AACH,OAAO,KAAK,uBAAuB,MAAM,8BAA8B,CAAA;AACvE;;;;;;;;GAQG;AACH,OAAO,KAAK,iBAAiB,MAAM,wBAAwB,CAAA;AAC3D;;;;;;;;GAQG;AACH,OAAO,KAAK,eAAe,MAAM,sBAAsB,CAAA;AACvD;;;;;;;;GAQG;AACH,OAAO,KAAK,WAAW,MAAM,kBAAkB,CAAA;AAC/C;;;;;;;;GAQG;AACH,OAAO,KAAK,UAAU,MAAM,iBAAiB,CAAA;AAC7C;;;;;;;;GAQG;AACH,OAAO,KAAK,cAAc,MAAM,qBAAqB,CAAA;AACrD;;;;;;;;GAQG;AACH,OAAO,KAAK,iBAAiB,MAAM,wBAAwB,CAAA;AAC3D;;;;;;;;GAQG;AACH,OAAO,KAAK,iBAAiB,MAAM,wBAAwB,CAAA;AAC3D;;;;;;;;GAQG;AACH,OAAO,KAAK,0BAA0B,MAAM,iCAAiC,CAAA;AAC7E;;;;;;;;GAQG;AACH,OAAO,KAAK,iBAAiB,MAAM,wBAAwB,CAAA;AAE3D;;;;;;;;GAQG;AACH,OAAO,KAAK,gBAAgB,MAAM,uBAAuB,CAAA;AAEzD;;;;;;;;GAQG;AACH,OAAO,KAAK,eAAe,MAAM,sBAAsB,CAAA;AAEvD;;;;;;;;GAQG;AACH,OAAO,KAAK,iBAAiB,MAAM,wBAAwB,CAAA","sourcesContent":["export * as resourceDefinitionTypes from './types/resource-definitions.js'\n/**\n * ## Form Elements Service\n *\n * Helper functions for handling form elements\n *\n * ```js\n * import { formElementsService } from '@oneblink/sdk-core'\n * ```\n */\nexport * as formElementsService from './formElementsService.js'\n/**\n * ## Conditional Logic Service\n *\n * Helper functions for handling conditional logic\n *\n * ```js\n * import { conditionalLogicService } from '@oneblink/sdk-core'\n * ```\n */\nexport * as conditionalLogicService from './conditionalLogicService.js'\n/**\n * ## Submission Service\n *\n * Helper functions for handling form submission data\n *\n * ```js\n * import { submissionService } from '@oneblink/sdk-core'\n * ```\n */\nexport * as submissionService from './submissionService.js'\n/**\n * ## Submission Service\n *\n * Helper functions for casting types\n *\n * ```js\n * import { typeCastService } from '@oneblink/sdk-core'\n * ```\n */\nexport * as typeCastService from './typeCastService.js'\n/**\n * ## User Service\n *\n * Helper functions for handling users\n *\n * ```js\n * import { userService } from '@oneblink/sdk-core'\n * ```\n */\nexport * as userService from './userService.js'\n/**\n * ## ABN Service\n *\n * Helper functions for handling ABN Records\n *\n * ```js\n * import { abnService } from '@oneblink/sdk-core'\n * ```\n */\nexport * as abnService from './abnService.js'\n/**\n * ## Payment Service\n *\n * Helper functions for payments\n *\n * ```js\n * import { paymentService } from '@oneblink/sdk-core'\n * ```\n */\nexport * as paymentService from './paymentService.js'\n/**\n * ## Scheduling Service\n *\n * Helper functions for scheduling\n *\n * ```js\n * import { schedulingService } from '@oneblink/sdk-core'\n * ```\n */\nexport * as schedulingService from './schedulingService.js'\n/**\n * ## File Upload Service\n *\n * Helper functions for handling file uploads\n *\n * ```js\n * import { fileUploadService } from '@oneblink/sdk-core'\n * ```\n */\nexport * as fileUploadService from './fileUploadService.js'\n/**\n * ## Webhook Subscription Service\n *\n * Helper functions for handling Webhook subscriptions\n *\n * ```js\n * import { webhookSubscriptionService } from '@oneblink/sdk-core'\n * ```\n */\nexport * as webhookSubscriptionService from './webhookSubscriptionService.js'\n/**\n * ## SharePoint Service\n *\n * Helper functions for handling SharePoint\n *\n * ```js\n * import { sharepointService } from '@oneblink/sdk-core'\n * ```\n */\nexport * as sharepointService from './sharepointService.js'\n\n/**\n * ## Freshdesk Service\n *\n * Helper functions for handling Freshdesk\n *\n * ```js\n * import { freshdeskService } from '@oneblink/sdk-core'\n * ```\n */\nexport * as freshdeskService from './freshdeskService.js'\n\n/**\n * ## GoodToGo Service\n *\n * Helper functions for handling GoodToGo\n *\n * ```js\n * import { goodToGoService } from '@oneblink/sdk-core'\n * ```\n */\nexport * as goodToGoService from './goodToGoService.js'\n\n/**\n * ## Salesforce Service\n *\n * Helper functions for handling Salesforce\n *\n * ```js\n * import { salesForceService } from '@oneblink/sdk-core'\n * ```\n */\nexport * as salesforceService from './salesforceService.js'\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,uBAAuB,MAAM,iCAAiC,CAAA;AAC1E;;;;;;;;GAQG;AACH,OAAO,KAAK,mBAAmB,MAAM,0BAA0B,CAAA;AAC/D;;;;;;;;GAQG;AACH,OAAO,KAAK,uBAAuB,MAAM,8BAA8B,CAAA;AACvE;;;;;;;;GAQG;AACH,OAAO,KAAK,kBAAkB,MAAM,yBAAyB,CAAA;AAC7D;;;;;;;;GAQG;AACH,OAAO,KAAK,iBAAiB,MAAM,wBAAwB,CAAA;AAC3D;;;;;;;;GAQG;AACH,OAAO,KAAK,eAAe,MAAM,sBAAsB,CAAA;AACvD;;;;;;;;GAQG;AACH,OAAO,KAAK,WAAW,MAAM,kBAAkB,CAAA;AAC/C;;;;;;;;GAQG;AACH,OAAO,KAAK,UAAU,MAAM,iBAAiB,CAAA;AAC7C;;;;;;;;GAQG;AACH,OAAO,KAAK,cAAc,MAAM,qBAAqB,CAAA;AACrD;;;;;;;;GAQG;AACH,OAAO,KAAK,iBAAiB,MAAM,wBAAwB,CAAA;AAC3D;;;;;;;;GAQG;AACH,OAAO,KAAK,iBAAiB,MAAM,wBAAwB,CAAA;AAC3D;;;;;;;;GAQG;AACH,OAAO,KAAK,0BAA0B,MAAM,iCAAiC,CAAA;AAC7E;;;;;;;;GAQG;AACH,OAAO,KAAK,iBAAiB,MAAM,wBAAwB,CAAA;AAE3D;;;;;;;;GAQG;AACH,OAAO,KAAK,gBAAgB,MAAM,uBAAuB,CAAA;AAEzD;;;;;;;;GAQG;AACH,OAAO,KAAK,eAAe,MAAM,sBAAsB,CAAA;AAEvD;;;;;;;;GAQG;AACH,OAAO,KAAK,iBAAiB,MAAM,wBAAwB,CAAA","sourcesContent":["export * as resourceDefinitionTypes from './types/resource-definitions.js'\n/**\n * ## Form Elements Service\n *\n * Helper functions for handling form elements\n *\n * ```js\n * import { formElementsService } from '@oneblink/sdk-core'\n * ```\n */\nexport * as formElementsService from './formElementsService.js'\n/**\n * ## Conditional Logic Service\n *\n * Helper functions for handling conditional logic\n *\n * ```js\n * import { conditionalLogicService } from '@oneblink/sdk-core'\n * ```\n */\nexport * as conditionalLogicService from './conditionalLogicService.js'\n/**\n * ## Calculation Service\n *\n * Helper functions for evaluating form calculation expressions\n *\n * ```js\n * import { calculationService } from '@oneblink/sdk-core'\n * ```\n */\nexport * as calculationService from './calculationService.js'\n/**\n * ## Submission Service\n *\n * Helper functions for handling form submission data\n *\n * ```js\n * import { submissionService } from '@oneblink/sdk-core'\n * ```\n */\nexport * as submissionService from './submissionService.js'\n/**\n * ## Submission Service\n *\n * Helper functions for casting types\n *\n * ```js\n * import { typeCastService } from '@oneblink/sdk-core'\n * ```\n */\nexport * as typeCastService from './typeCastService.js'\n/**\n * ## User Service\n *\n * Helper functions for handling users\n *\n * ```js\n * import { userService } from '@oneblink/sdk-core'\n * ```\n */\nexport * as userService from './userService.js'\n/**\n * ## ABN Service\n *\n * Helper functions for handling ABN Records\n *\n * ```js\n * import { abnService } from '@oneblink/sdk-core'\n * ```\n */\nexport * as abnService from './abnService.js'\n/**\n * ## Payment Service\n *\n * Helper functions for payments\n *\n * ```js\n * import { paymentService } from '@oneblink/sdk-core'\n * ```\n */\nexport * as paymentService from './paymentService.js'\n/**\n * ## Scheduling Service\n *\n * Helper functions for scheduling\n *\n * ```js\n * import { schedulingService } from '@oneblink/sdk-core'\n * ```\n */\nexport * as schedulingService from './schedulingService.js'\n/**\n * ## File Upload Service\n *\n * Helper functions for handling file uploads\n *\n * ```js\n * import { fileUploadService } from '@oneblink/sdk-core'\n * ```\n */\nexport * as fileUploadService from './fileUploadService.js'\n/**\n * ## Webhook Subscription Service\n *\n * Helper functions for handling Webhook subscriptions\n *\n * ```js\n * import { webhookSubscriptionService } from '@oneblink/sdk-core'\n * ```\n */\nexport * as webhookSubscriptionService from './webhookSubscriptionService.js'\n/**\n * ## SharePoint Service\n *\n * Helper functions for handling SharePoint\n *\n * ```js\n * import { sharepointService } from '@oneblink/sdk-core'\n * ```\n */\nexport * as sharepointService from './sharepointService.js'\n\n/**\n * ## Freshdesk Service\n *\n * Helper functions for handling Freshdesk\n *\n * ```js\n * import { freshdeskService } from '@oneblink/sdk-core'\n * ```\n */\nexport * as freshdeskService from './freshdeskService.js'\n\n/**\n * ## GoodToGo Service\n *\n * Helper functions for handling GoodToGo\n *\n * ```js\n * import { goodToGoService } from '@oneblink/sdk-core'\n * ```\n */\nexport * as goodToGoService from './goodToGoService.js'\n\n/**\n * ## Salesforce Service\n *\n * Helper functions for handling Salesforce\n *\n * ```js\n * import { salesForceService } from '@oneblink/sdk-core'\n * ```\n */\nexport * as salesforceService from './salesforceService.js'\n"]}
|
package/dist/paymentService.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { conditionalLogicService, formElementsService } from './index.js';
|
|
1
|
+
import { calculationService, conditionalLogicService, formElementsService, } from './index.js';
|
|
2
2
|
import { getRootElementValueById } from './submissionService.js';
|
|
3
3
|
import getPaymentProvider from './paymentService/getPaymentProvider.js';
|
|
4
4
|
/**
|
|
@@ -52,26 +52,80 @@ export function checkForPaymentEvent({ definition, submission, submissionTimesta
|
|
|
52
52
|
return;
|
|
53
53
|
}
|
|
54
54
|
console.log('Checking if submission with payment submission event needs processing');
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const amount = getRootElementValueById(amountElement.id, definition.elements, submission);
|
|
55
|
+
const amount = resolvePaymentEventAmount({
|
|
56
|
+
configuration: paymentSubmissionEvent.configuration,
|
|
57
|
+
definition,
|
|
58
|
+
submission,
|
|
59
|
+
parseDayOnlyDate,
|
|
60
|
+
});
|
|
62
61
|
if (!amount) {
|
|
63
62
|
console.log('Form has a payment submission event but the amount has been entered as 0 or not at all, finishing as normal submission');
|
|
64
63
|
return;
|
|
65
64
|
}
|
|
66
65
|
if (typeof amount !== 'number') {
|
|
67
66
|
console.log('Form has a payment submission event but the amount is not a number, throwing error');
|
|
68
|
-
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.');
|
|
69
68
|
}
|
|
70
|
-
|
|
69
|
+
return {
|
|
71
70
|
paymentSubmissionEvent,
|
|
72
71
|
amount,
|
|
73
72
|
};
|
|
74
|
-
|
|
73
|
+
}
|
|
74
|
+
function resolvePaymentEventAmount({ configuration, definition, submission, parseDayOnlyDate, }) {
|
|
75
|
+
switch (configuration.amountType) {
|
|
76
|
+
case 'NUMBER': {
|
|
77
|
+
console.log('Using fixed payment amount from payment submission event configuration', configuration.paymentAmount);
|
|
78
|
+
return configuration.paymentAmount;
|
|
79
|
+
}
|
|
80
|
+
case 'EXPRESSION': {
|
|
81
|
+
console.log('Evaluating payment calculation from payment submission event configuration', configuration.paymentCalculation);
|
|
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({
|
|
91
|
+
expression: configuration.paymentCalculation,
|
|
92
|
+
submission,
|
|
93
|
+
formElements: definition.elements,
|
|
94
|
+
parseDayOnlyDate,
|
|
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
|
+
}
|
|
113
|
+
}
|
|
114
|
+
case 'FORM_ELEMENT':
|
|
115
|
+
default: {
|
|
116
|
+
if (!configuration.elementId) {
|
|
117
|
+
console.log('Form has a payment submission event but the amount configuration is missing, throwing error');
|
|
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.');
|
|
119
|
+
}
|
|
120
|
+
const amountElement = formElementsService.findFormElement(definition.elements, (element) => element.id === configuration.elementId);
|
|
121
|
+
if (!amountElement) {
|
|
122
|
+
console.log('Form has a payment submission event but the amount element does not exist, throwing error');
|
|
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.');
|
|
124
|
+
}
|
|
125
|
+
console.log('Found form element for payment submission event', amountElement);
|
|
126
|
+
return getRootElementValueById(amountElement.id, definition.elements, submission);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
75
129
|
}
|
|
76
130
|
/**
|
|
77
131
|
* Retrieve an array of detail items from a form submission payment.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"paymentService.js","sourceRoot":"","sources":["../src/paymentService.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,uBAAuB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AACzE,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,aAAa,GAAG,mBAAmB,CAAC,eAAe,CACvD,UAAU,CAAC,QAAQ,EACnB,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,KAAK,sBAAsB,CAAC,aAAa,CAAC,SAAS,CAC3E,CAAA;IACD,IAAI,CAAC,aAAa,IAAI,aAAa,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QACpD,OAAO,CAAC,GAAG,CACT,2FAA2F,CAC5F,CAAA;QACD,MAAM,IAAI,KAAK,CACb,6KAA6K,CAC9K,CAAA;IACH,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,iDAAiD,EAAE,aAAa,CAAC,CAAA;IAE7E,MAAM,MAAM,GAAG,uBAAuB,CACpC,aAAa,CAAC,EAAE,EAChB,UAAU,CAAC,QAAQ,EACnB,UAAU,CACX,CAAA;IAED,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,MAAM,MAAM,GAAG;QACb,sBAAsB;QACtB,MAAM;KACP,CAAA;IAED,OAAO,MAAM,CAAA;AACf,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 { conditionalLogicService, formElementsService } 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 amountElement = formElementsService.findFormElement(\n definition.elements,\n (element) => element.id === paymentSubmissionEvent.configuration.elementId,\n )\n if (!amountElement || amountElement.type === 'page') {\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('Found form element for payment submission event', amountElement)\n\n const amount = getRootElementValueById(\n amountElement.id,\n definition.elements,\n submission,\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 const result = {\n paymentSubmissionEvent,\n amount,\n }\n\n return result\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,11 +1,14 @@
|
|
|
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.
|
|
4
|
+
"version": "10.1.0-beta.3",
|
|
5
5
|
"author": "OneBlink <developers@oneblink.io> (https://oneblink.io)",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/oneblink/sdk-core-js/issues"
|
|
8
8
|
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"morph-expressions": "^1.1.1"
|
|
11
|
+
},
|
|
9
12
|
"devDependencies": {
|
|
10
13
|
"@eslint/eslintrc": "^3.3.3",
|
|
11
14
|
"@eslint/js": "^9.39.2",
|