@roomwise/calc-engine 0.0.0-stage → 0.2.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.cjs ADDED
@@ -0,0 +1,1614 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+ //#endregion
24
+ let _jsep_plugin_ternary = require("@jsep-plugin/ternary");
25
+ _jsep_plugin_ternary = __toESM(_jsep_plugin_ternary, 1);
26
+ let jsep = require("jsep");
27
+ jsep = __toESM(jsep, 1);
28
+ let zod = require("zod");
29
+ //#region src/money/convert-uah.ts
30
+ const RATE_PATTERN = /^(\d+)(?:\.(\d{1,4}))?$/;
31
+ const RATE_SCALE = 10000n;
32
+ function scaleRate(rate) {
33
+ const match = RATE_PATTERN.exec(String(rate));
34
+ if (!match) throw new RangeError(`Invalid exchange rate: ${rate}`);
35
+ const scaled = BigInt(match[1]) * RATE_SCALE + BigInt((match[2] ?? "").padEnd(4, "0"));
36
+ if (scaled <= 0n) throw new RangeError(`Invalid exchange rate: ${rate}`);
37
+ return scaled;
38
+ }
39
+ function convertCentsToKopecks(cents, rate) {
40
+ if (!Number.isSafeInteger(cents) || cents < 0) throw new RangeError(`Invalid cents amount: ${cents}`);
41
+ const kopecks = (BigInt(cents) * scaleRate(rate) + RATE_SCALE / 2n) / RATE_SCALE;
42
+ if (kopecks > BigInt(Number.MAX_SAFE_INTEGER)) throw new RangeError(`Amount exceeds the safe integer range: ${cents}`);
43
+ return Number(kopecks);
44
+ }
45
+ //#endregion
46
+ //#region src/money/to-cents.ts
47
+ function toCents(dollars) {
48
+ if (!Number.isFinite(dollars) || dollars < 0) throw new RangeError(`Invalid dollar amount: ${dollars}`);
49
+ const cents = Math.round(Number((dollars * 100).toPrecision(15)));
50
+ if (!Number.isSafeInteger(cents)) throw new RangeError(`Amount exceeds the safe integer range: ${dollars}`);
51
+ return cents;
52
+ }
53
+ //#endregion
54
+ //#region src/estimate/line.ts
55
+ const MAX_LINE_CENTS = 1e10;
56
+ function createLine(init) {
57
+ return {
58
+ status: "OK",
59
+ roomClientId: null,
60
+ categoryId: null,
61
+ productId: null,
62
+ optionId: null,
63
+ packageItemId: null,
64
+ ruleId: null,
65
+ quantity: null,
66
+ unit: null,
67
+ unitPriceCents: null,
68
+ amountCents: 0,
69
+ amountUahKopecks: null,
70
+ warnings: [],
71
+ error: null,
72
+ ...init
73
+ };
74
+ }
75
+ function failedLine(init, status, error) {
76
+ return createLine({
77
+ ...init,
78
+ status,
79
+ error
80
+ });
81
+ }
82
+ function pricedLine(init, dollars) {
83
+ if (!Number.isFinite(dollars) || dollars < 0 || dollars * 100 > 1e10) return failedLine(init, "ERROR", {
84
+ code: "AMOUNT_OUT_OF_RANGE",
85
+ params: { maxCents: MAX_LINE_CENTS }
86
+ });
87
+ return createLine({
88
+ ...init,
89
+ amountCents: toCents(dollars)
90
+ });
91
+ }
92
+ function roundQuantity(quantity) {
93
+ return Math.round(quantity * 1e3) / 1e3;
94
+ }
95
+ //#endregion
96
+ //#region src/estimate/line-id.ts
97
+ function lineId(section, ...parts) {
98
+ return [section, ...parts].join(":");
99
+ }
100
+ //#endregion
101
+ //#region src/estimate/option-line.ts
102
+ function unitQuantity(unit, rooms, pieces) {
103
+ switch (unit) {
104
+ case "PIECE": return pieces;
105
+ case "PROJECT": return 1;
106
+ case "ROOM": return rooms.length;
107
+ case "ROOM_SQM": return rooms.reduce((sum, room) => sum + room.lengthM * room.widthM, 0);
108
+ }
109
+ }
110
+ function optionLine(configuration, option, selection) {
111
+ const init = {
112
+ id: lineId(option.kind, "option", option.id),
113
+ section: option.kind,
114
+ optionId: option.id
115
+ };
116
+ if (!option.available) return failedLine(init, "UNAVAILABLE", {
117
+ code: "PRODUCT_UNAVAILABLE",
118
+ params: { optionId: option.id }
119
+ });
120
+ const selected = configuration.rooms.filter((room) => selection.roomClientIds?.includes(room.clientId));
121
+ const allowed = option.roomTypeCodes.length === 0 ? selected : selected.filter((room) => option.roomTypeCodes.includes(room.roomTypeCode));
122
+ const rejected = selected.filter((room) => !allowed.includes(room));
123
+ const quantity = unitQuantity(option.unit, allowed, selection.quantity ?? option.minQuantity ?? 1);
124
+ return pricedLine({
125
+ ...init,
126
+ quantity: roundQuantity(quantity),
127
+ unit: option.unit,
128
+ unitPriceCents: option.priceCents,
129
+ warnings: rejected.map((room) => ({
130
+ code: "OPTION_ROOM_TYPE_NOT_ALLOWED",
131
+ params: {
132
+ roomClientId: room.clientId,
133
+ roomTypeCode: room.roomTypeCode
134
+ }
135
+ }))
136
+ }, option.priceCents / 100 * quantity);
137
+ }
138
+ function optionLines(configuration, data, kind) {
139
+ return configuration.options.flatMap((selection) => {
140
+ const option = data.options.find((item) => item.id === selection.optionId);
141
+ return option?.kind === kind ? [optionLine(configuration, option, selection)] : [];
142
+ });
143
+ }
144
+ //#endregion
145
+ //#region src/estimate/additional.ts
146
+ function additionalLines(configuration, data) {
147
+ return optionLines(configuration, data, "ADDITIONAL");
148
+ }
149
+ //#endregion
150
+ //#region src/estimate/base.ts
151
+ function baseLine(configuration, data) {
152
+ const init = {
153
+ id: lineId("BASE"),
154
+ section: "BASE"
155
+ };
156
+ const area = configuration.totalAreaSqm;
157
+ const rate = data.baseRates.find((candidate) => candidate.finishLevel === configuration.finishLevel && candidate.areaFromSqm <= area && area < candidate.areaToSqm);
158
+ if (rate === void 0) return failedLine(init, "NOT_CALCULATED", {
159
+ code: "BASE_RATE_NOT_FOUND",
160
+ params: {
161
+ totalAreaSqm: area,
162
+ finishLevel: configuration.finishLevel
163
+ }
164
+ });
165
+ return pricedLine({
166
+ ...init,
167
+ quantity: roundQuantity(area),
168
+ unit: "SQM",
169
+ unitPriceCents: rate.rateCentsPerSqm
170
+ }, rate.rateCentsPerSqm / 100 * area);
171
+ }
172
+ //#endregion
173
+ //#region src/estimate/engineering.ts
174
+ function packageLine(configuration, item) {
175
+ const init = {
176
+ id: lineId("ENGINEERING", "package", item.id),
177
+ section: "ENGINEERING",
178
+ packageItemId: item.id
179
+ };
180
+ if (item.includedInBase) return createLine({
181
+ ...init,
182
+ status: "INCLUDED"
183
+ });
184
+ if (item.priceCents === null || item.unit === null) return failedLine(init, "NOT_CALCULATED", {
185
+ code: "PACKAGE_ITEM_PRICE_MISSING",
186
+ params: { packageItemId: item.id }
187
+ });
188
+ const quantity = unitQuantity(item.unit, configuration.rooms, 1);
189
+ return pricedLine({
190
+ ...init,
191
+ quantity: roundQuantity(quantity),
192
+ unit: item.unit,
193
+ unitPriceCents: item.priceCents
194
+ }, item.priceCents / 100 * quantity);
195
+ }
196
+ function engineeringLines(configuration, data) {
197
+ return [...data.packageItems.map((item) => packageLine(configuration, item)), ...optionLines(configuration, data, "ENGINEERING")];
198
+ }
199
+ //#endregion
200
+ //#region src/variables/object.ts
201
+ const COUNTED_AS_ROOMS = [
202
+ "LIVING_ROOM",
203
+ "BEDROOM",
204
+ "KITCHEN_LIVING"
205
+ ];
206
+ function objectVariables(configuration) {
207
+ return {
208
+ totalArea: configuration.totalAreaSqm,
209
+ balconyArea: configuration.balconyAreaSqm,
210
+ balconyTiles: configuration.balconyTiles,
211
+ gas: configuration.gas,
212
+ kitchenType: configuration.kitchenType,
213
+ finishLevel: configuration.finishLevel,
214
+ roomsCount: configuration.rooms.filter((room) => COUNTED_AS_ROOMS.includes(room.roomTypeCode)).length,
215
+ bathroomsCount: configuration.rooms.filter((room) => room.roomTypeCode === "BATHROOM").length
216
+ };
217
+ }
218
+ //#endregion
219
+ //#region src/variables/product.ts
220
+ function productVariablesOf(product, wastePercent) {
221
+ return {
222
+ price: product.priceCents / 100,
223
+ waste: wastePercent / 100,
224
+ unit: product.unit
225
+ };
226
+ }
227
+ function productVariables(product, category) {
228
+ return productVariablesOf(product, product.wastePercentOverride ?? category.wastePercent);
229
+ }
230
+ //#endregion
231
+ //#region src/variables/room.ts
232
+ function roomVariables(room) {
233
+ const perimeter = 2 * (room.lengthM + room.widthM);
234
+ return {
235
+ length: room.lengthM,
236
+ width: room.widthM,
237
+ height: room.heightM,
238
+ floorArea: room.lengthM * room.widthM,
239
+ perimeter,
240
+ wallArea: perimeter * room.heightM,
241
+ heatedFloor: room.heatedFloor,
242
+ roomType: room.roomTypeCode
243
+ };
244
+ }
245
+ //#endregion
246
+ //#region src/formula/language.ts
247
+ const COEFFICIENT_NAMESPACE = "coef";
248
+ const FORMULA_FUNCTION_TABLE = new Map([
249
+ {
250
+ name: "min",
251
+ minArgs: 1,
252
+ maxArgs: Number.POSITIVE_INFINITY,
253
+ apply: (args) => Math.min(...args)
254
+ },
255
+ {
256
+ name: "max",
257
+ minArgs: 1,
258
+ maxArgs: Number.POSITIVE_INFINITY,
259
+ apply: (args) => Math.max(...args)
260
+ },
261
+ {
262
+ name: "round",
263
+ minArgs: 1,
264
+ maxArgs: 1,
265
+ apply: (args) => Math.round(args[0])
266
+ },
267
+ {
268
+ name: "ceil",
269
+ minArgs: 1,
270
+ maxArgs: 1,
271
+ apply: (args) => Math.ceil(args[0])
272
+ },
273
+ {
274
+ name: "floor",
275
+ minArgs: 1,
276
+ maxArgs: 1,
277
+ apply: (args) => Math.floor(args[0])
278
+ }
279
+ ].map((fn) => [fn.name, fn]));
280
+ const FORMULA_FUNCTIONS = [...FORMULA_FUNCTION_TABLE.keys()];
281
+ //#endregion
282
+ //#region src/formula/evaluate.ts
283
+ var EvaluationFailure = class {
284
+ error;
285
+ constructor(error) {
286
+ this.error = error;
287
+ }
288
+ };
289
+ function fail(node, code, params) {
290
+ throw new EvaluationFailure({
291
+ code,
292
+ position: node.start,
293
+ length: node.end - node.start,
294
+ ...params === void 0 ? {} : { params }
295
+ });
296
+ }
297
+ function runtimeFail(node, reason, params) {
298
+ return fail(node, "FORMULA_RUNTIME_ERROR", {
299
+ ...params,
300
+ reason
301
+ });
302
+ }
303
+ function mismatch(node, expected) {
304
+ return runtimeFail(node, "TYPE_MISMATCH", { expected });
305
+ }
306
+ function finite(node, value) {
307
+ return Number.isFinite(value) ? value : runtimeFail(node, "NOT_FINITE");
308
+ }
309
+ function asNumber(node, value) {
310
+ return typeof value === "number" ? value : mismatch(node, "number");
311
+ }
312
+ function asBoolean(node, value) {
313
+ return typeof value === "boolean" ? value : mismatch(node, "boolean");
314
+ }
315
+ function lookupVariable(node, scope) {
316
+ if (!Object.hasOwn(scope.variables, node.name)) return fail(node, "UNKNOWN_VARIABLE", { name: node.name });
317
+ return scope.variables[node.name];
318
+ }
319
+ function evaluateMember(node, scope) {
320
+ if (node.object.type !== "Identifier" || node.object.name !== "coef") return fail(node, "UNKNOWN_VARIABLE");
321
+ const key = node.property.name;
322
+ if (!Object.hasOwn(scope.coefficients, key)) return fail(node, "UNKNOWN_COEFFICIENT", { key });
323
+ return scope.coefficients[key];
324
+ }
325
+ function evaluateCall(node, scope) {
326
+ const spec = node.callee.type === "Identifier" ? FORMULA_FUNCTION_TABLE.get(node.callee.name) : void 0;
327
+ if (spec === void 0) return fail(node.callee, "UNKNOWN_FUNCTION");
328
+ if (node.args.length < spec.minArgs || node.args.length > spec.maxArgs) return mismatch(node, "argument count");
329
+ const args = node.args.map((arg) => asNumber(arg, walk(arg, scope)));
330
+ return finite(node, spec.apply(args));
331
+ }
332
+ function evaluateEquality(node, scope) {
333
+ const { left, right } = node;
334
+ const literal = [left, right].find((side) => side.type === "Literal" && typeof side.value === "string") ?? null;
335
+ if (literal !== null) {
336
+ const other = literal === left ? right : left;
337
+ const variable = other.type === "Identifier" ? lookupVariable(other, scope) : void 0;
338
+ if (typeof variable !== "string") return mismatch(other, "enum variable");
339
+ return node.operator === "==" ? variable === literal.value : variable !== literal.value;
340
+ }
341
+ const leftValue = walk(left, scope);
342
+ const rightValue = walk(right, scope);
343
+ if (typeof leftValue !== typeof rightValue) return mismatch(right, typeof leftValue);
344
+ return node.operator === "==" ? leftValue === rightValue : leftValue !== rightValue;
345
+ }
346
+ function evaluateBinary(node, scope) {
347
+ const { operator, left, right } = node;
348
+ if (operator === "==" || operator === "!=") return evaluateEquality(node, scope);
349
+ if (operator === "&&" || operator === "||") {
350
+ const leftValue = asBoolean(left, walk(left, scope));
351
+ if (operator === "&&" ? !leftValue : leftValue) return leftValue;
352
+ return asBoolean(right, walk(right, scope));
353
+ }
354
+ const a = asNumber(left, walk(left, scope));
355
+ const b = asNumber(right, walk(right, scope));
356
+ switch (operator) {
357
+ case "<": return a < b;
358
+ case ">": return a > b;
359
+ case "<=": return a <= b;
360
+ case ">=": return a >= b;
361
+ case "+": return finite(node, a + b);
362
+ case "-": return finite(node, a - b);
363
+ case "*": return finite(node, a * b);
364
+ case "/": return b === 0 ? runtimeFail(node, "DIVISION_BY_ZERO") : finite(node, a / b);
365
+ }
366
+ }
367
+ function walk(node, scope) {
368
+ switch (node.type) {
369
+ case "Literal": return typeof node.value === "string" ? mismatch(node, "number or boolean") : node.value;
370
+ case "Identifier": return lookupVariable(node, scope);
371
+ case "Member": return evaluateMember(node, scope);
372
+ case "Call": return evaluateCall(node, scope);
373
+ case "Unary": {
374
+ const argument = walk(node.argument, scope);
375
+ return node.operator === "!" ? !asBoolean(node.argument, argument) : finite(node, -asNumber(node.argument, argument));
376
+ }
377
+ case "Binary": return evaluateBinary(node, scope);
378
+ case "Conditional": return asBoolean(node.test, walk(node.test, scope)) ? walk(node.consequent, scope) : walk(node.alternate, scope);
379
+ default: return runtimeFail(node, "TYPE_MISMATCH", { expected: "known node" });
380
+ }
381
+ }
382
+ function evaluate(ast, scope) {
383
+ try {
384
+ return {
385
+ ok: true,
386
+ value: walk(ast, scope)
387
+ };
388
+ } catch (error) {
389
+ if (error instanceof EvaluationFailure) return {
390
+ ok: false,
391
+ error: error.error
392
+ };
393
+ throw error;
394
+ }
395
+ }
396
+ //#endregion
397
+ //#region src/formula/grammar.ts
398
+ const SEMICOLON_CODE = 59;
399
+ const REMOVED_BINARY_OPERATORS = [
400
+ "|",
401
+ "^",
402
+ "&",
403
+ "<<",
404
+ ">>",
405
+ ">>>",
406
+ "**",
407
+ "%",
408
+ "??",
409
+ "===",
410
+ "!==",
411
+ "in",
412
+ "instanceof"
413
+ ];
414
+ const REMOVED_UNARY_OPERATORS = ["~", "+"];
415
+ const REMOVED_LITERALS = ["null"];
416
+ const { Jsep } = jsep.default;
417
+ function trimEnd(source, end) {
418
+ let result = Math.min(end, source.length);
419
+ while (result > 0 && /\s/.test(source.charAt(result - 1))) result -= 1;
420
+ return result;
421
+ }
422
+ function configure() {
423
+ for (const operator of REMOVED_BINARY_OPERATORS) Jsep.removeBinaryOp(operator);
424
+ for (const operator of REMOVED_UNARY_OPERATORS) Jsep.removeUnaryOp(operator);
425
+ for (const literal of REMOVED_LITERALS) Jsep.removeLiteral(literal);
426
+ jsep.default.plugins.register(_jsep_plugin_ternary.default);
427
+ jsep.default.hooks.add("gobble-spaces", function rejectSemicolon() {
428
+ if (this.code === SEMICOLON_CODE) this.throwError("Unexpected \";\"");
429
+ });
430
+ const { gobbleToken, gobbleIdentifier } = Jsep.prototype;
431
+ Jsep.prototype.gobbleToken = function stampToken() {
432
+ this.gobbleSpaces();
433
+ const start = this.index;
434
+ const node = gobbleToken.call(this);
435
+ if (node) {
436
+ node.start ??= start;
437
+ node.end ??= trimEnd(this.expr, this.index);
438
+ }
439
+ return node;
440
+ };
441
+ Jsep.prototype.gobbleIdentifier = function stampIdentifier() {
442
+ const start = this.index;
443
+ const node = gobbleIdentifier.call(this);
444
+ node.start = start;
445
+ node.end = this.index;
446
+ return node;
447
+ };
448
+ }
449
+ configure();
450
+ function parseRaw(source) {
451
+ return (0, jsep.default)(source);
452
+ }
453
+ //#endregion
454
+ //#region src/formula/parse.ts
455
+ const FORMULA_LIMITS = {
456
+ maxLength: 500,
457
+ maxDepth: 32,
458
+ maxNodes: 200
459
+ };
460
+ var ParseFailure = class extends Error {
461
+ error;
462
+ constructor(error) {
463
+ super(error.code);
464
+ this.error = error;
465
+ }
466
+ };
467
+ function syntaxFailure(detail, span) {
468
+ return new ParseFailure({
469
+ code: "FORMULA_SYNTAX",
470
+ position: span.start,
471
+ length: span.end - span.start,
472
+ params: { detail }
473
+ });
474
+ }
475
+ function span$1(raw, children) {
476
+ return {
477
+ start: Math.min(raw.start ?? Number.POSITIVE_INFINITY, ...children.map((node) => node.start)),
478
+ end: Math.max(raw.end ?? Number.NEGATIVE_INFINITY, ...children.map((node) => node.end))
479
+ };
480
+ }
481
+ function child(raw) {
482
+ return raw;
483
+ }
484
+ function convertIdentifier(raw) {
485
+ return {
486
+ type: "Identifier",
487
+ name: String(raw.name),
488
+ ...span$1(raw, [])
489
+ };
490
+ }
491
+ function convert(raw) {
492
+ switch (raw.type) {
493
+ case "Literal": return {
494
+ type: "Literal",
495
+ value: raw.value,
496
+ ...span$1(raw, [])
497
+ };
498
+ case "Identifier": return convertIdentifier(raw);
499
+ case "MemberExpression": {
500
+ const property = child(raw.property);
501
+ if (raw.computed || raw.optional || property.type !== "Identifier") throw syntaxFailure("Only dotted access is supported", span$1(raw, []));
502
+ const object = convert(child(raw.object));
503
+ const identifier = convertIdentifier(property);
504
+ return {
505
+ type: "Member",
506
+ object,
507
+ property: identifier,
508
+ ...span$1(raw, [object, identifier])
509
+ };
510
+ }
511
+ case "CallExpression": {
512
+ const callee = convert(child(raw.callee));
513
+ const args = raw.arguments.map(convert);
514
+ return {
515
+ type: "Call",
516
+ callee,
517
+ args,
518
+ ...span$1(raw, [callee, ...args])
519
+ };
520
+ }
521
+ case "UnaryExpression": {
522
+ const operator = String(raw.operator);
523
+ const argument = convert(child(raw.argument));
524
+ return {
525
+ type: "Unary",
526
+ operator,
527
+ argument,
528
+ ...span$1(raw, [argument])
529
+ };
530
+ }
531
+ case "BinaryExpression": {
532
+ const operator = String(raw.operator);
533
+ const left = convert(child(raw.left));
534
+ const right = convert(child(raw.right));
535
+ return {
536
+ type: "Binary",
537
+ operator,
538
+ left,
539
+ right,
540
+ ...span$1(raw, [left, right])
541
+ };
542
+ }
543
+ case "ConditionalExpression": {
544
+ const test = convert(child(raw.test));
545
+ const consequent = convert(child(raw.consequent));
546
+ const alternate = convert(child(raw.alternate));
547
+ return {
548
+ type: "Conditional",
549
+ test,
550
+ consequent,
551
+ alternate,
552
+ ...span$1(raw, [
553
+ test,
554
+ consequent,
555
+ alternate
556
+ ])
557
+ };
558
+ }
559
+ case "Compound": throw syntaxFailure("Unexpected expression", raw.body.map(convert)[1]);
560
+ case "SequenceExpression": throw syntaxFailure("Sequences are not supported", raw.expressions.map(convert)[1]);
561
+ default: throw syntaxFailure(`${raw.type} is not supported`, span$1(raw, []));
562
+ }
563
+ }
564
+ function children(node) {
565
+ switch (node.type) {
566
+ case "Literal":
567
+ case "Identifier": return [];
568
+ case "Member": return [node.object, node.property];
569
+ case "Call": return [node.callee, ...node.args];
570
+ case "Unary": return [node.argument];
571
+ case "Binary": return [node.left, node.right];
572
+ case "Conditional": return [
573
+ node.test,
574
+ node.consequent,
575
+ node.alternate
576
+ ];
577
+ }
578
+ }
579
+ function complexityFailure(limit, max, node) {
580
+ return new ParseFailure({
581
+ code: "FORMULA_TOO_COMPLEX",
582
+ position: node.start,
583
+ length: node.end - node.start,
584
+ params: {
585
+ limit,
586
+ max
587
+ }
588
+ });
589
+ }
590
+ function checkComplexity(root) {
591
+ let count = 0;
592
+ const visit = (node, depth) => {
593
+ if (depth > FORMULA_LIMITS.maxDepth) throw complexityFailure("DEPTH", FORMULA_LIMITS.maxDepth, node);
594
+ count += 1;
595
+ if (count > FORMULA_LIMITS.maxNodes) throw complexityFailure("NODES", FORMULA_LIMITS.maxNodes, node);
596
+ for (const next of children(node)) visit(next, depth + 1);
597
+ };
598
+ visit(root, 1);
599
+ }
600
+ function jsepFailure(source, thrown) {
601
+ const { index, description } = thrown;
602
+ return new ParseFailure({
603
+ code: "FORMULA_SYNTAX",
604
+ position: index,
605
+ length: Number(index < source.length),
606
+ params: { detail: description }
607
+ });
608
+ }
609
+ function parse(source) {
610
+ try {
611
+ if (source.length > FORMULA_LIMITS.maxLength) throw new ParseFailure({
612
+ code: "FORMULA_TOO_COMPLEX",
613
+ position: FORMULA_LIMITS.maxLength,
614
+ length: source.length - FORMULA_LIMITS.maxLength,
615
+ params: {
616
+ limit: "LENGTH",
617
+ max: FORMULA_LIMITS.maxLength
618
+ }
619
+ });
620
+ if (source.trim() === "") throw syntaxFailure("Formula is empty", {
621
+ start: 0,
622
+ end: source.length
623
+ });
624
+ let raw;
625
+ try {
626
+ raw = parseRaw(source);
627
+ } catch (thrown) {
628
+ throw jsepFailure(source, thrown);
629
+ }
630
+ const ast = convert(raw);
631
+ checkComplexity(ast);
632
+ return {
633
+ ok: true,
634
+ ast
635
+ };
636
+ } catch (thrown) {
637
+ if (!(thrown instanceof ParseFailure)) throw thrown;
638
+ return {
639
+ ok: false,
640
+ error: thrown.error
641
+ };
642
+ }
643
+ }
644
+ //#endregion
645
+ //#region src/estimate/run-formula.ts
646
+ function message(code, params) {
647
+ return params === void 0 ? { code } : {
648
+ code,
649
+ params: { ...params }
650
+ };
651
+ }
652
+ function runtimeError(reason) {
653
+ return {
654
+ ok: false,
655
+ error: message("FORMULA_RUNTIME_ERROR", { reason })
656
+ };
657
+ }
658
+ function runNumericFormula(source, scope) {
659
+ const parsed = parse(source);
660
+ if (!parsed.ok) return {
661
+ ok: false,
662
+ error: message(parsed.error.code, parsed.error.params)
663
+ };
664
+ const result = evaluate(parsed.ast, scope);
665
+ if (!result.ok) return {
666
+ ok: false,
667
+ error: message(result.error.code, result.error.params)
668
+ };
669
+ if (typeof result.value !== "number") return runtimeError("TYPE_MISMATCH");
670
+ if (result.value < 0) return runtimeError("NEGATIVE");
671
+ return {
672
+ ok: true,
673
+ value: result.value
674
+ };
675
+ }
676
+ //#endregion
677
+ //#region src/estimate/materials.ts
678
+ const isMaterialRule = (rule) => rule.quantityFormula !== null;
679
+ function selectRule(data, categoryId, materialTypeId) {
680
+ const rules = data.rules.filter(isMaterialRule);
681
+ return rules.find((rule) => rule.level === "MATERIAL_TYPE" && rule.categoryId === categoryId && rule.materialTypeId === materialTypeId) ?? rules.find((rule) => rule.level === "CATEGORY" && rule.categoryId === categoryId);
682
+ }
683
+ function materialLine(configuration, data, room, categoryId, productId) {
684
+ const init = {
685
+ id: lineId("MATERIALS", room.clientId, categoryId),
686
+ section: "MATERIALS",
687
+ roomClientId: room.clientId,
688
+ categoryId,
689
+ productId
690
+ };
691
+ const category = data.categories.find((item) => item.id === categoryId);
692
+ const product = data.products.find((item) => item.id === productId && item.categoryId === categoryId);
693
+ if (product === void 0 || category === void 0 || !product.available) return failedLine(init, "UNAVAILABLE", {
694
+ code: "PRODUCT_UNAVAILABLE",
695
+ params: { productId }
696
+ });
697
+ const warnings = room.heatedFloor && category.surface === "FLOOR" && !product.heatedFloorCompatible ? [{
698
+ code: "HEATED_FLOOR_INCOMPATIBLE",
699
+ params: { productId }
700
+ }] : [];
701
+ const rule = selectRule(data, categoryId, product.materialTypeId);
702
+ if (rule === void 0) return failedLine({
703
+ ...init,
704
+ warnings
705
+ }, "NOT_CALCULATED", {
706
+ code: "RULE_NOT_FOUND",
707
+ params: {
708
+ categoryId,
709
+ materialTypeId: product.materialTypeId
710
+ }
711
+ });
712
+ const variables = {
713
+ ...roomVariables(room),
714
+ ...productVariables(product, category),
715
+ ...objectVariables(configuration)
716
+ };
717
+ const { coefficients } = data;
718
+ const ruled = {
719
+ ...init,
720
+ warnings,
721
+ ruleId: rule.id
722
+ };
723
+ const quantity = runNumericFormula(rule.quantityFormula, {
724
+ variables,
725
+ coefficients
726
+ });
727
+ if (!quantity.ok) return failedLine(ruled, "ERROR", quantity.error);
728
+ const cost = runNumericFormula(rule.costFormula, {
729
+ variables: {
730
+ ...variables,
731
+ quantity: quantity.value
732
+ },
733
+ coefficients
734
+ });
735
+ if (!cost.ok) return failedLine(ruled, "ERROR", cost.error);
736
+ return pricedLine({
737
+ ...ruled,
738
+ quantity: roundQuantity(quantity.value),
739
+ unit: product.unit,
740
+ unitPriceCents: product.priceCents
741
+ }, cost.value);
742
+ }
743
+ function materialLines(configuration, data) {
744
+ return configuration.rooms.flatMap((room) => room.selections.flatMap(({ categoryId, productId }) => productId === null ? [] : [materialLine(configuration, data, room, categoryId, productId)]));
745
+ }
746
+ //#endregion
747
+ //#region src/estimate/object-rules.ts
748
+ function objectRuleLines(configuration, data) {
749
+ const scope = {
750
+ variables: objectVariables(configuration),
751
+ coefficients: data.coefficients
752
+ };
753
+ return data.rules.filter((rule) => rule.level === "OBJECT").sort((a, b) => a.sortOrder - b.sortOrder).map((rule) => {
754
+ const init = {
755
+ id: lineId("OBJECT", rule.id),
756
+ section: "OBJECT",
757
+ ruleId: rule.id
758
+ };
759
+ const outcome = runNumericFormula(rule.costFormula, scope);
760
+ return outcome.ok ? pricedLine(init, outcome.value) : failedLine(init, "ERROR", outcome.error);
761
+ });
762
+ }
763
+ //#endregion
764
+ //#region src/schemas/common.ts
765
+ const LocalizedTextSchema = zod.z.object({
766
+ en: zod.z.string(),
767
+ uk: zod.z.string()
768
+ });
769
+ const FinishLevelSchema = zod.z.enum(["ROUGH", "PRE_FINISH"]);
770
+ const RuleLevelSchema = zod.z.enum([
771
+ "OBJECT",
772
+ "CATEGORY",
773
+ "MATERIAL_TYPE"
774
+ ]);
775
+ const RateSourceSchema = zod.z.enum(["NBU", "MANUAL"]);
776
+ const SurfaceKindSchema = zod.z.enum([
777
+ "NONE",
778
+ "FLOOR",
779
+ "WALLS",
780
+ "CEILING"
781
+ ]);
782
+ const ProductUnitSchema = zod.z.enum([
783
+ "SQM",
784
+ "LINEAR_M",
785
+ "PIECE"
786
+ ]);
787
+ const OptionUnitSchema = zod.z.enum([
788
+ "PIECE",
789
+ "ROOM_SQM",
790
+ "ROOM",
791
+ "PROJECT"
792
+ ]);
793
+ const OptionKindSchema = zod.z.enum(["ENGINEERING", "ADDITIONAL"]);
794
+ const OptionCodeSchema = zod.z.enum(["HEATED_FLOOR"]);
795
+ const RoomTypeCodeSchema = zod.z.enum([
796
+ "LIVING_ROOM",
797
+ "BEDROOM",
798
+ "KITCHEN",
799
+ "KITCHEN_LIVING",
800
+ "BATHROOM"
801
+ ]);
802
+ const KitchenTypeSchema = zod.z.enum(["SEPARATE", "KITCHEN_LIVING"]);
803
+ const CurrencySchema = zod.z.enum(["USD", "UAH"]);
804
+ const SectionSchema = zod.z.enum([
805
+ "BASE",
806
+ "OBJECT",
807
+ "MATERIALS",
808
+ "ENGINEERING",
809
+ "ADDITIONAL"
810
+ ]);
811
+ const LineStatusSchema = zod.z.enum([
812
+ "OK",
813
+ "INCLUDED",
814
+ "NOT_CALCULATED",
815
+ "UNAVAILABLE",
816
+ "ERROR"
817
+ ]);
818
+ const CentsSchema = zod.z.int().min(0);
819
+ const RangeSchema = zod.z.object({
820
+ min: zod.z.number(),
821
+ max: zod.z.number()
822
+ });
823
+ //#endregion
824
+ //#region src/schemas/configuration.ts
825
+ const RoomSelectionSchema = zod.z.object({
826
+ categoryId: zod.z.string(),
827
+ productId: zod.z.string().nullable()
828
+ });
829
+ const RoomConfigurationSchema = zod.z.object({
830
+ clientId: zod.z.string(),
831
+ name: zod.z.string().trim().min(1).max(60).optional(),
832
+ roomTypeCode: RoomTypeCodeSchema,
833
+ lengthM: zod.z.number().positive(),
834
+ widthM: zod.z.number().positive(),
835
+ heightM: zod.z.number().positive(),
836
+ heatedFloor: zod.z.boolean(),
837
+ selections: zod.z.array(RoomSelectionSchema)
838
+ });
839
+ const OptionSelectionSchema = zod.z.object({
840
+ optionId: zod.z.string(),
841
+ quantity: zod.z.int().positive().optional(),
842
+ roomClientIds: zod.z.array(zod.z.string()).optional()
843
+ });
844
+ function duplicateIndexes(items, key) {
845
+ const seen = /* @__PURE__ */ new Set();
846
+ return items.flatMap((item, index) => {
847
+ const value = key(item);
848
+ if (seen.has(value)) return [index];
849
+ seen.add(value);
850
+ return [];
851
+ });
852
+ }
853
+ const ConfigurationSchema = zod.z.object({
854
+ finishLevel: FinishLevelSchema,
855
+ totalAreaSqm: zod.z.number().positive(),
856
+ balconyAreaSqm: zod.z.number().min(0),
857
+ balconyTiles: zod.z.boolean(),
858
+ gas: zod.z.boolean(),
859
+ kitchenType: KitchenTypeSchema,
860
+ rooms: zod.z.array(RoomConfigurationSchema),
861
+ options: zod.z.array(OptionSelectionSchema),
862
+ currency: CurrencySchema
863
+ }).superRefine((configuration, ctx) => {
864
+ const addDuplicate = (path) => ctx.addIssue({
865
+ code: "custom",
866
+ path,
867
+ message: "Duplicate"
868
+ });
869
+ for (const index of duplicateIndexes(configuration.rooms, (room) => room.clientId)) addDuplicate([
870
+ "rooms",
871
+ index,
872
+ "clientId"
873
+ ]);
874
+ configuration.rooms.forEach((room, roomIndex) => {
875
+ for (const index of duplicateIndexes(room.selections, (selection) => selection.categoryId)) addDuplicate([
876
+ "rooms",
877
+ roomIndex,
878
+ "selections",
879
+ index,
880
+ "categoryId"
881
+ ]);
882
+ });
883
+ for (const index of duplicateIndexes(configuration.options, (option) => option.optionId)) addDuplicate([
884
+ "options",
885
+ index,
886
+ "optionId"
887
+ ]);
888
+ });
889
+ //#endregion
890
+ //#region src/schemas/pricing-data.ts
891
+ const ExchangeRateSchema = zod.z.object({
892
+ rate: zod.z.string().regex(/^\d+\.\d{4}$/),
893
+ date: zod.z.string(),
894
+ source: RateSourceSchema
895
+ });
896
+ const PricingLimitsSchema = zod.z.object({
897
+ totalAreaSqm: RangeSchema,
898
+ rooms: RangeSchema,
899
+ roomSideM: RangeSchema,
900
+ roomHeightM: RangeSchema
901
+ });
902
+ const BaseRateSchema = zod.z.object({
903
+ finishLevel: FinishLevelSchema,
904
+ areaFromSqm: zod.z.number().min(0),
905
+ areaToSqm: zod.z.number(),
906
+ rateCentsPerSqm: zod.z.int().positive()
907
+ });
908
+ const PricingRuleSchema = zod.z.object({
909
+ id: zod.z.string(),
910
+ level: RuleLevelSchema,
911
+ categoryId: zod.z.string().nullable(),
912
+ materialTypeId: zod.z.string().nullable(),
913
+ name: LocalizedTextSchema,
914
+ quantityFormula: zod.z.string().nullable(),
915
+ costFormula: zod.z.string().max(500),
916
+ sortOrder: zod.z.int()
917
+ }).refine((rule) => rule.level === "OBJECT" === (rule.quantityFormula === null), { path: ["quantityFormula"] });
918
+ const PricingRoomTypeSchema = zod.z.object({
919
+ id: zod.z.string(),
920
+ code: zod.z.string()
921
+ });
922
+ const PricingCategorySchema = zod.z.object({
923
+ id: zod.z.string(),
924
+ wastePercent: zod.z.number().min(0),
925
+ surface: SurfaceKindSchema
926
+ });
927
+ const PricingMaterialTypeSchema = zod.z.object({
928
+ id: zod.z.string(),
929
+ code: zod.z.string()
930
+ });
931
+ const PricingProductSchema = zod.z.object({
932
+ id: zod.z.string(),
933
+ categoryId: zod.z.string(),
934
+ materialTypeId: zod.z.string(),
935
+ priceCents: CentsSchema,
936
+ unit: ProductUnitSchema,
937
+ wastePercentOverride: zod.z.number().min(0).nullable(),
938
+ heatedFloorCompatible: zod.z.boolean(),
939
+ available: zod.z.boolean()
940
+ });
941
+ const PricingPackageItemSchema = zod.z.object({
942
+ id: zod.z.string(),
943
+ includedInBase: zod.z.boolean(),
944
+ priceCents: CentsSchema.nullable(),
945
+ unit: OptionUnitSchema.nullable()
946
+ });
947
+ const PricingOptionSchema = zod.z.object({
948
+ id: zod.z.string(),
949
+ code: OptionCodeSchema.nullable(),
950
+ kind: OptionKindSchema,
951
+ priceCents: CentsSchema,
952
+ unit: OptionUnitSchema,
953
+ roomTypeCodes: zod.z.array(zod.z.string()),
954
+ minQuantity: zod.z.int().nullable(),
955
+ maxQuantity: zod.z.int().nullable(),
956
+ available: zod.z.boolean()
957
+ });
958
+ const PricingDataSchema = zod.z.object({
959
+ version: zod.z.string().regex(/^[0-9a-f]{16}$/),
960
+ exchangeRate: ExchangeRateSchema.nullable(),
961
+ limits: PricingLimitsSchema,
962
+ baseRates: zod.z.array(BaseRateSchema),
963
+ coefficients: zod.z.record(zod.z.string(), zod.z.number()),
964
+ rules: zod.z.array(PricingRuleSchema),
965
+ roomTypes: zod.z.array(PricingRoomTypeSchema),
966
+ categories: zod.z.array(PricingCategorySchema),
967
+ materialTypes: zod.z.array(PricingMaterialTypeSchema),
968
+ products: zod.z.array(PricingProductSchema),
969
+ packageItems: zod.z.array(PricingPackageItemSchema),
970
+ options: zod.z.array(PricingOptionSchema)
971
+ });
972
+ //#endregion
973
+ //#region src/schemas/estimate.ts
974
+ const EstimateMessageSchema = zod.z.object({
975
+ code: zod.z.string(),
976
+ params: zod.z.record(zod.z.string(), zod.z.unknown()).optional()
977
+ });
978
+ const EstimateLineSchema = zod.z.object({
979
+ id: zod.z.string(),
980
+ section: SectionSchema,
981
+ status: LineStatusSchema,
982
+ roomClientId: zod.z.string().nullable(),
983
+ categoryId: zod.z.string().nullable(),
984
+ productId: zod.z.string().nullable(),
985
+ optionId: zod.z.string().nullable(),
986
+ packageItemId: zod.z.string().nullable(),
987
+ ruleId: zod.z.string().nullable(),
988
+ quantity: zod.z.number().nullable(),
989
+ unit: zod.z.string().nullable(),
990
+ unitPriceCents: CentsSchema.nullable(),
991
+ amountCents: CentsSchema,
992
+ amountUahKopecks: CentsSchema.nullable(),
993
+ warnings: zod.z.array(EstimateMessageSchema),
994
+ error: EstimateMessageSchema.nullable()
995
+ });
996
+ const SectionTotalSchema = zod.z.object({
997
+ usdCents: CentsSchema,
998
+ uahKopecks: CentsSchema.nullable()
999
+ });
1000
+ const EstimateTotalsSchema = zod.z.object({
1001
+ usdCents: CentsSchema,
1002
+ uahKopecks: CentsSchema.nullable(),
1003
+ sections: zod.z.record(SectionSchema, SectionTotalSchema)
1004
+ });
1005
+ const EstimateWarningSchema = EstimateMessageSchema.extend({ lineId: zod.z.string().optional() });
1006
+ const EstimateSchema = zod.z.object({
1007
+ pricingVersion: zod.z.string(),
1008
+ currency: CurrencySchema,
1009
+ exchangeRate: ExchangeRateSchema.nullable(),
1010
+ lines: zod.z.array(EstimateLineSchema),
1011
+ totals: EstimateTotalsSchema,
1012
+ warnings: zod.z.array(EstimateWarningSchema),
1013
+ incomplete: zod.z.boolean()
1014
+ });
1015
+ //#endregion
1016
+ //#region src/schemas/preview.ts
1017
+ const RuleSampleSchema = zod.z.object({
1018
+ room: zod.z.object({
1019
+ roomTypeCode: RoomTypeCodeSchema,
1020
+ lengthM: zod.z.number().positive(),
1021
+ widthM: zod.z.number().positive(),
1022
+ heightM: zod.z.number().positive(),
1023
+ heatedFloor: zod.z.boolean()
1024
+ }).optional(),
1025
+ product: zod.z.object({
1026
+ priceCents: CentsSchema,
1027
+ unit: ProductUnitSchema,
1028
+ wastePercent: zod.z.number().min(0)
1029
+ }).optional(),
1030
+ object: zod.z.object({
1031
+ totalAreaSqm: zod.z.number().positive(),
1032
+ balconyAreaSqm: zod.z.number().min(0),
1033
+ balconyTiles: zod.z.boolean(),
1034
+ gas: zod.z.boolean(),
1035
+ kitchenType: KitchenTypeSchema,
1036
+ finishLevel: FinishLevelSchema,
1037
+ roomsCount: zod.z.int().min(0),
1038
+ bathroomsCount: zod.z.int().min(0)
1039
+ })
1040
+ });
1041
+ const RulePreviewSchema = zod.z.object({
1042
+ quantity: zod.z.number().nullable(),
1043
+ amountCents: CentsSchema.nullable(),
1044
+ variables: zod.z.record(zod.z.string(), zod.z.union([
1045
+ zod.z.number(),
1046
+ zod.z.boolean(),
1047
+ zod.z.string()
1048
+ ])),
1049
+ coefficients: zod.z.record(zod.z.string(), zod.z.number()),
1050
+ error: EstimateMessageSchema.nullable()
1051
+ });
1052
+ //#endregion
1053
+ //#region src/estimate/totals.ts
1054
+ function sumTotal(lines, hasRate) {
1055
+ return {
1056
+ usdCents: lines.reduce((sum, line) => sum + line.amountCents, 0),
1057
+ uahKopecks: hasRate ? lines.reduce((sum, line) => sum + (line.amountUahKopecks ?? 0), 0) : null
1058
+ };
1059
+ }
1060
+ function computeTotals(lines, hasRate) {
1061
+ const sections = Object.fromEntries(SectionSchema.options.map((section) => [section, sumTotal(lines.filter((line) => line.section === section), hasRate)]));
1062
+ return {
1063
+ ...sumTotal(lines, hasRate),
1064
+ sections
1065
+ };
1066
+ }
1067
+ function collectWarnings(lines) {
1068
+ return lines.flatMap((line) => line.warnings.map((warning) => ({
1069
+ ...warning,
1070
+ lineId: line.id
1071
+ })));
1072
+ }
1073
+ function isIncomplete(lines) {
1074
+ return lines.some((line) => line.status === "ERROR" || line.status === "NOT_CALCULATED");
1075
+ }
1076
+ //#endregion
1077
+ //#region src/estimate/warnings.ts
1078
+ function roomsAreaWarnings(configuration) {
1079
+ const roomsAreaSqm = Math.round(configuration.rooms.reduce((sum, room) => sum + roomVariables(room).floorArea, 0) * 1e4) / 1e4;
1080
+ return roomsAreaSqm > configuration.totalAreaSqm ? [{
1081
+ code: "ROOMS_AREA_EXCEEDS_TOTAL",
1082
+ params: {
1083
+ roomsAreaSqm,
1084
+ totalAreaSqm: configuration.totalAreaSqm
1085
+ }
1086
+ }] : [];
1087
+ }
1088
+ function estimateWarnings(lines, configuration, rate) {
1089
+ return [
1090
+ ...collectWarnings(lines),
1091
+ ...roomsAreaWarnings(configuration),
1092
+ ...configuration.currency === "UAH" && rate === null ? [{ code: "EXCHANGE_RATE_UNAVAILABLE" }] : []
1093
+ ];
1094
+ }
1095
+ //#endregion
1096
+ //#region src/estimate/estimate.ts
1097
+ function withKopecks(line, rate) {
1098
+ return {
1099
+ ...line,
1100
+ amountUahKopecks: rate === null ? null : convertCentsToKopecks(line.amountCents, rate)
1101
+ };
1102
+ }
1103
+ function estimate(configuration, data) {
1104
+ const rate = data.exchangeRate?.rate ?? null;
1105
+ const lines = [
1106
+ baseLine(configuration, data),
1107
+ ...objectRuleLines(configuration, data),
1108
+ ...materialLines(configuration, data),
1109
+ ...engineeringLines(configuration, data),
1110
+ ...additionalLines(configuration, data)
1111
+ ].map((line) => withKopecks(line, rate));
1112
+ return {
1113
+ pricingVersion: data.version,
1114
+ currency: configuration.currency,
1115
+ exchangeRate: data.exchangeRate,
1116
+ lines,
1117
+ totals: computeTotals(lines, rate !== null),
1118
+ warnings: estimateWarnings(lines, configuration, rate),
1119
+ incomplete: isIncomplete(lines)
1120
+ };
1121
+ }
1122
+ //#endregion
1123
+ //#region src/estimate/limits.ts
1124
+ function checkLimits(configuration, limits, options = []) {
1125
+ const violations = [];
1126
+ const check = (field, value, min, max) => {
1127
+ if (value < min || value > max) violations.push({
1128
+ field,
1129
+ min,
1130
+ max
1131
+ });
1132
+ };
1133
+ check("totalAreaSqm", configuration.totalAreaSqm, limits.totalAreaSqm.min, limits.totalAreaSqm.max);
1134
+ check("balconyAreaSqm", configuration.balconyAreaSqm, 0, configuration.totalAreaSqm);
1135
+ check("rooms", configuration.rooms.length, limits.rooms.min, limits.rooms.max);
1136
+ configuration.rooms.forEach((room, index) => {
1137
+ const path = `rooms[${index}]`;
1138
+ check(`${path}.lengthM`, room.lengthM, limits.roomSideM.min, limits.roomSideM.max);
1139
+ check(`${path}.widthM`, room.widthM, limits.roomSideM.min, limits.roomSideM.max);
1140
+ check(`${path}.heightM`, room.heightM, limits.roomHeightM.min, limits.roomHeightM.max);
1141
+ });
1142
+ configuration.options.forEach((selection, index) => {
1143
+ const option = options.find((candidate) => candidate.id === selection.optionId);
1144
+ if (option?.unit !== "PIECE" || selection.quantity === void 0) return;
1145
+ check(`options[${index}].quantity`, selection.quantity, option.minQuantity ?? 1, option.maxQuantity ?? Number.MAX_SAFE_INTEGER);
1146
+ });
1147
+ return violations;
1148
+ }
1149
+ //#endregion
1150
+ //#region src/formula/preview.ts
1151
+ function collectReferences(node, found) {
1152
+ switch (node.type) {
1153
+ case "Identifier":
1154
+ found.variables.add(node.name);
1155
+ return;
1156
+ case "Member":
1157
+ if (node.object.type === "Identifier" && node.object.name === "coef") found.coefficients.add(node.property.name);
1158
+ return;
1159
+ case "Call":
1160
+ node.args.forEach((argument) => collectReferences(argument, found));
1161
+ return;
1162
+ case "Unary":
1163
+ collectReferences(node.argument, found);
1164
+ return;
1165
+ case "Binary":
1166
+ collectReferences(node.left, found);
1167
+ collectReferences(node.right, found);
1168
+ return;
1169
+ case "Conditional":
1170
+ collectReferences(node.test, found);
1171
+ collectReferences(node.consequent, found);
1172
+ collectReferences(node.alternate, found);
1173
+ return;
1174
+ case "Literal": return;
1175
+ }
1176
+ }
1177
+ function referencesOf(sources) {
1178
+ const found = {
1179
+ variables: /* @__PURE__ */ new Set(),
1180
+ coefficients: /* @__PURE__ */ new Set()
1181
+ };
1182
+ for (const source of sources) {
1183
+ const parsed = source === null ? void 0 : parse(source);
1184
+ if (parsed?.ok) collectReferences(parsed.ast, found);
1185
+ }
1186
+ return found;
1187
+ }
1188
+ function objectScope({ object }) {
1189
+ return {
1190
+ totalArea: object.totalAreaSqm,
1191
+ balconyArea: object.balconyAreaSqm,
1192
+ balconyTiles: object.balconyTiles,
1193
+ gas: object.gas,
1194
+ kitchenType: object.kitchenType,
1195
+ finishLevel: object.finishLevel,
1196
+ roomsCount: object.roomsCount,
1197
+ bathroomsCount: object.bathroomsCount
1198
+ };
1199
+ }
1200
+ function scopeOf(level, sample) {
1201
+ if (level === "OBJECT") return { scope: objectScope(sample) };
1202
+ const { room, product } = sample;
1203
+ if (room === void 0 || product === void 0) return { error: {
1204
+ code: "SAMPLE_INCOMPLETE",
1205
+ params: { missing: room === void 0 ? "room" : "product" }
1206
+ } };
1207
+ return { scope: {
1208
+ ...roomVariables(room),
1209
+ ...productVariablesOf(product, product.wastePercent),
1210
+ ...objectScope(sample)
1211
+ } };
1212
+ }
1213
+ function pick(source, names) {
1214
+ return Object.fromEntries(Object.entries(source).filter(([name]) => names.has(name)));
1215
+ }
1216
+ function previewRule(input) {
1217
+ const { level, quantityFormula, costFormula, coefficients } = input;
1218
+ const references = referencesOf([quantityFormula, costFormula]);
1219
+ const report = (variables, quantity, amountCents, error) => ({
1220
+ quantity,
1221
+ amountCents,
1222
+ variables: pick(variables, references.variables),
1223
+ coefficients: pick(coefficients, references.coefficients),
1224
+ error
1225
+ });
1226
+ if (level === "OBJECT" !== (quantityFormula === null)) return report({}, null, null, { code: "QUANTITY_FORMULA_MISMATCH" });
1227
+ const prepared = scopeOf(level, input.sample);
1228
+ if ("error" in prepared) return report({}, null, null, prepared.error);
1229
+ let variables = prepared.scope;
1230
+ let quantity = null;
1231
+ if (quantityFormula !== null) {
1232
+ const raw = runNumericFormula(quantityFormula, {
1233
+ variables,
1234
+ coefficients
1235
+ });
1236
+ if (!raw.ok) return report(variables, null, null, raw.error);
1237
+ variables = {
1238
+ ...variables,
1239
+ quantity: raw.value
1240
+ };
1241
+ quantity = roundQuantity(raw.value);
1242
+ }
1243
+ const cost = runNumericFormula(costFormula, {
1244
+ variables,
1245
+ coefficients
1246
+ });
1247
+ if (!cost.ok) return report(variables, quantity, null, cost.error);
1248
+ const line = pricedLine({
1249
+ id: "preview",
1250
+ section: "OBJECT"
1251
+ }, cost.value);
1252
+ return line.error === null ? report(variables, quantity, line.amountCents, null) : report(variables, quantity, null, line.error);
1253
+ }
1254
+ //#endregion
1255
+ //#region src/formula/suggest.ts
1256
+ function levenshtein(a, b) {
1257
+ let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
1258
+ for (let i = 1; i <= a.length; i++) {
1259
+ const current = [i];
1260
+ for (let j = 1; j <= b.length; j++) {
1261
+ const substitution = a[i - 1] === b[j - 1] ? 0 : 1;
1262
+ current[j] = Math.min(previous[j] + 1, current[j - 1] + 1, previous[j - 1] + substitution);
1263
+ }
1264
+ previous = current;
1265
+ }
1266
+ return previous[b.length];
1267
+ }
1268
+ function suggestNearest(name, candidates, maxDistance = 2) {
1269
+ let best;
1270
+ let bestDistance = maxDistance + 1;
1271
+ for (const candidate of candidates) {
1272
+ const distance = levenshtein(name, candidate);
1273
+ if (distance < bestDistance || distance === bestDistance && best !== void 0 && candidate < best) {
1274
+ best = candidate;
1275
+ bestDistance = distance;
1276
+ }
1277
+ }
1278
+ return best;
1279
+ }
1280
+ //#endregion
1281
+ //#region src/formula/validate.ts
1282
+ const NUMBER = { kind: "number" };
1283
+ const BOOLEAN = { kind: "boolean" };
1284
+ const STRING = { kind: "string" };
1285
+ const UNKNOWN = { kind: "unknown" };
1286
+ function typeName(type) {
1287
+ return type.kind === "enum" ? type.name : type.kind;
1288
+ }
1289
+ const number = (name) => ({
1290
+ name,
1291
+ kind: "number"
1292
+ });
1293
+ const boolean = (name) => ({
1294
+ name,
1295
+ kind: "boolean"
1296
+ });
1297
+ const enumeration = (name, values) => ({
1298
+ name,
1299
+ kind: "enum",
1300
+ values
1301
+ });
1302
+ const FORMULA_SCOPE_VARIABLES = {
1303
+ room: [
1304
+ number("length"),
1305
+ number("width"),
1306
+ number("height"),
1307
+ number("floorArea"),
1308
+ number("perimeter"),
1309
+ number("wallArea"),
1310
+ boolean("heatedFloor"),
1311
+ enumeration("roomType", RoomTypeCodeSchema.options)
1312
+ ],
1313
+ product: [
1314
+ number("price"),
1315
+ number("waste"),
1316
+ enumeration("unit", ProductUnitSchema.options)
1317
+ ],
1318
+ object: [
1319
+ number("totalArea"),
1320
+ number("balconyArea"),
1321
+ boolean("balconyTiles"),
1322
+ boolean("gas"),
1323
+ enumeration("kitchenType", KitchenTypeSchema.options),
1324
+ enumeration("finishLevel", FinishLevelSchema.options),
1325
+ number("roomsCount"),
1326
+ number("bathroomsCount")
1327
+ ]
1328
+ };
1329
+ const FORMULA_QUANTITY_VARIABLE = number("quantity");
1330
+ const FORMULA_VARIABLES = {
1331
+ OBJECT: FORMULA_SCOPE_VARIABLES.object,
1332
+ CATEGORY: [
1333
+ ...FORMULA_SCOPE_VARIABLES.room,
1334
+ ...FORMULA_SCOPE_VARIABLES.product,
1335
+ ...FORMULA_SCOPE_VARIABLES.object
1336
+ ],
1337
+ MATERIAL_TYPE: [
1338
+ ...FORMULA_SCOPE_VARIABLES.room,
1339
+ ...FORMULA_SCOPE_VARIABLES.product,
1340
+ ...FORMULA_SCOPE_VARIABLES.object
1341
+ ]
1342
+ };
1343
+ const ALL_VARIABLE_NAMES = new Set(FORMULA_VARIABLES.CATEGORY.map((variable) => variable.name));
1344
+ function span(node) {
1345
+ return {
1346
+ position: node.start,
1347
+ length: node.end - node.start
1348
+ };
1349
+ }
1350
+ function validateFormula(input) {
1351
+ const parsed = parse(input.source);
1352
+ if (!parsed.ok) return {
1353
+ valid: false,
1354
+ coefficientKeys: [],
1355
+ errors: [parsed.error]
1356
+ };
1357
+ const variables = FORMULA_VARIABLES[input.level];
1358
+ const variableTypes = new Map(variables.map((variable) => [variable.name, variable.kind === "enum" ? {
1359
+ kind: "enum",
1360
+ name: variable.name,
1361
+ values: variable.values ?? []
1362
+ } : { kind: variable.kind }]));
1363
+ const allowedNames = variables.map((variable) => variable.name);
1364
+ const knownCoefficients = new Set(input.coefficientKeys);
1365
+ const usedCoefficients = /* @__PURE__ */ new Set();
1366
+ const errors = [];
1367
+ const quantityAllowed = input.kind === "cost" && input.level !== "OBJECT";
1368
+ function mismatch(node, expected) {
1369
+ errors.push({
1370
+ code: "FORMULA_SYNTAX",
1371
+ ...span(node),
1372
+ params: {
1373
+ reason: "TYPE_MISMATCH",
1374
+ expected
1375
+ }
1376
+ });
1377
+ }
1378
+ function expectType(node, expected) {
1379
+ const type = infer(node);
1380
+ if (type.kind === "unknown") return false;
1381
+ if (type.kind !== expected) {
1382
+ mismatch(node, expected);
1383
+ return false;
1384
+ }
1385
+ return true;
1386
+ }
1387
+ function identifier(node) {
1388
+ const { name } = node;
1389
+ if (name === FORMULA_QUANTITY_VARIABLE.name) {
1390
+ if (!quantityAllowed) {
1391
+ errors.push({
1392
+ code: "QUANTITY_NOT_ALLOWED",
1393
+ ...span(node)
1394
+ });
1395
+ return UNKNOWN;
1396
+ }
1397
+ return NUMBER;
1398
+ }
1399
+ const type = variableTypes.get(name);
1400
+ if (type !== void 0) return type;
1401
+ if (ALL_VARIABLE_NAMES.has(name)) {
1402
+ errors.push({
1403
+ code: "VARIABLE_NOT_ALLOWED_AT_LEVEL",
1404
+ ...span(node),
1405
+ params: {
1406
+ variable: name,
1407
+ level: input.level
1408
+ }
1409
+ });
1410
+ return UNKNOWN;
1411
+ }
1412
+ const suggestion = suggestNearest(name, allowedNames);
1413
+ errors.push({
1414
+ code: "UNKNOWN_VARIABLE",
1415
+ ...span(node),
1416
+ params: suggestion === void 0 ? { name } : {
1417
+ name,
1418
+ suggestion
1419
+ }
1420
+ });
1421
+ return UNKNOWN;
1422
+ }
1423
+ function member(node) {
1424
+ const { object, property } = node;
1425
+ if (object.type !== "Identifier" || object.name !== "coef") {
1426
+ errors.push({
1427
+ code: "UNKNOWN_VARIABLE",
1428
+ ...span(node),
1429
+ params: { name: input.source.slice(node.start, node.end) }
1430
+ });
1431
+ return UNKNOWN;
1432
+ }
1433
+ usedCoefficients.add(property.name);
1434
+ if (!knownCoefficients.has(property.name)) {
1435
+ errors.push({
1436
+ code: "UNKNOWN_COEFFICIENT",
1437
+ ...span(node),
1438
+ params: { key: property.name }
1439
+ });
1440
+ return UNKNOWN;
1441
+ }
1442
+ return NUMBER;
1443
+ }
1444
+ function call(node) {
1445
+ const { callee } = node;
1446
+ const fn = callee.type === "Identifier" ? FORMULA_FUNCTION_TABLE.get(callee.name) : void 0;
1447
+ if (fn === void 0) {
1448
+ errors.push({
1449
+ code: "UNKNOWN_FUNCTION",
1450
+ ...span(callee),
1451
+ params: { name: input.source.slice(callee.start, callee.end) }
1452
+ });
1453
+ for (const argument of node.args) infer(argument);
1454
+ return UNKNOWN;
1455
+ }
1456
+ let sound = true;
1457
+ if (node.args.length < fn.minArgs || node.args.length > fn.maxArgs) {
1458
+ errors.push({
1459
+ code: "FORMULA_SYNTAX",
1460
+ ...span(node),
1461
+ params: {
1462
+ reason: "ARGUMENT_COUNT",
1463
+ min: fn.minArgs,
1464
+ ...Number.isFinite(fn.maxArgs) ? { max: fn.maxArgs } : {}
1465
+ }
1466
+ });
1467
+ sound = false;
1468
+ }
1469
+ for (const argument of node.args) sound = expectType(argument, "number") && sound;
1470
+ return sound ? NUMBER : UNKNOWN;
1471
+ }
1472
+ function enumLiteral(literal, other) {
1473
+ const type = infer(other);
1474
+ if (type.kind === "unknown") return;
1475
+ if (other.type !== "Identifier" || type.kind !== "enum") {
1476
+ mismatch(other, "enum variable");
1477
+ return;
1478
+ }
1479
+ if (!type.values.includes(literal.value)) errors.push({
1480
+ code: "FORMULA_SYNTAX",
1481
+ ...span(literal),
1482
+ params: {
1483
+ reason: "UNKNOWN_ENUM_VALUE",
1484
+ value: literal.value,
1485
+ variable: type.name
1486
+ }
1487
+ });
1488
+ }
1489
+ function equality(node) {
1490
+ const { left, right } = node;
1491
+ const literal = [left, right].find((side) => side.type === "Literal" && typeof side.value === "string");
1492
+ if (literal !== void 0) {
1493
+ enumLiteral(literal, literal === left ? right : left);
1494
+ return BOOLEAN;
1495
+ }
1496
+ const a = infer(left);
1497
+ const b = infer(right);
1498
+ if (a.kind !== "unknown" && b.kind !== "unknown" && a.kind !== b.kind) mismatch(right, typeName(a));
1499
+ return BOOLEAN;
1500
+ }
1501
+ function binary(node) {
1502
+ const { operator, left, right } = node;
1503
+ if (operator === "==" || operator === "!=") return equality(node);
1504
+ const logical = operator === "&&" || operator === "||";
1505
+ const operand = logical ? "boolean" : "number";
1506
+ const leftSound = expectType(left, operand);
1507
+ const rightSound = expectType(right, operand);
1508
+ if (!(leftSound && rightSound)) return UNKNOWN;
1509
+ const arithmetic = [
1510
+ "+",
1511
+ "-",
1512
+ "*",
1513
+ "/"
1514
+ ].includes(operator);
1515
+ return logical || !arithmetic ? BOOLEAN : NUMBER;
1516
+ }
1517
+ function unary(node) {
1518
+ const operand = node.operator === "!" ? "boolean" : "number";
1519
+ return expectType(node.argument, operand) ? { kind: operand } : UNKNOWN;
1520
+ }
1521
+ function conditional(node) {
1522
+ const testSound = expectType(node.test, "boolean");
1523
+ const consequent = infer(node.consequent);
1524
+ const alternate = infer(node.alternate);
1525
+ if (consequent.kind === "unknown") return alternate;
1526
+ if (alternate.kind === "unknown") return consequent;
1527
+ if (consequent.kind !== alternate.kind) {
1528
+ mismatch(node.alternate, typeName(consequent));
1529
+ return UNKNOWN;
1530
+ }
1531
+ return testSound ? consequent : UNKNOWN;
1532
+ }
1533
+ function infer(node) {
1534
+ switch (node.type) {
1535
+ case "Literal":
1536
+ if (typeof node.value === "string") return STRING;
1537
+ return typeof node.value === "number" ? NUMBER : BOOLEAN;
1538
+ case "Identifier": return identifier(node);
1539
+ case "Member": return member(node);
1540
+ case "Call": return call(node);
1541
+ case "Unary": return unary(node);
1542
+ case "Binary": return binary(node);
1543
+ case "Conditional": return conditional(node);
1544
+ }
1545
+ }
1546
+ const resultType = infer(parsed.ast);
1547
+ if (resultType.kind !== "unknown" && resultType.kind !== "number") mismatch(parsed.ast, "number");
1548
+ return {
1549
+ valid: errors.length === 0,
1550
+ coefficientKeys: [...usedCoefficients],
1551
+ errors
1552
+ };
1553
+ }
1554
+ //#endregion
1555
+ exports.BaseRateSchema = BaseRateSchema;
1556
+ exports.COEFFICIENT_NAMESPACE = COEFFICIENT_NAMESPACE;
1557
+ exports.CentsSchema = CentsSchema;
1558
+ exports.ConfigurationSchema = ConfigurationSchema;
1559
+ exports.CurrencySchema = CurrencySchema;
1560
+ exports.EstimateLineSchema = EstimateLineSchema;
1561
+ exports.EstimateMessageSchema = EstimateMessageSchema;
1562
+ exports.EstimateSchema = EstimateSchema;
1563
+ exports.EstimateTotalsSchema = EstimateTotalsSchema;
1564
+ exports.EstimateWarningSchema = EstimateWarningSchema;
1565
+ exports.ExchangeRateSchema = ExchangeRateSchema;
1566
+ exports.FORMULA_FUNCTIONS = FORMULA_FUNCTIONS;
1567
+ exports.FORMULA_FUNCTION_TABLE = FORMULA_FUNCTION_TABLE;
1568
+ exports.FORMULA_LIMITS = FORMULA_LIMITS;
1569
+ exports.FORMULA_QUANTITY_VARIABLE = FORMULA_QUANTITY_VARIABLE;
1570
+ exports.FORMULA_SCOPE_VARIABLES = FORMULA_SCOPE_VARIABLES;
1571
+ exports.FORMULA_VARIABLES = FORMULA_VARIABLES;
1572
+ exports.FinishLevelSchema = FinishLevelSchema;
1573
+ exports.KitchenTypeSchema = KitchenTypeSchema;
1574
+ exports.LineStatusSchema = LineStatusSchema;
1575
+ exports.LocalizedTextSchema = LocalizedTextSchema;
1576
+ exports.OptionCodeSchema = OptionCodeSchema;
1577
+ exports.OptionKindSchema = OptionKindSchema;
1578
+ exports.OptionSelectionSchema = OptionSelectionSchema;
1579
+ exports.OptionUnitSchema = OptionUnitSchema;
1580
+ exports.PricingCategorySchema = PricingCategorySchema;
1581
+ exports.PricingDataSchema = PricingDataSchema;
1582
+ exports.PricingLimitsSchema = PricingLimitsSchema;
1583
+ exports.PricingMaterialTypeSchema = PricingMaterialTypeSchema;
1584
+ exports.PricingOptionSchema = PricingOptionSchema;
1585
+ exports.PricingPackageItemSchema = PricingPackageItemSchema;
1586
+ exports.PricingProductSchema = PricingProductSchema;
1587
+ exports.PricingRoomTypeSchema = PricingRoomTypeSchema;
1588
+ exports.PricingRuleSchema = PricingRuleSchema;
1589
+ exports.ProductUnitSchema = ProductUnitSchema;
1590
+ exports.RangeSchema = RangeSchema;
1591
+ exports.RateSourceSchema = RateSourceSchema;
1592
+ exports.RoomConfigurationSchema = RoomConfigurationSchema;
1593
+ exports.RoomSelectionSchema = RoomSelectionSchema;
1594
+ exports.RoomTypeCodeSchema = RoomTypeCodeSchema;
1595
+ exports.RuleLevelSchema = RuleLevelSchema;
1596
+ exports.RulePreviewSchema = RulePreviewSchema;
1597
+ exports.RuleSampleSchema = RuleSampleSchema;
1598
+ exports.SectionSchema = SectionSchema;
1599
+ exports.SectionTotalSchema = SectionTotalSchema;
1600
+ exports.SurfaceKindSchema = SurfaceKindSchema;
1601
+ exports.checkLimits = checkLimits;
1602
+ exports.convertCentsToKopecks = convertCentsToKopecks;
1603
+ exports.estimate = estimate;
1604
+ exports.evaluate = evaluate;
1605
+ exports.levenshtein = levenshtein;
1606
+ exports.objectVariables = objectVariables;
1607
+ exports.parse = parse;
1608
+ exports.previewRule = previewRule;
1609
+ exports.productVariables = productVariables;
1610
+ exports.productVariablesOf = productVariablesOf;
1611
+ exports.roomVariables = roomVariables;
1612
+ exports.suggestNearest = suggestNearest;
1613
+ exports.toCents = toCents;
1614
+ exports.validateFormula = validateFormula;