@onabl/js 0.1.0

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/index.js ADDED
@@ -0,0 +1,1259 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __commonJS = (cb, mod) => function __require() {
9
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
10
+ };
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
+ var __copyProps = (to, from, except, desc) => {
16
+ if (from && typeof from === "object" || typeof from === "function") {
17
+ for (let key of __getOwnPropNames(from))
18
+ if (!__hasOwnProp.call(to, key) && key !== except)
19
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
20
+ }
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
24
+ // If the importer is in node compatibility mode or this is not an ESM
25
+ // file that has been converted to a CommonJS file using a Babel-
26
+ // compatible transform (i.e. "__esModule" has not been set), then set
27
+ // "default" to the CommonJS "module.exports" for node compatibility.
28
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
29
+ mod
30
+ ));
31
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
+
33
+ // ../engine/dist/presentation.js
34
+ var require_presentation = __commonJS({
35
+ "../engine/dist/presentation.js"(exports2) {
36
+ "use strict";
37
+ Object.defineProperty(exports2, "__esModule", { value: true });
38
+ exports2.shareOfGroup = shareOfGroup;
39
+ exports2.groupLineItems = groupLineItems2;
40
+ exports2.presentQuote = presentQuote2;
41
+ function shareOfGroup(item, tag) {
42
+ return item.groupAmounts?.[tag] ?? (item.tag === tag ? item.amount : 0);
43
+ }
44
+ function labelWithFactor(item) {
45
+ if (item.factor === void 0 || item.factor === 1)
46
+ return item.label;
47
+ const percent = Math.abs(Math.round((item.factor - 1) * 100));
48
+ return item.factor > 1 ? `${item.label} (+${percent}%)` : `${item.label} (${percent}% off)`;
49
+ }
50
+ function groupLineItems2(lineItems, displayGroups) {
51
+ const visible = lineItems.filter((item) => item.showOnQuote);
52
+ const groupTotals = {};
53
+ if (!displayGroups || displayGroups.length === 0) {
54
+ const main2 = visible.filter((item) => item.amount !== 0).map((item) => ({ key: item.ruleId, label: labelWithFactor(item), amount: item.amount, isGroup: false, sourceItem: item }));
55
+ return { main: main2, separateSections: [], groupTotals };
56
+ }
57
+ const main = [];
58
+ const separateSections = [];
59
+ const accounted = /* @__PURE__ */ new Map();
60
+ const account = (item, amount) => accounted.set(item.ruleId, (accounted.get(item.ruleId) ?? 0) + amount);
61
+ for (const group of displayGroups) {
62
+ const matching = visible.filter((item) => item.groupAmounts && group.tag in item.groupAmounts || item.tag === group.tag);
63
+ const total = matching.reduce((sum, item) => sum + shareOfGroup(item, group.tag), 0);
64
+ groupTotals[group.tag] = total;
65
+ if (group.excludeFromTotal)
66
+ matching.forEach((item) => account(item, shareOfGroup(item, group.tag)));
67
+ if (!group.showOnQuote || matching.length === 0 || total === 0)
68
+ continue;
69
+ if (!group.excludeFromTotal)
70
+ matching.forEach((item) => account(item, shareOfGroup(item, group.tag)));
71
+ const active = matching.filter((item) => shareOfGroup(item, group.tag) !== 0);
72
+ const rowFor = (item) => ({
73
+ key: `${group.tag}:${item.ruleId}`,
74
+ label: item.label,
75
+ amount: shareOfGroup(item, group.tag),
76
+ isGroup: false,
77
+ sourceItem: item,
78
+ groupTag: group.tag
79
+ });
80
+ let rows;
81
+ if (group.displayMode === "individual") {
82
+ rows = active.map(rowFor);
83
+ } else if (group.displayMode === "both") {
84
+ rows = [{ key: group.tag, label: group.label, amount: total, isGroup: true, displayMode: "both", children: active.map(rowFor), groupTag: group.tag }];
85
+ } else {
86
+ const label = active.length === 1 ? active[0]?.label ?? group.label : group.label;
87
+ rows = [{ key: group.tag, label, amount: total, isGroup: true, displayMode: "grouped", groupTag: group.tag }];
88
+ }
89
+ if (group.excludeFromTotal)
90
+ separateSections.push({ group, rows, amount: total });
91
+ else
92
+ main.push(...rows);
93
+ }
94
+ for (const item of visible) {
95
+ const remaining = Math.round((item.amount - (accounted.get(item.ruleId) ?? 0)) * 100) / 100;
96
+ if (remaining === 0)
97
+ continue;
98
+ main.push({ key: item.ruleId, label: labelWithFactor(item), amount: remaining, isGroup: false, sourceItem: item });
99
+ }
100
+ return { main, separateSections, groupTotals };
101
+ }
102
+ function presentQuote2(input) {
103
+ const { main, separateSections } = groupLineItems2(input.lineItems, input.displayGroups);
104
+ const subtotal = input.finalTotal;
105
+ const discountAmount = Math.round((input.finalTotal - input.totalAfterDiscount) * 100) / 100;
106
+ return {
107
+ rows: main,
108
+ separateSections,
109
+ subtotal,
110
+ discount: discountAmount > 0 ? { label: input.discountLabel ?? "Discount", amount: discountAmount } : null,
111
+ total: input.totalAfterDiscount
112
+ };
113
+ }
114
+ }
115
+ });
116
+
117
+ // ../types/dist/config.js
118
+ var require_config = __commonJS({
119
+ "../types/dist/config.js"(exports2) {
120
+ "use strict";
121
+ Object.defineProperty(exports2, "__esModule", { value: true });
122
+ exports2.isAndCondition = isAndCondition;
123
+ function isAndCondition(c) {
124
+ return c.operator === "and";
125
+ }
126
+ }
127
+ });
128
+
129
+ // ../engine/dist/expression.js
130
+ var require_expression = __commonJS({
131
+ "../engine/dist/expression.js"(exports2) {
132
+ "use strict";
133
+ Object.defineProperty(exports2, "__esModule", { value: true });
134
+ exports2.parseExpression = parseExpression;
135
+ exports2.evaluateNode = evaluateNode;
136
+ exports2.evaluateExpression = evaluateExpression;
137
+ exports2.printExpression = printExpression;
138
+ exports2.collectFieldKeys = collectFieldKeys;
139
+ var IDENT = /[\w]/;
140
+ function tokenise(source) {
141
+ const lexemes = [];
142
+ const src = source.trim();
143
+ let i = 0;
144
+ const readIdent = (from) => {
145
+ let out = "";
146
+ let j = from;
147
+ while (j < src.length && IDENT.test(src[j]))
148
+ out += src[j++];
149
+ return out;
150
+ };
151
+ while (i < src.length) {
152
+ const ch = src[i];
153
+ if (ch === " " || ch === " " || ch === "\n") {
154
+ i++;
155
+ continue;
156
+ }
157
+ if (ch === "$") {
158
+ const key = readIdent(i + 1);
159
+ lexemes.push({ type: "TOKEN", value: key, position: i });
160
+ i += key.length + 1;
161
+ continue;
162
+ }
163
+ if (src.startsWith("field:", i)) {
164
+ const key = readIdent(i + 6);
165
+ lexemes.push({ type: "FIELD", value: key, position: i });
166
+ i += key.length + 6;
167
+ continue;
168
+ }
169
+ if (src.startsWith("lookup:", i)) {
170
+ const ref = readIdent(i + 7);
171
+ lexemes.push({ type: "LOOKUP", value: ref, position: i });
172
+ i += ref.length + 7;
173
+ continue;
174
+ }
175
+ if (/[0-9]/.test(ch) || ch === "." && /[0-9]/.test(src[i + 1] ?? "")) {
176
+ let num = "";
177
+ while (i < src.length && /[0-9.]/.test(src[i]))
178
+ num += src[i++];
179
+ lexemes.push({ type: "NUMBER", value: num, position: i - num.length });
180
+ continue;
181
+ }
182
+ if (ch === "+" || ch === "-" || ch === "*" || ch === "/") {
183
+ lexemes.push({ type: "OP", value: ch, position: i });
184
+ i++;
185
+ continue;
186
+ }
187
+ if (ch === "(") {
188
+ lexemes.push({ type: "LPAREN", value: ch, position: i });
189
+ i++;
190
+ continue;
191
+ }
192
+ if (ch === ")") {
193
+ lexemes.push({ type: "RPAREN", value: ch, position: i });
194
+ i++;
195
+ continue;
196
+ }
197
+ lexemes.push({ type: "UNKNOWN", value: ch, position: i });
198
+ i++;
199
+ }
200
+ lexemes.push({ type: "EOF", value: "", position: src.length });
201
+ return lexemes;
202
+ }
203
+ function parseExpression(source) {
204
+ const errors = [];
205
+ if (!source || source.trim().length === 0) {
206
+ return { node: null, errors: [{ ruleId: null, message: "Empty expression" }] };
207
+ }
208
+ const lexemes = tokenise(source);
209
+ let pos = 0;
210
+ const peek = () => lexemes[pos] ?? { type: "EOF", value: "", position: 0 };
211
+ const next = () => lexemes[pos++] ?? { type: "EOF", value: "", position: 0 };
212
+ const fail = (message) => {
213
+ errors.push({ ruleId: null, message });
214
+ };
215
+ function parsePrimary() {
216
+ const lexeme = next();
217
+ switch (lexeme.type) {
218
+ case "NUMBER": {
219
+ const value = parseFloat(lexeme.value);
220
+ if (Number.isNaN(value)) {
221
+ fail(`Invalid number "${lexeme.value}"`);
222
+ return null;
223
+ }
224
+ return { kind: "number", value };
225
+ }
226
+ case "TOKEN":
227
+ if (lexeme.value === "") {
228
+ fail("Malformed token reference \u2014 nothing after '$'");
229
+ return null;
230
+ }
231
+ return { kind: "token", key: lexeme.value };
232
+ case "FIELD":
233
+ if (lexeme.value === "") {
234
+ fail("Malformed field reference \u2014 nothing after 'field:'");
235
+ return null;
236
+ }
237
+ return { kind: "field", key: lexeme.value };
238
+ case "LOOKUP":
239
+ if (lexeme.value === "") {
240
+ fail("Malformed lookup reference \u2014 nothing after 'lookup:'");
241
+ return null;
242
+ }
243
+ return { kind: "lookup", ref: lexeme.value };
244
+ case "LPAREN": {
245
+ const inner = parseExpr();
246
+ if (peek().type === "RPAREN") {
247
+ next();
248
+ } else {
249
+ fail("Unclosed '(' \u2014 every opening bracket needs a closing one");
250
+ }
251
+ return inner;
252
+ }
253
+ case "EOF":
254
+ fail("Expression ended unexpectedly");
255
+ return null;
256
+ default:
257
+ fail(`Unexpected "${lexeme.value}" in expression`);
258
+ return null;
259
+ }
260
+ }
261
+ function parseFactor() {
262
+ if (peek().type === "OP" && peek().value === "-") {
263
+ next();
264
+ const operand = parseFactor();
265
+ return operand === null ? null : { kind: "negate", operand };
266
+ }
267
+ return parsePrimary();
268
+ }
269
+ function parseTerm() {
270
+ let left = parseFactor();
271
+ while (peek().type === "OP" && (peek().value === "*" || peek().value === "/")) {
272
+ const op = next().value;
273
+ const right = parseFactor();
274
+ if (left === null || right === null)
275
+ return null;
276
+ left = { kind: "binary", op, left, right };
277
+ }
278
+ return left;
279
+ }
280
+ function parseExpr() {
281
+ let left = parseTerm();
282
+ while (peek().type === "OP" && (peek().value === "+" || peek().value === "-")) {
283
+ const op = next().value;
284
+ const right = parseTerm();
285
+ if (left === null || right === null)
286
+ return null;
287
+ left = { kind: "binary", op, left, right };
288
+ }
289
+ return left;
290
+ }
291
+ const node = parseExpr();
292
+ if (peek().type !== "EOF") {
293
+ fail(`Unexpected "${peek().value}" after the end of the expression`);
294
+ }
295
+ return { node: errors.length > 0 ? null : node, errors };
296
+ }
297
+ function evaluateNode(node, scope) {
298
+ const errors = [];
299
+ function walk(n) {
300
+ switch (n.kind) {
301
+ case "number":
302
+ return n.value;
303
+ case "token": {
304
+ if (!(n.key in scope.tokens)) {
305
+ errors.push({ ruleId: null, message: `Undefined token: "$${n.key}"` });
306
+ return 0;
307
+ }
308
+ return scope.tokens[n.key];
309
+ }
310
+ case "field": {
311
+ const fields = scope.fields ?? {};
312
+ if (!(n.key in fields)) {
313
+ errors.push({ ruleId: null, message: `Unanswered field referenced in a price: "${n.key}"` });
314
+ return 0;
315
+ }
316
+ const value2 = fields[n.key];
317
+ if (!Number.isFinite(value2)) {
318
+ errors.push({ ruleId: null, message: `Field "${n.key}" is not a number` });
319
+ return 0;
320
+ }
321
+ return value2;
322
+ }
323
+ case "lookup": {
324
+ const lookups = scope.lookups ?? {};
325
+ if (!(n.ref in lookups)) {
326
+ errors.push({ ruleId: null, message: `Unresolved lookup reference: "lookup:${n.ref}"` });
327
+ return 0;
328
+ }
329
+ return lookups[n.ref];
330
+ }
331
+ case "negate":
332
+ return -walk(n.operand);
333
+ case "binary": {
334
+ const left = walk(n.left);
335
+ const right = walk(n.right);
336
+ switch (n.op) {
337
+ case "+":
338
+ return left + right;
339
+ case "-":
340
+ return left - right;
341
+ case "*":
342
+ return left * right;
343
+ case "/":
344
+ if (right === 0) {
345
+ errors.push({ ruleId: null, message: "Division by zero in expression" });
346
+ return 0;
347
+ }
348
+ return left / right;
349
+ }
350
+ }
351
+ }
352
+ }
353
+ const value = walk(node);
354
+ return { value: Number.isFinite(value) ? value : 0, errors };
355
+ }
356
+ function evaluateExpression(source, scope) {
357
+ const { node, errors } = parseExpression(source);
358
+ if (node === null)
359
+ return { value: 0, errors };
360
+ return evaluateNode(node, scope);
361
+ }
362
+ var PRECEDENCE = { "+": 1, "-": 1, "*": 2, "/": 2 };
363
+ function printExpression(node) {
364
+ const ASSOCIATIVE = { "+": true, "*": true, "-": false, "/": false };
365
+ function render(n, parentPrecedence = 0, isRightOperand = false, parentOp = null) {
366
+ switch (n.kind) {
367
+ case "number":
368
+ return String(n.value);
369
+ case "token":
370
+ return `$${n.key}`;
371
+ case "field":
372
+ return `field:${n.key}`;
373
+ case "lookup":
374
+ return `lookup:${n.ref}`;
375
+ case "negate":
376
+ return `-${render(n.operand, 3)}`;
377
+ case "binary": {
378
+ const precedence = PRECEDENCE[n.op];
379
+ const body = `${render(n.left, precedence, false, n.op)} ${n.op} ${render(n.right, precedence, true, n.op)}`;
380
+ const parentKeepsOrder = parentOp !== null && !ASSOCIATIVE[parentOp];
381
+ const needsBrackets = precedence < parentPrecedence || precedence === parentPrecedence && isRightOperand && parentKeepsOrder;
382
+ return needsBrackets ? `(${body})` : body;
383
+ }
384
+ }
385
+ }
386
+ return render(node);
387
+ }
388
+ function collectFieldKeys(node) {
389
+ const keys = /* @__PURE__ */ new Set();
390
+ (function walk(n) {
391
+ if (n.kind === "field")
392
+ keys.add(n.key);
393
+ else if (n.kind === "binary") {
394
+ walk(n.left);
395
+ walk(n.right);
396
+ } else if (n.kind === "negate")
397
+ walk(n.operand);
398
+ })(node);
399
+ return [...keys];
400
+ }
401
+ }
402
+ });
403
+
404
+ // ../engine/dist/resolver.js
405
+ var require_resolver = __commonJS({
406
+ "../engine/dist/resolver.js"(exports2) {
407
+ "use strict";
408
+ Object.defineProperty(exports2, "__esModule", { value: true });
409
+ exports2.resolveExpression = resolveExpression;
410
+ exports2.evaluateCondition = evaluateCondition2;
411
+ var config_1 = require_config();
412
+ var expression_1 = require_expression();
413
+ function resolveExpression(expr, variables, lookupValues, fields) {
414
+ return (0, expression_1.evaluateExpression)(expr, { tokens: variables, lookups: lookupValues, fields });
415
+ }
416
+ function evaluateCondition2(condition, answers) {
417
+ if ((0, config_1.isAndCondition)(condition)) {
418
+ return condition.clauses.every((c) => evaluateCondition2(c, answers));
419
+ }
420
+ const { field, operator, value: condValue } = condition;
421
+ const answer = answers[field];
422
+ if (answer === void 0 || answer === null) {
423
+ return false;
424
+ }
425
+ if (operator === "includes") {
426
+ if (Array.isArray(answer))
427
+ return answer.includes(String(condValue));
428
+ return String(answer) === String(condValue);
429
+ }
430
+ if (operator === "includes_any") {
431
+ if (!Array.isArray(answer) || !Array.isArray(condValue)) {
432
+ return false;
433
+ }
434
+ return condValue.some((v) => answer.includes(v));
435
+ }
436
+ if (operator === "in") {
437
+ if (Array.isArray(answer) || !Array.isArray(condValue)) {
438
+ return false;
439
+ }
440
+ return condValue.includes(String(answer));
441
+ }
442
+ if (Array.isArray(answer)) {
443
+ return false;
444
+ }
445
+ const op = operator;
446
+ switch (op) {
447
+ case "eq":
448
+ return answer === condValue;
449
+ case "neq":
450
+ return answer !== condValue;
451
+ case "gt":
452
+ case "gte":
453
+ case "lt":
454
+ case "lte": {
455
+ const numAnswer = Number(answer);
456
+ const numValue = Number(condValue);
457
+ if (isNaN(numAnswer) || isNaN(numValue)) {
458
+ return false;
459
+ }
460
+ if (op === "gt")
461
+ return numAnswer > numValue;
462
+ if (op === "gte")
463
+ return numAnswer >= numValue;
464
+ if (op === "lt")
465
+ return numAnswer < numValue;
466
+ if (op === "lte")
467
+ return numAnswer <= numValue;
468
+ return false;
469
+ }
470
+ default: {
471
+ const _exhaustive = op;
472
+ void _exhaustive;
473
+ return false;
474
+ }
475
+ }
476
+ }
477
+ }
478
+ });
479
+
480
+ // ../engine/dist/date-derival.js
481
+ var require_date_derival = __commonJS({
482
+ "../engine/dist/date-derival.js"(exports2) {
483
+ "use strict";
484
+ Object.defineProperty(exports2, "__esModule", { value: true });
485
+ exports2.injectDateDerivals = injectDateDerivals;
486
+ function injectDateDerivals(config, raw, now = /* @__PURE__ */ new Date()) {
487
+ const augmented = { ...raw };
488
+ for (const field of config.fields) {
489
+ if (field.type !== "date" || !field.derivedFields)
490
+ continue;
491
+ const value = raw[field.key];
492
+ if (!value || typeof value !== "string")
493
+ continue;
494
+ const [year, month, day] = value.split("-").map(Number);
495
+ if (!year || !month || !day)
496
+ continue;
497
+ const date = new Date(year, month - 1, day);
498
+ const { isWeekend, isPeakSeason, daysUntil } = field.derivedFields;
499
+ if (isWeekend) {
500
+ const d = date.getDay();
501
+ augmented[`__is_weekend__${field.key}`] = d === 0 || d === 6;
502
+ }
503
+ if (isPeakSeason?.length) {
504
+ augmented[`__is_peak_season__${field.key}`] = isPeakSeason.includes(date.getMonth() + 1);
505
+ }
506
+ if (daysUntil) {
507
+ const today = new Date(now);
508
+ today.setHours(0, 0, 0, 0);
509
+ const diff = Math.round((date.getTime() - today.getTime()) / 864e5);
510
+ augmented[`__days_until__${field.key}`] = diff;
511
+ }
512
+ }
513
+ return augmented;
514
+ }
515
+ }
516
+ });
517
+
518
+ // ../engine/dist/variables.js
519
+ var require_variables = __commonJS({
520
+ "../engine/dist/variables.js"(exports2) {
521
+ "use strict";
522
+ Object.defineProperty(exports2, "__esModule", { value: true });
523
+ exports2.resolveTokens = resolveTokens;
524
+ var resolver_1 = require_resolver();
525
+ function extractTokenRefs(expr) {
526
+ const refs = /* @__PURE__ */ new Set();
527
+ for (const match of expr.matchAll(/\$(\w+)/g)) {
528
+ refs.add(match[1]);
529
+ }
530
+ return Array.from(refs);
531
+ }
532
+ function resolveTokens(rawTokens) {
533
+ const errors = [];
534
+ const resolved = {};
535
+ const resolving = /* @__PURE__ */ new Set();
536
+ const done = /* @__PURE__ */ new Set();
537
+ function resolveKey(key) {
538
+ if (done.has(key))
539
+ return resolved[key] ?? 0;
540
+ const raw = rawTokens[key];
541
+ if (raw === void 0)
542
+ return 0;
543
+ if (typeof raw === "number") {
544
+ resolved[key] = raw;
545
+ done.add(key);
546
+ return raw;
547
+ }
548
+ if (resolving.has(key)) {
549
+ errors.push({ ruleId: null, message: `Circular reference detected in token "$${key}"` });
550
+ resolved[key] = 0;
551
+ done.add(key);
552
+ return 0;
553
+ }
554
+ resolving.add(key);
555
+ for (const dep of extractTokenRefs(raw)) {
556
+ resolveKey(dep);
557
+ }
558
+ resolving.delete(key);
559
+ const { value, errors: exprErrors } = (0, resolver_1.resolveExpression)(raw, resolved, {});
560
+ errors.push(...exprErrors.map((e) => ({ ...e, message: `Token "$${key}": ${e.message}` })));
561
+ resolved[key] = value;
562
+ done.add(key);
563
+ return value;
564
+ }
565
+ for (const key of Object.keys(rawTokens)) {
566
+ resolveKey(key);
567
+ }
568
+ return { variables: resolved, errors };
569
+ }
570
+ }
571
+ });
572
+
573
+ // ../engine/dist/engine.js
574
+ var require_engine = __commonJS({
575
+ "../engine/dist/engine.js"(exports2) {
576
+ "use strict";
577
+ Object.defineProperty(exports2, "__esModule", { value: true });
578
+ exports2.resolveTokens = exports2.injectDateDerivals = void 0;
579
+ exports2.applyDefaults = applyDefaults2;
580
+ exports2.calculate = calculate;
581
+ var resolver_1 = require_resolver();
582
+ var date_derival_1 = require_date_derival();
583
+ Object.defineProperty(exports2, "injectDateDerivals", { enumerable: true, get: function() {
584
+ return date_derival_1.injectDateDerivals;
585
+ } });
586
+ var variables_1 = require_variables();
587
+ Object.defineProperty(exports2, "resolveTokens", { enumerable: true, get: function() {
588
+ return variables_1.resolveTokens;
589
+ } });
590
+ var presentation_1 = require_presentation();
591
+ function computeGroupAmounts(groupAssignments, baseAmount, variables, lookupValues, ruleId) {
592
+ if (!groupAssignments || groupAssignments.length === 0) {
593
+ return { errors: [] };
594
+ }
595
+ const groupAmounts = {};
596
+ const errors = [];
597
+ for (const assignment of groupAssignments) {
598
+ if (assignment.expression) {
599
+ const { value, errors: exprErrors } = (0, resolver_1.resolveExpression)(assignment.expression, variables, lookupValues);
600
+ errors.push(...exprErrors.map((e) => ({ ...e, ruleId })));
601
+ groupAmounts[assignment.tag] = roundCents(value);
602
+ } else {
603
+ groupAmounts[assignment.tag] = baseAmount;
604
+ }
605
+ }
606
+ return { groupAmounts: Object.keys(groupAmounts).length > 0 ? groupAmounts : void 0, errors };
607
+ }
608
+ function roundCents(n) {
609
+ return Math.round((n + Number.EPSILON) * 100) / 100;
610
+ }
611
+ function applyDefaults2(config, answers) {
612
+ const result = { ...answers };
613
+ for (const field of config.fields) {
614
+ if (result[field.key] === void 0 || result[field.key] === null) {
615
+ if (field.defaultValue !== null && field.defaultValue !== void 0) {
616
+ result[field.key] = field.defaultValue;
617
+ } else {
618
+ if (field.type === "number")
619
+ result[field.key] = 0;
620
+ if (field.type === "toggle")
621
+ result[field.key] = false;
622
+ }
623
+ }
624
+ }
625
+ return result;
626
+ }
627
+ function processUnitRate(rule, answers, variables, lookupValues, fieldValues, quantityUnit) {
628
+ const errors = [];
629
+ const quantityRaw = answers[rule.quantityField];
630
+ const quantity = Number(quantityRaw);
631
+ if (quantityRaw === void 0 || quantityRaw === null) {
632
+ errors.push({
633
+ ruleId: rule.id,
634
+ message: `unit_rate rule "${rule.id}": field "${rule.quantityField}" not found in answers`
635
+ });
636
+ return { lineItem: null, amount: 0, errors };
637
+ }
638
+ if (isNaN(quantity)) {
639
+ errors.push({
640
+ ruleId: rule.id,
641
+ message: `unit_rate rule "${rule.id}": field "${rule.quantityField}" is not a number (got "${quantityRaw}")`
642
+ });
643
+ return { lineItem: null, amount: 0, errors };
644
+ }
645
+ const { value: rate, errors: rateErrors } = (0, resolver_1.resolveExpression)(rule.rateExpr, variables, lookupValues, fieldValues);
646
+ const stampedErrors = rateErrors.map((e) => ({ ...e, ruleId: rule.id }));
647
+ errors.push(...stampedErrors);
648
+ const amount = roundCents(quantity * rate);
649
+ const { groupAmounts, errors: groupErrors } = computeGroupAmounts(rule.groupAssignments, amount, variables, lookupValues, rule.id);
650
+ errors.push(...groupErrors);
651
+ const lineItem = {
652
+ ruleId: rule.id,
653
+ label: rule.label,
654
+ amount,
655
+ type: "unit_rate",
656
+ showOnQuote: rule.showOnQuote,
657
+ ...rule.tag !== void 0 && { tag: rule.tag },
658
+ ...groupAmounts !== void 0 && { groupAmounts },
659
+ quantity,
660
+ ...quantityUnit !== void 0 && { quantityUnit },
661
+ ratePerUnit: rate
662
+ };
663
+ return { lineItem, amount, errors };
664
+ }
665
+ function processConditionalAddon(rule, variables, lookupValues, fieldValues) {
666
+ const { value: rawAmount, errors: resolveErrors } = (0, resolver_1.resolveExpression)(rule.amountExpr, variables, lookupValues, fieldValues);
667
+ const amount = roundCents(rawAmount);
668
+ const errors = resolveErrors.map((e) => ({ ...e, ruleId: rule.id }));
669
+ const { groupAmounts, errors: groupErrors } = computeGroupAmounts(rule.groupAssignments, amount, variables, lookupValues, rule.id);
670
+ errors.push(...groupErrors);
671
+ const lineItem = {
672
+ ruleId: rule.id,
673
+ label: rule.label,
674
+ amount,
675
+ type: "conditional_addon",
676
+ showOnQuote: rule.showOnQuote,
677
+ ...rule.tag !== void 0 && { tag: rule.tag },
678
+ ...groupAmounts !== void 0 && { groupAmounts }
679
+ };
680
+ return { lineItem, amount, errors };
681
+ }
682
+ function processLookupTable(rule, answers, variables, lookupValues) {
683
+ const errors = [];
684
+ const rowAnswer = String(answers[rule.rowField] ?? "");
685
+ const colAnswer = String(answers[rule.colField] ?? "");
686
+ if (answers[rule.rowField] === void 0) {
687
+ errors.push({
688
+ ruleId: rule.id,
689
+ message: `lookup_table rule "${rule.id}": row field "${rule.rowField}" not found in answers`
690
+ });
691
+ }
692
+ if (answers[rule.colField] === void 0) {
693
+ errors.push({
694
+ ruleId: rule.id,
695
+ message: `lookup_table rule "${rule.id}": col field "${rule.colField}" not found in answers`
696
+ });
697
+ }
698
+ if (errors.length > 0) {
699
+ return { lineItem: null, amount: 0, errors };
700
+ }
701
+ const row = rule.table[rowAnswer];
702
+ if (row === void 0) {
703
+ errors.push({
704
+ ruleId: rule.id,
705
+ message: `lookup_table rule "${rule.id}": no row for value "${rowAnswer}"`
706
+ });
707
+ return { lineItem: null, amount: 0, errors };
708
+ }
709
+ const rawAmount = row[colAnswer];
710
+ if (rawAmount === void 0) {
711
+ errors.push({
712
+ ruleId: rule.id,
713
+ message: `lookup_table rule "${rule.id}": no column "${colAnswer}" in row "${rowAnswer}"`
714
+ });
715
+ return { lineItem: null, amount: 0, errors };
716
+ }
717
+ const amount = roundCents(rawAmount);
718
+ const { groupAmounts, errors: groupErrors } = computeGroupAmounts(rule.groupAssignments, amount, variables, lookupValues, rule.id);
719
+ errors.push(...groupErrors);
720
+ const lineItem = {
721
+ ruleId: rule.id,
722
+ label: rule.label,
723
+ amount,
724
+ type: "lookup_table",
725
+ showOnQuote: rule.showOnQuote,
726
+ ...rule.tag !== void 0 && { tag: rule.tag },
727
+ ...groupAmounts !== void 0 && { groupAmounts }
728
+ };
729
+ return { lineItem, amount, errors };
730
+ }
731
+ function processSelectionMultiplier(rule, answers, runningTotal, variables, lookupValues) {
732
+ const errors = [];
733
+ const selection = String(answers[rule.field] ?? "");
734
+ if (answers[rule.field] === void 0) {
735
+ return { lineItem: null, newTotal: runningTotal, errors };
736
+ }
737
+ const factor = rule.multipliers[selection];
738
+ if (factor === void 0) {
739
+ return { lineItem: null, newTotal: runningTotal, errors };
740
+ }
741
+ const appliedTo = runningTotal;
742
+ const newTotal = runningTotal * factor;
743
+ const amount = roundCents(newTotal - appliedTo);
744
+ const { groupAmounts, errors: groupErrors } = computeGroupAmounts(rule.groupAssignments, amount, variables, lookupValues, rule.id);
745
+ errors.push(...groupErrors);
746
+ const lineItem = {
747
+ ruleId: rule.id,
748
+ label: rule.label,
749
+ amount,
750
+ type: "selection_multiplier",
751
+ showOnQuote: rule.showOnQuote,
752
+ ...rule.tag !== void 0 && { tag: rule.tag },
753
+ ...groupAmounts !== void 0 && { groupAmounts },
754
+ factor,
755
+ appliedTo
756
+ };
757
+ return { lineItem, newTotal, errors };
758
+ }
759
+ function processConditionalMultiplier(rule, variables, lookupValues, fieldValues, runningTotal) {
760
+ const { value: factor, errors: resolveErrors } = (0, resolver_1.resolveExpression)(rule.factorExpr, variables, lookupValues, fieldValues);
761
+ const errors = resolveErrors.map((e) => ({ ...e, ruleId: rule.id }));
762
+ const appliedTo = runningTotal;
763
+ const newTotal = runningTotal * factor;
764
+ const amount = roundCents(newTotal - appliedTo);
765
+ const { groupAmounts, errors: groupErrors } = computeGroupAmounts(rule.groupAssignments, amount, variables, lookupValues, rule.id);
766
+ errors.push(...groupErrors);
767
+ const lineItem = {
768
+ ruleId: rule.id,
769
+ label: rule.label,
770
+ amount,
771
+ type: "conditional_multiplier",
772
+ showOnQuote: rule.showOnQuote,
773
+ ...rule.tag !== void 0 && { tag: rule.tag },
774
+ ...groupAmounts !== void 0 && { groupAmounts },
775
+ factor,
776
+ appliedTo
777
+ };
778
+ return { lineItem, newTotal, errors };
779
+ }
780
+ function processFloorMinimum(rule, variables, lookupValues, fieldValues, currentTotal) {
781
+ const { value: minimum, errors: resolveErrors } = (0, resolver_1.resolveExpression)(rule.minimumExpr, variables, lookupValues, fieldValues);
782
+ const errors = resolveErrors.map((e) => ({ ...e, ruleId: rule.id }));
783
+ if (currentTotal < minimum) {
784
+ const amount = roundCents(minimum - currentTotal);
785
+ const { groupAmounts, errors: groupErrors } = computeGroupAmounts(rule.groupAssignments, amount, variables, lookupValues, rule.id);
786
+ errors.push(...groupErrors);
787
+ const lineItem = {
788
+ ruleId: rule.id,
789
+ label: rule.label,
790
+ amount,
791
+ type: "floor_minimum",
792
+ showOnQuote: rule.showOnQuote,
793
+ ...rule.tag !== void 0 && { tag: rule.tag },
794
+ ...groupAmounts !== void 0 && { groupAmounts }
795
+ };
796
+ return { lineItem, finalTotal: minimum, minimumApplied: true, minimumAmount: minimum, errors };
797
+ }
798
+ return { lineItem: null, finalTotal: currentTotal, minimumApplied: false, minimumAmount: minimum, errors };
799
+ }
800
+ function buildTotals(lineItems, finalTotal, displayGroups, minimumApplied) {
801
+ const errors = [];
802
+ const cents = (n) => Math.round(n * 100);
803
+ for (const item of lineItems) {
804
+ if (!item.groupAmounts)
805
+ continue;
806
+ const allocated = Object.values(item.groupAmounts).reduce((a, b) => a + b, 0);
807
+ if (cents(allocated) !== cents(item.amount)) {
808
+ errors.push({
809
+ ruleId: item.ruleId,
810
+ message: `"${item.label}" is split across groups totalling ${allocated.toFixed(2)}, but the item is ${item.amount.toFixed(2)} \u2014 the split must account for the whole amount`
811
+ });
812
+ }
813
+ }
814
+ const { groupTotals: rawGroupTotals } = (0, presentation_1.groupLineItems)(lineItems, displayGroups);
815
+ const groupTotals = {};
816
+ let separatelyQuoted = 0;
817
+ for (const group of displayGroups ?? []) {
818
+ const total = rawGroupTotals[group.tag] ?? 0;
819
+ groupTotals[group.tag] = roundCents(total);
820
+ if (group.excludeFromTotal)
821
+ separatelyQuoted += total;
822
+ }
823
+ if (!minimumApplied) {
824
+ const visible = lineItems.filter((i) => i.showOnQuote).reduce((a, i) => a + i.amount, 0);
825
+ const hidden = lineItems.filter((i) => !i.showOnQuote).reduce((a, i) => a + i.amount, 0);
826
+ if (cents(visible + hidden) !== cents(finalTotal)) {
827
+ errors.push({
828
+ ruleId: null,
829
+ message: `Line items sum to ${(visible + hidden).toFixed(2)} but the quote total is ${finalTotal.toFixed(2)} \u2014 the breakdown does not reconcile`
830
+ });
831
+ }
832
+ }
833
+ return {
834
+ totals: {
835
+ gross: roundCents(finalTotal),
836
+ separatelyQuoted: roundCents(separatelyQuoted),
837
+ payable: roundCents(finalTotal - separatelyQuoted),
838
+ groupTotals
839
+ },
840
+ errors
841
+ };
842
+ }
843
+ function calculate(config, answers) {
844
+ const allErrors = [];
845
+ const lineItems = [];
846
+ const suppressedRuleIds = [];
847
+ const lookupValues = {};
848
+ const { variables, errors: variableErrors } = (0, variables_1.resolveTokens)(config.tokens ?? {});
849
+ allErrors.push(...variableErrors);
850
+ const fieldValues = {};
851
+ for (const [key, answer] of Object.entries(answers)) {
852
+ const numeric = typeof answer === "boolean" ? answer ? 1 : 0 : Number(answer);
853
+ if (typeof answer !== "object" && Number.isFinite(numeric))
854
+ fieldValues[key] = numeric;
855
+ }
856
+ let phase1Total = 0;
857
+ function ruleConditionsMet(rule) {
858
+ if (rule.condition !== void 0 && !(0, resolver_1.evaluateCondition)(rule.condition, answers))
859
+ return false;
860
+ if (rule.scopeCondition !== void 0 && !(0, resolver_1.evaluateCondition)(rule.scopeCondition, answers))
861
+ return false;
862
+ return true;
863
+ }
864
+ for (const rule of config.rules.phase1) {
865
+ if (rule.type === "conditional_addon" && rule.feeType === "percent") {
866
+ continue;
867
+ }
868
+ if (!ruleConditionsMet(rule)) {
869
+ suppressedRuleIds.push(rule.id);
870
+ continue;
871
+ }
872
+ if (rule.type === "unit_rate") {
873
+ const quantityUnit = config.meta.tokenLabels?.[rule.quantityField]?.unit;
874
+ const { lineItem, amount, errors } = processUnitRate(rule, answers, variables, lookupValues, fieldValues, quantityUnit);
875
+ allErrors.push(...errors);
876
+ if (lineItem !== null) {
877
+ lineItems.push(lineItem);
878
+ phase1Total += amount;
879
+ }
880
+ continue;
881
+ }
882
+ if (rule.type === "conditional_addon") {
883
+ const { lineItem, amount, errors } = processConditionalAddon(rule, variables, lookupValues, fieldValues);
884
+ allErrors.push(...errors);
885
+ if (lineItem !== null) {
886
+ lineItems.push(lineItem);
887
+ phase1Total += amount;
888
+ }
889
+ continue;
890
+ }
891
+ if (rule.type === "lookup_table") {
892
+ const { lineItem, amount, errors } = processLookupTable(rule, answers, variables, lookupValues);
893
+ allErrors.push(...errors);
894
+ if (lineItem !== null) {
895
+ lookupValues[rule.id] = amount;
896
+ lineItems.push(lineItem);
897
+ phase1Total += amount;
898
+ }
899
+ continue;
900
+ }
901
+ }
902
+ const phase1SubTotal = phase1Total;
903
+ for (const rule of config.rules.phase1) {
904
+ if (!(rule.type === "conditional_addon" && rule.feeType === "percent")) {
905
+ continue;
906
+ }
907
+ if (!ruleConditionsMet(rule)) {
908
+ suppressedRuleIds.push(rule.id);
909
+ continue;
910
+ }
911
+ const { value: percent, errors: resolveErrors } = (0, resolver_1.resolveExpression)(rule.amountExpr, variables, lookupValues, fieldValues);
912
+ const errors = resolveErrors.map((e) => ({ ...e, ruleId: rule.id }));
913
+ allErrors.push(...errors);
914
+ const amount = roundCents(phase1SubTotal * (percent / 100));
915
+ const { groupAmounts, errors: groupErrors } = computeGroupAmounts(rule.groupAssignments, amount, variables, lookupValues, rule.id);
916
+ errors.push(...groupErrors);
917
+ allErrors.push(...errors);
918
+ const lineItem = {
919
+ ruleId: rule.id,
920
+ label: rule.label,
921
+ amount,
922
+ type: "conditional_addon",
923
+ showOnQuote: rule.showOnQuote,
924
+ ...rule.tag !== void 0 && { tag: rule.tag },
925
+ ...groupAmounts !== void 0 && { groupAmounts }
926
+ };
927
+ lineItems.push(lineItem);
928
+ phase1Total += amount;
929
+ }
930
+ let runningTotal = phase1Total;
931
+ for (const rule of config.rules.phase2) {
932
+ if (!ruleConditionsMet(rule)) {
933
+ suppressedRuleIds.push(rule.id);
934
+ continue;
935
+ }
936
+ if (rule.type === "selection_multiplier") {
937
+ const { lineItem, newTotal, errors } = processSelectionMultiplier(rule, answers, runningTotal, variables, lookupValues);
938
+ allErrors.push(...errors);
939
+ if (lineItem !== null) {
940
+ lineItems.push(lineItem);
941
+ }
942
+ runningTotal = newTotal;
943
+ continue;
944
+ }
945
+ if (rule.type === "conditional_multiplier") {
946
+ const { lineItem, newTotal, errors } = processConditionalMultiplier(rule, variables, lookupValues, fieldValues, runningTotal);
947
+ allErrors.push(...errors);
948
+ if (lineItem !== null) {
949
+ lineItems.push(lineItem);
950
+ }
951
+ runningTotal = newTotal;
952
+ continue;
953
+ }
954
+ }
955
+ const phase2Total = runningTotal;
956
+ let finalTotal = phase2Total;
957
+ let minimumApplied = false;
958
+ let minimumAmount = 0;
959
+ for (const rule of config.rules.phase3) {
960
+ if (!ruleConditionsMet(rule)) {
961
+ suppressedRuleIds.push(rule.id);
962
+ continue;
963
+ }
964
+ if (rule.type === "floor_minimum") {
965
+ const result = processFloorMinimum(rule, variables, lookupValues, fieldValues, finalTotal);
966
+ allErrors.push(...result.errors);
967
+ if (result.lineItem !== null) {
968
+ lineItems.push(result.lineItem);
969
+ }
970
+ finalTotal = result.finalTotal;
971
+ if (result.minimumApplied) {
972
+ minimumApplied = true;
973
+ minimumAmount = result.minimumAmount;
974
+ }
975
+ continue;
976
+ }
977
+ }
978
+ const { totals, errors: totalsErrors } = buildTotals(lineItems, roundCents(finalTotal), config.meta.displayGroups, minimumApplied);
979
+ allErrors.push(...totalsErrors);
980
+ return {
981
+ phase1Total: roundCents(phase1Total),
982
+ phase2Total: roundCents(phase2Total),
983
+ finalTotal: roundCents(finalTotal),
984
+ minimumApplied,
985
+ minimumAmount: roundCents(minimumAmount),
986
+ lineItems,
987
+ suppressedRuleIds,
988
+ totals,
989
+ errors: allErrors
990
+ };
991
+ }
992
+ }
993
+ });
994
+
995
+ // src/index.ts
996
+ var index_exports = {};
997
+ __export(index_exports, {
998
+ OnablError: () => OnablError,
999
+ OnablNetwork: () => OnablNetwork,
1000
+ OnablNotFound: () => OnablNotFound,
1001
+ OnablUnauthorized: () => OnablUnauthorized,
1002
+ OnablValidation: () => OnablValidation,
1003
+ createOnablClient: () => createOnablClient,
1004
+ getAllFields: () => getAllFields,
1005
+ getInitialAnswers: () => getInitialAnswers,
1006
+ getVisibleSteps: () => getVisibleSteps,
1007
+ groupLineItems: () => import_presentation.groupLineItems,
1008
+ presentQuote: () => import_presentation.presentQuote,
1009
+ validateFields: () => validateFields
1010
+ });
1011
+ module.exports = __toCommonJS(index_exports);
1012
+
1013
+ // src/client.ts
1014
+ var import_public_api = require("@onabl/contracts/public-api");
1015
+
1016
+ // src/errors.ts
1017
+ var OnablError = class extends Error {
1018
+ constructor(message, status) {
1019
+ super(message);
1020
+ this.status = status;
1021
+ this.name = "OnablError";
1022
+ }
1023
+ status;
1024
+ };
1025
+ var OnablNotFound = class extends OnablError {
1026
+ constructor(message = "Not found") {
1027
+ super(message, 404);
1028
+ this.name = "OnablNotFound";
1029
+ }
1030
+ };
1031
+ var OnablUnauthorized = class extends OnablError {
1032
+ constructor(message = "Unauthorized") {
1033
+ super(message, 401);
1034
+ this.name = "OnablUnauthorized";
1035
+ }
1036
+ };
1037
+ var OnablValidation = class extends OnablError {
1038
+ constructor(message, fieldErrors) {
1039
+ super(message, 400);
1040
+ this.fieldErrors = fieldErrors;
1041
+ this.name = "OnablValidation";
1042
+ }
1043
+ fieldErrors;
1044
+ };
1045
+ var OnablNetwork = class extends OnablError {
1046
+ constructor(message = "Network error", cause) {
1047
+ super(message);
1048
+ this.name = "OnablNetwork";
1049
+ if (cause !== void 0) this.cause = cause;
1050
+ }
1051
+ };
1052
+
1053
+ // src/client.ts
1054
+ var BUILT_AGAINST_API_VERSION = "2026-09-01";
1055
+ var DEFAULT_BASE_URL = "https://api.onabl.com";
1056
+ var DEFAULT_QUOTE_PATH = "/quote/:uuid";
1057
+ function createOnablClient(config) {
1058
+ const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
1059
+ const apiVersion = config.apiVersion ?? BUILT_AGAINST_API_VERSION;
1060
+ const quotePath = config.quotePath ?? DEFAULT_QUOTE_PATH;
1061
+ async function request(path, init) {
1062
+ const headers = new Headers(init.headers);
1063
+ headers.set("Onabl-Version", apiVersion);
1064
+ if (init.auth === "secret") {
1065
+ headers.set("x-api-key", config.secretKey);
1066
+ } else {
1067
+ headers.set("Authorization", `Bearer ${init.auth.token}`);
1068
+ }
1069
+ if (init.body) headers.set("Content-Type", "application/json");
1070
+ let response;
1071
+ try {
1072
+ response = await fetch(`${baseUrl}${path}`, { ...init, headers });
1073
+ } catch (cause) {
1074
+ throw new OnablNetwork("Request to onabl failed", cause);
1075
+ }
1076
+ if (response.ok) {
1077
+ if (response.status === 204) return void 0;
1078
+ try {
1079
+ return await response.json();
1080
+ } catch (cause) {
1081
+ throw new OnablNetwork("Received an unparseable response from onabl", cause);
1082
+ }
1083
+ }
1084
+ const body = await response.json().catch(() => void 0);
1085
+ if (response.status === 404) throw new OnablNotFound(body?.message ?? "Not found");
1086
+ if (response.status === 401) throw new OnablUnauthorized(body?.message ?? "Unauthorized");
1087
+ if (response.status === 400) throw new OnablValidation(body?.message ?? "Validation failed", body?.fieldErrors);
1088
+ throw new OnablError(body?.message ?? `Request failed with status ${response.status}`, response.status);
1089
+ }
1090
+ function formPath(opts) {
1091
+ return opts?.slug ? `/forms/q/${config.handle}/${opts.slug}` : `/forms/q/${config.handle}`;
1092
+ }
1093
+ function submissionPath(opts) {
1094
+ return opts?.slug ? `/submissions/q/${config.handle}/${opts.slug}` : `/submissions/q/${config.handle}`;
1095
+ }
1096
+ return {
1097
+ forms: {
1098
+ async get(opts) {
1099
+ const raw = await request(formPath(opts), { method: "GET", auth: "secret" });
1100
+ return import_public_api.PublicFormSchema.parse(raw);
1101
+ },
1102
+ async validateDiscountCode(code, opts) {
1103
+ const raw = await request(`${formPath(opts)}/validate-code?code=${encodeURIComponent(code)}`, { method: "GET", auth: "secret" });
1104
+ return import_public_api.ValidateCodeResponseSchema.parse(raw);
1105
+ }
1106
+ },
1107
+ quotes: {
1108
+ async get(uuid, opts) {
1109
+ const raw = await request(`/quotes/${uuid}`, { method: "GET", auth: opts });
1110
+ return import_public_api.QuoteSchema.parse(raw);
1111
+ },
1112
+ async accept(uuid, opts) {
1113
+ const raw = await request(`/quotes/${uuid}/accept`, { method: "POST", auth: opts });
1114
+ import_public_api.QuoteAcceptResponseSchema.parse(raw);
1115
+ },
1116
+ async decline(uuid, reason, note, opts) {
1117
+ const raw = await request(`/quotes/${uuid}/decline`, { method: "POST", auth: opts, body: JSON.stringify({ declineReason: reason, declineNote: note }) });
1118
+ import_public_api.QuoteDeclineResponseSchema.parse(raw);
1119
+ }
1120
+ },
1121
+ invoices: {
1122
+ async get(uuid, opts) {
1123
+ const raw = await request(`/invoices/${uuid}`, { method: "GET", auth: opts });
1124
+ return import_public_api.InvoiceSchema.parse(raw);
1125
+ },
1126
+ async savePopUrl(uuid, key, milestone, opts) {
1127
+ const path = milestone === "deposit" ? `/invoices/${uuid}/deposit-pop-url` : `/invoices/${uuid}/pop-url`;
1128
+ const raw = await request(path, { method: "PATCH", auth: opts, body: JSON.stringify({ key }) });
1129
+ import_public_api.PopUrlUpdateResponseSchema.parse(raw);
1130
+ }
1131
+ },
1132
+ storage: {
1133
+ async presignPop(resourceId, filename, contentType, fileSize, opts) {
1134
+ const raw = await request("/storage/presign/public", {
1135
+ method: "POST",
1136
+ auth: opts,
1137
+ body: JSON.stringify({ resourceId, filename, contentType, fileSize, pathPrefix: "pop" })
1138
+ });
1139
+ return import_public_api.PresignResponseSchema.parse(raw);
1140
+ }
1141
+ },
1142
+ submissions: {
1143
+ async create(answers, opts) {
1144
+ const path = opts?.isTenantSource ? `${submissionPath(opts)}?source=tenant` : submissionPath(opts);
1145
+ const raw = await request(path, {
1146
+ method: "POST",
1147
+ auth: "secret",
1148
+ body: JSON.stringify({ answers, consentGiven: opts?.consentGiven, discountCode: opts?.discountCode })
1149
+ });
1150
+ const parsed = import_public_api.SubmissionCreateResponseSchema.parse(raw);
1151
+ return { quoteId: parsed.uuid, quoteUrl: quotePath.replace(":uuid", parsed.uuid), token: parsed.token };
1152
+ }
1153
+ }
1154
+ };
1155
+ }
1156
+
1157
+ // src/presentation.ts
1158
+ var import_presentation = __toESM(require_presentation());
1159
+
1160
+ // src/form-state.ts
1161
+ var import_engine = __toESM(require_engine());
1162
+ var import_resolver = __toESM(require_resolver());
1163
+ var UNGROUPED_STEP_ID = "__ungrouped";
1164
+ function getVisibleSteps(form, answers) {
1165
+ const config = form.config;
1166
+ const steps = [];
1167
+ for (const step of config.steps) {
1168
+ const fields = fieldsForStep(config.fields, step.id, answers);
1169
+ if (fields.length > 0) steps.push({ ...step, fields });
1170
+ }
1171
+ const ungroupedFields = fieldsForStep(config.fields, null, answers);
1172
+ if (ungroupedFields.length > 0) {
1173
+ steps.push({ id: UNGROUPED_STEP_ID, label: "Details", fields: ungroupedFields });
1174
+ }
1175
+ return steps;
1176
+ }
1177
+ function fieldsForStep(fields, stepId, answers) {
1178
+ return fields.filter((f) => f.stepId === stepId && f.type !== "hidden" && conditionMet(f.condition, answers) && conditionMet(f.scopeCondition, answers));
1179
+ }
1180
+ function conditionMet(condition, answers) {
1181
+ if (!condition) return true;
1182
+ return (0, import_resolver.evaluateCondition)(condition, answers);
1183
+ }
1184
+ function getAllFields(form) {
1185
+ return form.config.fields;
1186
+ }
1187
+ function getInitialAnswers(form) {
1188
+ return (0, import_engine.applyDefaults)(form.config, {});
1189
+ }
1190
+ function validateFields(fields, answers) {
1191
+ const errors = {};
1192
+ for (const field of fields) {
1193
+ if (!field.required) continue;
1194
+ const value = answers[field.key];
1195
+ if (field.type === "multicheck") {
1196
+ if (!Array.isArray(value) || value.length === 0) {
1197
+ errors[field.key] = "Please select at least one option.";
1198
+ }
1199
+ continue;
1200
+ }
1201
+ if (field.type === "toggle") {
1202
+ if (value === void 0 || value === null || value === "") {
1203
+ errors[field.key] = "Please make a selection.";
1204
+ }
1205
+ continue;
1206
+ }
1207
+ if (field.type === "number") {
1208
+ if (value === void 0 || value === null || value === "") {
1209
+ errors[field.key] = "Please enter a value.";
1210
+ continue;
1211
+ }
1212
+ const num = Number(value);
1213
+ if (isNaN(num)) {
1214
+ errors[field.key] = "Please enter a valid number.";
1215
+ continue;
1216
+ }
1217
+ if (field.min !== void 0 && num < field.min) {
1218
+ errors[field.key] = `Minimum value is ${field.min}.`;
1219
+ continue;
1220
+ }
1221
+ if (field.max !== void 0 && num > field.max) {
1222
+ errors[field.key] = `Maximum value is ${field.max}.`;
1223
+ continue;
1224
+ }
1225
+ continue;
1226
+ }
1227
+ if (field.type === "date") {
1228
+ if (!value || value === "") {
1229
+ errors[field.key] = "Please enter a date.";
1230
+ continue;
1231
+ }
1232
+ if (isNaN(new Date(value).getTime())) {
1233
+ errors[field.key] = "Please enter a valid date.";
1234
+ }
1235
+ continue;
1236
+ }
1237
+ if (!value || value === "") {
1238
+ const isChoice = field.type === "radio" || field.type === "select";
1239
+ errors[field.key] = isChoice ? "Please make a selection." : "This field is required.";
1240
+ }
1241
+ }
1242
+ return errors;
1243
+ }
1244
+ // Annotate the CommonJS export names for ESM import in node:
1245
+ 0 && (module.exports = {
1246
+ OnablError,
1247
+ OnablNetwork,
1248
+ OnablNotFound,
1249
+ OnablUnauthorized,
1250
+ OnablValidation,
1251
+ createOnablClient,
1252
+ getAllFields,
1253
+ getInitialAnswers,
1254
+ getVisibleSteps,
1255
+ groupLineItems,
1256
+ presentQuote,
1257
+ validateFields
1258
+ });
1259
+ //# sourceMappingURL=index.js.map