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

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.
Files changed (33) hide show
  1. package/dist/calculationService.d.ts +144 -0
  2. package/dist/calculationService.js +297 -0
  3. package/dist/calculationService.js.map +1 -0
  4. package/dist/conditionalLogicService/evaluateConditionalSubmissionTimestampPredicate.js +1 -1
  5. package/dist/conditionalLogicService/evaluateConditionalSubmissionTimestampPredicate.js.map +1 -1
  6. package/dist/conditionalLogicService/types.d.ts +5 -0
  7. package/dist/conditionalLogicService/types.js +5 -1
  8. package/dist/conditionalLogicService/types.js.map +1 -1
  9. package/dist/index.d.ts +10 -0
  10. package/dist/index.js +10 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/paymentService/getPaymentProvider.d.ts +9 -0
  13. package/dist/paymentService/getPaymentProvider.js +20 -0
  14. package/dist/paymentService/getPaymentProvider.js.map +1 -0
  15. package/dist/paymentService/providers/bpoint.d.ts +7 -0
  16. package/dist/paymentService/providers/bpoint.js +76 -0
  17. package/dist/paymentService/providers/bpoint.js.map +1 -0
  18. package/dist/paymentService/providers/cpPay.d.ts +7 -0
  19. package/dist/paymentService/providers/cpPay.js +119 -0
  20. package/dist/paymentService/providers/cpPay.js.map +1 -0
  21. package/dist/paymentService/providers/nswGovPay.d.ts +7 -0
  22. package/dist/paymentService/providers/nswGovPay.js +90 -0
  23. package/dist/paymentService/providers/nswGovPay.js.map +1 -0
  24. package/dist/paymentService/providers/westpacQuickStream.d.ts +7 -0
  25. package/dist/paymentService/providers/westpacQuickStream.js +64 -0
  26. package/dist/paymentService/providers/westpacQuickStream.js.map +1 -0
  27. package/dist/paymentService/types.d.ts +25 -0
  28. package/dist/paymentService/types.js +2 -0
  29. package/dist/paymentService/types.js.map +1 -0
  30. package/dist/paymentService.d.ts +19 -15
  31. package/dist/paymentService.js +61 -296
  32. package/dist/paymentService.js.map +1 -1
  33. package/package.json +4 -1
@@ -0,0 +1,144 @@
1
+ import { FormTypes, SubmissionTypes } from '@oneblink/types';
2
+ import { type ParseDayOnlyDate } from './conditionalLogicService/types.js';
3
+ export type { ParseDayOnlyDate } from './conditionalLogicService/types.js';
4
+ /**
5
+ * Evaluate a OneBlink calculation expression against form submission data.
6
+ *
7
+ * Day-only (`YYYY-MM-DD`) date parsing is injected via `parseDayOnlyDate` so
8
+ * callers control timezone-aware behaviour (client vs server), matching the
9
+ * pattern used by `conditionalLogicService`. Other date strings use
10
+ * `new Date(value)`.
11
+ *
12
+ * #### Expression syntax
13
+ *
14
+ * Expressions are mathematical / logical formulas. Form element values are
15
+ * referenced with `{ELEMENT:<elementName>}` tags (including nested paths via
16
+ * `|`, e.g. `{ELEMENT:Children|Age}`).
17
+ *
18
+ * ##### Literals
19
+ *
20
+ * - **Numbers** — e.g. `0`, `5`, `5.4`
21
+ * - **Strings** — single or double quoted, e.g. `"abc"`, `'abc'`
22
+ * - **Booleans** — `true`, `false`
23
+ *
24
+ * ##### Arithmetic operators
25
+ *
26
+ * | Operator | Description | Example |
27
+ * | -------- | ------------------------------------ | -------------- |
28
+ * | `+` | Addition (also unary plus) | `1 + 2` → `3` |
29
+ * | `-` | Subtraction (also unary minus) | `1 - 2` → `-1` |
30
+ * | `*` | Multiplication | `4 * 2` → `8` |
31
+ * | `/` | Division | `4 / 2` → `2` |
32
+ * | `%` | Remainder | `8 % 3` → `2` |
33
+ * | `++` | Unary increment | `++2` → `3` |
34
+ * | `--` | Unary decrement | `--2` → `1` |
35
+ *
36
+ * Standard operator precedence applies. Use parentheses to override it, e.g.
37
+ * `3 * (2 + 1)` → `9`.
38
+ *
39
+ * ##### Comparison operators
40
+ *
41
+ * | Operator | Description | Example |
42
+ * | -------- | --------------------- | ------------------- |
43
+ * | `==` | Equal | `2 == 2` → `true` |
44
+ * | `!=` | Not equal | `2 != 3` → `true` |
45
+ * | `>` | Greater than | `2 > 1` → `true` |
46
+ * | `>=` | Greater than or equal | `2 >= 2` → `true` |
47
+ * | `<` | Less than | `2 < 3` → `true` |
48
+ * | `<=` | Less than or equal | `2 <= 2` → `true` |
49
+ *
50
+ * ##### Logical operators
51
+ *
52
+ * | Operator | Description | Example |
53
+ * | -------- | ------------------------------------ | -------------------------- |
54
+ * | `&&` | Logical AND (returns last operand) | `true && false` → `false` |
55
+ * | `\|\|` | Logical OR (returns first truthy) | `false \|\| true` → `true` |
56
+ * | `!` | Logical NOT | `!false` → `true` |
57
+ *
58
+ * ##### Element references
59
+ *
60
+ * - `{ELEMENT:Number}` — value of the root element named `Number`
61
+ * - `{ELEMENT:Set\|Child}` — value of `Child` within repeatable set / nested
62
+ * form element `Set`
63
+ * - Nested paths may include multiple segments:
64
+ * `{ELEMENT:Parents\|Children\|Age}`
65
+ *
66
+ * How values are coerced for evaluation:
67
+ *
68
+ * - Numbers are used as-is
69
+ * - Day-only (`YYYY-MM-DD`) strings are parsed via `parseDayOnlyDate`; other
70
+ * non-numeric date strings use `new Date(value)`. Valid dates use
71
+ * `Date#getTime()`, otherwise the string is parsed as a float via
72
+ * `parseFloat`
73
+ * - Arrays of numeric strings (e.g. checkboxes) are summed
74
+ * - Repeatable set entries sum the referenced nested numeric values
75
+ * - Empty arrays yield `NaN` (calculation result becomes `undefined`)
76
+ * - Compliance element objects use their `value` property
77
+ *
78
+ * ##### Built-in functions
79
+ *
80
+ * | Function | Description | Example |
81
+ * | -------------------------------- | --------------------------------------------------------------------------- | ------------------------------- |
82
+ * | `ROUND(value, precision)` | Round `value` to `precision` decimal places (correct floating-point rounds) | `ROUND(1.255, 2)` → `1.26` |
83
+ * | `ROUND_DOWN(value)` | Round down to the nearest integer (`Math.floor`) | `ROUND_DOWN(1.9)` → `1` |
84
+ * | `ROUND_UP(value)` | Round up to the nearest integer (`Math.ceil`) | `ROUND_UP(1.1)` → `2` |
85
+ * | `ISNULL(value, defaultValue)` | If `value` is unentered (`null`/`undefined`/`""`), return `defaultValue` or `0`; otherwise return `value` | `ISNULL({ELEMENT:A}, 10)` |
86
+ *
87
+ * `ROUND`, `ROUND_DOWN`, and `ROUND_UP` return `null` when `value` is `NaN` or
88
+ * not finite.
89
+ *
90
+ * ##### Examples
91
+ *
92
+ * ```js
93
+ * const value = calculationService.evaluateExpression({
94
+ * expression: '{ELEMENT:Quantity} * {ELEMENT:Price}',
95
+ * submission: { Quantity: 3, Price: 12.5 },
96
+ * formElements: form.elements,
97
+ * // Client: local timezone. Server: organisation timezone.
98
+ * parseDayOnlyDate: (value) => new Date(`${value}T00:00:00.000Z`),
99
+ * })
100
+ * // value === 37.5
101
+ * ```
102
+ *
103
+ * ```js
104
+ * calculationService.evaluateExpression({
105
+ * expression: 'ROUND({ELEMENT:Amount} * 1.1, 2)',
106
+ * submission: { Amount: 100 },
107
+ * formElements: form.elements,
108
+ * parseDayOnlyDate: (value) => new Date(`${value}T00:00:00.000Z`),
109
+ * })
110
+ * // 110
111
+ * ```
112
+ *
113
+ * ```js
114
+ * calculationService.evaluateExpression({
115
+ * expression: 'ISNULL({ELEMENT:Optional}, 0) + {ELEMENT:Required}',
116
+ * submission: { Required: 5 },
117
+ * formElements: form.elements,
118
+ * parseDayOnlyDate: (value) => new Date(`${value}T00:00:00.000Z`),
119
+ * })
120
+ * // 5
121
+ * ```
122
+ *
123
+ * @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.
127
+ */
128
+ export declare function evaluateExpression({ expression, submission, formElements, parseDayOnlyDate, }: {
129
+ /** The calculation expression string to evaluate */
130
+ expression: string;
131
+ /** Form submission data used to resolve `{ELEMENT:...}` references */
132
+ submission: SubmissionTypes.S3SubmissionData['submission'];
133
+ /**
134
+ * Form elements definition. Used to resolve nested `form` element values
135
+ * referenced in the expression.
136
+ */
137
+ formElements: FormTypes.FormElement[];
138
+ /**
139
+ * Parse `YYYY-MM-DD` strings when resolving date element values. sdk-core
140
+ * only calls this for day-only values; other date strings use
141
+ * `new Date(value)`.
142
+ */
143
+ parseDayOnlyDate: ParseDayOnlyDate;
144
+ }): number | undefined;
@@ -0,0 +1,297 @@
1
+ import ExpressionParser from 'morph-expressions';
2
+ import { findFormElement, 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 resolveElementValue({ submission, nestedElementNames, formElements, parseDayOnlyDate, }) {
26
+ const defaultAccumulator = submission[nestedElementNames[0]];
27
+ return nestedElementNames.reduce((elementValue, elementName, index) => {
28
+ // Numbers can just be returned as is
29
+ if (typeof elementValue === 'number') {
30
+ return elementValue;
31
+ }
32
+ // Attempt to get a number from the element value as a string.
33
+ // NaN is accounted for in the calculation so we can return that from here.
34
+ if (typeof elementValue === 'string') {
35
+ // Day-only (`YYYY-MM-DD`) strings are parsed via parseDayOnlyDate so
36
+ // callers control timezone. Other date strings use `new Date(value)`.
37
+ // Numeric strings (e.g. `"10"`) fall through to parseFloat — `Date`
38
+ // can otherwise treat them as valid dates.
39
+ const isDayOnly = DATE_ONLY_PATTERN.test(elementValue);
40
+ if (isDayOnly) {
41
+ const parsedDate = parseDayOnlyDate(elementValue);
42
+ if (!Number.isNaN(parsedDate.getTime())) {
43
+ return parsedDate.getTime();
44
+ }
45
+ }
46
+ else if (Number.isNaN(Number(elementValue))) {
47
+ const parsedDate = new Date(elementValue);
48
+ if (!Number.isNaN(parsedDate.getTime())) {
49
+ return parsedDate.getTime();
50
+ }
51
+ }
52
+ return parseFloat(elementValue);
53
+ }
54
+ if (Array.isArray(elementValue)) {
55
+ // If there are no entries, return NaN to prevent the calculation from
56
+ // running.
57
+ if (!elementValue.length) {
58
+ return NaN;
59
+ }
60
+ // An array could be an element that allows multiple values e.g.
61
+ // checkboxes. If that is the case, add them all together and move on.
62
+ const elementValues = elementValue.map((entry) => parseFloat(entry));
63
+ if (elementValues.every((entry) => !Number.isNaN(entry))) {
64
+ return elementValues.reduce((number, entry) => number + entry, 0);
65
+ }
66
+ // Otherwise attempt to process it as a repeatable set. If we found
67
+ // another repeatable set to process, pass it to the next element name
68
+ // to iterate over the entries. If we are processing the entries in a
69
+ // repeatable set, we can sum the number elements in the entries.
70
+ const nextElementName = nestedElementNames[index + 1];
71
+ let isNestedRepeatableSet = false;
72
+ const nestedElementValues = elementValue.reduce((nestedElementValues, entry) => {
73
+ if (entry) {
74
+ const nextElementValue = entry[nextElementName];
75
+ if (Array.isArray(nextElementValue)) {
76
+ if (nextElementValue.length) {
77
+ nestedElementValues.push(...nextElementValue);
78
+ isNestedRepeatableSet = true;
79
+ }
80
+ }
81
+ else {
82
+ nestedElementValues.push(nextElementValue);
83
+ }
84
+ }
85
+ return nestedElementValues;
86
+ }, []);
87
+ // If the nested element values are all arrays, pass them on to the
88
+ // next iteration.
89
+ if (isNestedRepeatableSet) {
90
+ return nestedElementValues;
91
+ }
92
+ return nestedElementValues.reduce((total, nestedElementValue) => {
93
+ if (Number.isNaN(total)) {
94
+ return NaN;
95
+ }
96
+ const value = parseFloat(nestedElementValue);
97
+ if (Number.isNaN(value)) {
98
+ return NaN;
99
+ }
100
+ return total + value;
101
+ }, 0);
102
+ }
103
+ // If the value is an object, take the element name and check to see if
104
+ // this element is a nested form element. If so, take the next nested
105
+ // element name, find its value in the object and return it for the next
106
+ // iteration to handle.
107
+ if (typeof elementValue === 'object') {
108
+ const formFormElement = findFormElement(formElements, (e) => e.type === 'form' && e.name === nestedElementNames[index]);
109
+ const nextElementName = nestedElementNames[index + 1];
110
+ if (formFormElement && nextElementName) {
111
+ return elementValue[nextElementName];
112
+ }
113
+ }
114
+ // "compliance" form element has an object value with a "value" property.
115
+ if (isObjectWithValue(elementValue) &&
116
+ typeof elementValue.value === 'string') {
117
+ return parseFloat(elementValue.value);
118
+ }
119
+ // We did not find a number value from the known elements, assume we are
120
+ // at the end of the line.
121
+ return NaN;
122
+ }, defaultAccumulator);
123
+ }
124
+ /**
125
+ * Evaluate a OneBlink calculation expression against form submission data.
126
+ *
127
+ * Day-only (`YYYY-MM-DD`) date parsing is injected via `parseDayOnlyDate` so
128
+ * callers control timezone-aware behaviour (client vs server), matching the
129
+ * pattern used by `conditionalLogicService`. Other date strings use
130
+ * `new Date(value)`.
131
+ *
132
+ * #### Expression syntax
133
+ *
134
+ * Expressions are mathematical / logical formulas. Form element values are
135
+ * referenced with `{ELEMENT:<elementName>}` tags (including nested paths via
136
+ * `|`, e.g. `{ELEMENT:Children|Age}`).
137
+ *
138
+ * ##### Literals
139
+ *
140
+ * - **Numbers** — e.g. `0`, `5`, `5.4`
141
+ * - **Strings** — single or double quoted, e.g. `"abc"`, `'abc'`
142
+ * - **Booleans** — `true`, `false`
143
+ *
144
+ * ##### Arithmetic operators
145
+ *
146
+ * | Operator | Description | Example |
147
+ * | -------- | ------------------------------------ | -------------- |
148
+ * | `+` | Addition (also unary plus) | `1 + 2` → `3` |
149
+ * | `-` | Subtraction (also unary minus) | `1 - 2` → `-1` |
150
+ * | `*` | Multiplication | `4 * 2` → `8` |
151
+ * | `/` | Division | `4 / 2` → `2` |
152
+ * | `%` | Remainder | `8 % 3` → `2` |
153
+ * | `++` | Unary increment | `++2` → `3` |
154
+ * | `--` | Unary decrement | `--2` → `1` |
155
+ *
156
+ * Standard operator precedence applies. Use parentheses to override it, e.g.
157
+ * `3 * (2 + 1)` → `9`.
158
+ *
159
+ * ##### Comparison operators
160
+ *
161
+ * | Operator | Description | Example |
162
+ * | -------- | --------------------- | ------------------- |
163
+ * | `==` | Equal | `2 == 2` → `true` |
164
+ * | `!=` | Not equal | `2 != 3` → `true` |
165
+ * | `>` | Greater than | `2 > 1` → `true` |
166
+ * | `>=` | Greater than or equal | `2 >= 2` → `true` |
167
+ * | `<` | Less than | `2 < 3` → `true` |
168
+ * | `<=` | Less than or equal | `2 <= 2` → `true` |
169
+ *
170
+ * ##### Logical operators
171
+ *
172
+ * | Operator | Description | Example |
173
+ * | -------- | ------------------------------------ | -------------------------- |
174
+ * | `&&` | Logical AND (returns last operand) | `true && false` → `false` |
175
+ * | `\|\|` | Logical OR (returns first truthy) | `false \|\| true` → `true` |
176
+ * | `!` | Logical NOT | `!false` → `true` |
177
+ *
178
+ * ##### Element references
179
+ *
180
+ * - `{ELEMENT:Number}` — value of the root element named `Number`
181
+ * - `{ELEMENT:Set\|Child}` — value of `Child` within repeatable set / nested
182
+ * form element `Set`
183
+ * - Nested paths may include multiple segments:
184
+ * `{ELEMENT:Parents\|Children\|Age}`
185
+ *
186
+ * How values are coerced for evaluation:
187
+ *
188
+ * - Numbers are used as-is
189
+ * - Day-only (`YYYY-MM-DD`) strings are parsed via `parseDayOnlyDate`; other
190
+ * non-numeric date strings use `new Date(value)`. Valid dates use
191
+ * `Date#getTime()`, otherwise the string is parsed as a float via
192
+ * `parseFloat`
193
+ * - Arrays of numeric strings (e.g. checkboxes) are summed
194
+ * - Repeatable set entries sum the referenced nested numeric values
195
+ * - Empty arrays yield `NaN` (calculation result becomes `undefined`)
196
+ * - Compliance element objects use their `value` property
197
+ *
198
+ * ##### Built-in functions
199
+ *
200
+ * | Function | Description | Example |
201
+ * | -------------------------------- | --------------------------------------------------------------------------- | ------------------------------- |
202
+ * | `ROUND(value, precision)` | Round `value` to `precision` decimal places (correct floating-point rounds) | `ROUND(1.255, 2)` → `1.26` |
203
+ * | `ROUND_DOWN(value)` | Round down to the nearest integer (`Math.floor`) | `ROUND_DOWN(1.9)` → `1` |
204
+ * | `ROUND_UP(value)` | Round up to the nearest integer (`Math.ceil`) | `ROUND_UP(1.1)` → `2` |
205
+ * | `ISNULL(value, defaultValue)` | If `value` is unentered (`null`/`undefined`/`""`), return `defaultValue` or `0`; otherwise return `value` | `ISNULL({ELEMENT:A}, 10)` |
206
+ *
207
+ * `ROUND`, `ROUND_DOWN`, and `ROUND_UP` return `null` when `value` is `NaN` or
208
+ * not finite.
209
+ *
210
+ * ##### Examples
211
+ *
212
+ * ```js
213
+ * const value = calculationService.evaluateExpression({
214
+ * expression: '{ELEMENT:Quantity} * {ELEMENT:Price}',
215
+ * submission: { Quantity: 3, Price: 12.5 },
216
+ * formElements: form.elements,
217
+ * // Client: local timezone. Server: organisation timezone.
218
+ * parseDayOnlyDate: (value) => new Date(`${value}T00:00:00.000Z`),
219
+ * })
220
+ * // value === 37.5
221
+ * ```
222
+ *
223
+ * ```js
224
+ * calculationService.evaluateExpression({
225
+ * expression: 'ROUND({ELEMENT:Amount} * 1.1, 2)',
226
+ * submission: { Amount: 100 },
227
+ * formElements: form.elements,
228
+ * parseDayOnlyDate: (value) => new Date(`${value}T00:00:00.000Z`),
229
+ * })
230
+ * // 110
231
+ * ```
232
+ *
233
+ * ```js
234
+ * calculationService.evaluateExpression({
235
+ * expression: 'ISNULL({ELEMENT:Optional}, 0) + {ELEMENT:Required}',
236
+ * submission: { Required: 5 },
237
+ * formElements: form.elements,
238
+ * parseDayOnlyDate: (value) => new Date(`${value}T00:00:00.000Z`),
239
+ * })
240
+ * // 5
241
+ * ```
242
+ *
243
+ * @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.
247
+ */
248
+ export function evaluateExpression({ expression, submission, formElements, parseDayOnlyDate, }) {
249
+ if (!expression) {
250
+ throw new Error('Expression is required.');
251
+ }
252
+ const exprParser = new ExpressionParser();
253
+ exprParser.registerFunction('ROUND', (value, precision) => {
254
+ if (!Number.isNaN(value) && Number.isFinite(value)) {
255
+ return parseFloat(roundToFixed(value, precision));
256
+ }
257
+ return null;
258
+ });
259
+ exprParser.registerFunction('ROUND_DOWN', (value) => {
260
+ if (!Number.isNaN(value) && Number.isFinite(value)) {
261
+ return Math.floor(value);
262
+ }
263
+ return null;
264
+ });
265
+ exprParser.registerFunction('ROUND_UP', (value) => {
266
+ if (!Number.isNaN(value) && Number.isFinite(value)) {
267
+ return Math.ceil(value);
268
+ }
269
+ return null;
270
+ });
271
+ exprParser.registerFunction('ISNULL', (value, defaultValue) => {
272
+ if (isUnenteredValue(value)) {
273
+ return defaultValue || 0;
274
+ }
275
+ return value;
276
+ });
277
+ const elementNames = [];
278
+ matchElementsTagRegex(expression, ({ elementName }) => {
279
+ elementNames.push(elementName);
280
+ });
281
+ const code = elementNames.reduce((code, elementName, index) => {
282
+ const regex = new RegExp(escapeRegExp(`{ELEMENT:${elementName}}`), 'g');
283
+ const replacement = `a${index}`;
284
+ exprParser.registerProperty(replacement, (submissionScope) => resolveElementValue({
285
+ submission: submissionScope,
286
+ nestedElementNames: elementName.split('|'),
287
+ formElements,
288
+ parseDayOnlyDate,
289
+ }));
290
+ return code.replace(regex, replacement);
291
+ }, expression);
292
+ const result = exprParser.parse(code.trim()).eval(submission);
293
+ if (typeof result === 'number' && !Number.isNaN(result)) {
294
+ return result;
295
+ }
296
+ }
297
+ //# 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 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,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":"AAQA,OAAO,EAAE,kBAAkB,EAAE,MAAM,8CAA8C,CAAA;AAEjF,MAAM,iBAAiB,GAAG,qBAAqB,CAAA;AAI/C,SAAS,gBAAgB,CACvB,SAAuD,EACvD,gBAAkC,EAClC,gBAAkC,EAClC,aAA8B,EAC9B,UAAsB,EACtB,QAAkB,EAClB,WAAwB;IAExB,IAAI,UAA8B,CAAA;IAElC,IAAI,SAAS,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;QACxC,MAAM,eAAe,GAAG,kBAAkB,CACxC,gBAAgB,EAChB,SAAS,CAAC,SAAS,CACpB,CAAA;QACD,IAAI,OAAO,eAAe,CAAC,KAAK,KAAK,QAAQ,IAAI,eAAe,CAAC,KAAK,EAAE,CAAC;YACvE,UAAU,GAAG,eAAe,CAAC,KAAK,CAAA;QACpC,CAAC;aAAM,CAAC;YACN,OAAO,SAAS,CAAA;QAClB,CAAC;IACH,CAAC;SAAM,CAAC;QACN,UAAU,GAAG,SAAS,CAAC,KAAK,CAAA;IAC9B,CAAC;IAED,MAAM,SAAS,GAAG,iBAAiB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;IACpD,MAAM,IAAI,GAAG,SAAS,CAAC,CAAC,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,CAAA;IAC5E,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC;QACjC,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,MAAM,cAAc,GAClB,OAAO,SAAS,CAAC,UAAU,KAAK,QAAQ;QACxC,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,UAAU,CAAC;QAChC,CAAC,CAAC,IAAI;QACN,CAAC,CAAC,aAAa,CAAC,IAAI,EAAE,SAAS,CAAC,UAAU,CAAC,CAAA;IAE/C,IAAI,SAAS,EAAE,CAAC;QACd,IAAI,WAAW,KAAK,OAAO,EAAE,CAAC;YAC5B,OAAO,UAAU,CAAC,cAAc,CAAC,CAAA;QACnC,CAAC;aAAM,CAAC;YACN,OAAO,QAAQ,CAAC,cAAc,CAAC,CAAA;QACjC,CAAC;IACH,CAAC;IAED,OAAO,cAAc,CAAA;AACvB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,OAAO,UAAU,+CAA+C,CAAC,EACtE,SAAS,EACT,gBAAgB,EAChB,mBAAmB,EACnB,gBAAgB,EAChB,aAAa,EACb,UAAU,EACV,QAAQ,GAST;IACC,MAAM,cAAc,GAAG,IAAI,IAAI,CAAC,mBAAmB,CAAC,CAAA;IACpD,IAAI,MAAM,CAAC,KAAK,CAAC,cAAc,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC;QAC3C,OAAO,KAAK,CAAA;IACd,CAAC;IAED,MAAM,cAAc,GAAG,cAAc,CAAC,OAAO,EAAE,CAAA;IAE/C,QAAQ,SAAS,CAAC,QAAQ,EAAE,CAAC;QAC3B,KAAK,SAAS,CAAC,CAAC,CAAC;YACf,MAAM,GAAG,GAAG,gBAAgB,CAC1B,SAAS,CAAC,GAAG,EACb,gBAAgB,EAChB,gBAAgB,EAChB,aAAa,EACb,UAAU,EACV,QAAQ,EACR,OAAO,CACR,CAAA;YACD,MAAM,GAAG,GAAG,gBAAgB,CAC1B,SAAS,CAAC,GAAG,EACb,gBAAgB,EAChB,gBAAgB,EAChB,aAAa,EACb,UAAU,EACV,QAAQ,EACR,KAAK,CACN,CAAA;YACD,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;gBACjB,OAAO,KAAK,CAAA;YACd,CAAC;YACD,OAAO,cAAc,IAAI,GAAG,CAAC,OAAO,EAAE,IAAI,cAAc,IAAI,GAAG,CAAC,OAAO,EAAE,CAAA;QAC3E,CAAC;QACD,OAAO,CAAC,CAAC,CAAC;YACR,MAAM,WAAW,GACf,SAAS,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAA;YAClD,MAAM,WAAW,GAAG,gBAAgB,CAClC,SAAS,EACT,gBAAgB,EAChB,gBAAgB,EAChB,aAAa,EACb,UAAU,EACV,QAAQ,EACR,WAAW,CACZ,CAAA;YACD,IAAI,CAAC,WAAW,EAAE,CAAC;gBACjB,OAAO,KAAK,CAAA;YACd,CAAC;YAED,QAAQ,SAAS,CAAC,QAAQ,EAAE,CAAC;gBAC3B,KAAK,OAAO;oBACV,OAAO,cAAc,GAAG,WAAW,CAAC,OAAO,EAAE,CAAA;gBAC/C,KAAK,QAAQ;oBACX,OAAO,cAAc,GAAG,WAAW,CAAC,OAAO,EAAE,CAAA;gBAC/C,OAAO,CAAC,CAAC,CAAC;oBACR,MAAM,CAAC,GAAU,SAAS,CAAA;oBAC1B,OAAO,CAAC,IAAI,CAAC,8BAA8B,EAAE,CAAC,CAAC,CAAA;oBAC/C,OAAO,KAAK,CAAA;gBACd,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;AACH,CAAC","sourcesContent":["import { ConditionTypes } from '@oneblink/types'\nimport {\n AddOffsetToDate,\n EndOfDay,\n FormElementsCtrl,\n ParseDayOnlyDate,\n StartOfDay,\n} from './types.js'\nimport { getElementAndValue } from './evaluateFormElementConditionalPredicate.js'\n\nconst DATE_ONLY_PATTERN = /^\\d{4}-\\d{2}-\\d{2}$/\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
+ {"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,2 +1,6 @@
1
- export {};
1
+ /**
2
+ * Matches day-only (`YYYY-MM-DD`) date strings. Used to decide when to call
3
+ * `parseDayOnlyDate` instead of `new Date(value)`.
4
+ */
5
+ export const DATE_ONLY_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
2
6
  //# sourceMappingURL=types.js.map
@@ -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"]}
@@ -0,0 +1,9 @@
1
+ import { SubmissionTypes } from '@oneblink/types';
2
+ import { PaymentProvider } from './types.js';
3
+ /**
4
+ * Resolve the pure payment provider implementation for a payment type.
5
+ *
6
+ * @param type
7
+ * @returns
8
+ */
9
+ export default function getPaymentProvider(type: SubmissionTypes.FormSubmissionPayment['type']): PaymentProvider<SubmissionTypes.FormSubmissionPayment>;