@roomwise/calc-engine 0.0.0-stage → 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.cjs ADDED
@@ -0,0 +1,1610 @@
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 = 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 RoomTypeCodeSchema = zod.z.enum([
795
+ "LIVING_ROOM",
796
+ "BEDROOM",
797
+ "KITCHEN",
798
+ "KITCHEN_LIVING",
799
+ "BATHROOM"
800
+ ]);
801
+ const KitchenTypeSchema = zod.z.enum(["SEPARATE", "KITCHEN_LIVING"]);
802
+ const CurrencySchema = zod.z.enum(["USD", "UAH"]);
803
+ const SectionSchema = zod.z.enum([
804
+ "BASE",
805
+ "OBJECT",
806
+ "MATERIALS",
807
+ "ENGINEERING",
808
+ "ADDITIONAL"
809
+ ]);
810
+ const LineStatusSchema = zod.z.enum([
811
+ "OK",
812
+ "INCLUDED",
813
+ "NOT_CALCULATED",
814
+ "UNAVAILABLE",
815
+ "ERROR"
816
+ ]);
817
+ const CentsSchema = zod.z.int().min(0);
818
+ const RangeSchema = zod.z.object({
819
+ min: zod.z.number(),
820
+ max: zod.z.number()
821
+ });
822
+ //#endregion
823
+ //#region src/schemas/configuration.ts
824
+ const RoomSelectionSchema = zod.z.object({
825
+ categoryId: zod.z.string(),
826
+ productId: zod.z.string().nullable()
827
+ });
828
+ const RoomConfigurationSchema = zod.z.object({
829
+ clientId: zod.z.string(),
830
+ roomTypeCode: RoomTypeCodeSchema,
831
+ lengthM: zod.z.number().positive(),
832
+ widthM: zod.z.number().positive(),
833
+ heightM: zod.z.number().positive(),
834
+ heatedFloor: zod.z.boolean(),
835
+ selections: zod.z.array(RoomSelectionSchema)
836
+ });
837
+ const OptionSelectionSchema = zod.z.object({
838
+ optionId: zod.z.string(),
839
+ quantity: zod.z.int().positive().optional(),
840
+ roomClientIds: zod.z.array(zod.z.string()).optional()
841
+ });
842
+ function duplicateIndexes(items, key) {
843
+ const seen = /* @__PURE__ */ new Set();
844
+ return items.flatMap((item, index) => {
845
+ const value = key(item);
846
+ if (seen.has(value)) return [index];
847
+ seen.add(value);
848
+ return [];
849
+ });
850
+ }
851
+ const ConfigurationSchema = zod.z.object({
852
+ finishLevel: FinishLevelSchema,
853
+ totalAreaSqm: zod.z.number().positive(),
854
+ balconyAreaSqm: zod.z.number().min(0),
855
+ balconyTiles: zod.z.boolean(),
856
+ gas: zod.z.boolean(),
857
+ kitchenType: KitchenTypeSchema,
858
+ rooms: zod.z.array(RoomConfigurationSchema),
859
+ options: zod.z.array(OptionSelectionSchema),
860
+ currency: CurrencySchema
861
+ }).superRefine((configuration, ctx) => {
862
+ const addDuplicate = (path) => ctx.addIssue({
863
+ code: "custom",
864
+ path,
865
+ message: "Duplicate"
866
+ });
867
+ for (const index of duplicateIndexes(configuration.rooms, (room) => room.clientId)) addDuplicate([
868
+ "rooms",
869
+ index,
870
+ "clientId"
871
+ ]);
872
+ configuration.rooms.forEach((room, roomIndex) => {
873
+ for (const index of duplicateIndexes(room.selections, (selection) => selection.categoryId)) addDuplicate([
874
+ "rooms",
875
+ roomIndex,
876
+ "selections",
877
+ index,
878
+ "categoryId"
879
+ ]);
880
+ });
881
+ for (const index of duplicateIndexes(configuration.options, (option) => option.optionId)) addDuplicate([
882
+ "options",
883
+ index,
884
+ "optionId"
885
+ ]);
886
+ });
887
+ //#endregion
888
+ //#region src/schemas/pricing-data.ts
889
+ const ExchangeRateSchema = zod.z.object({
890
+ rate: zod.z.string().regex(/^\d+\.\d{4}$/),
891
+ date: zod.z.string(),
892
+ source: RateSourceSchema
893
+ });
894
+ const PricingLimitsSchema = zod.z.object({
895
+ totalAreaSqm: RangeSchema,
896
+ rooms: RangeSchema,
897
+ roomSideM: RangeSchema,
898
+ roomHeightM: RangeSchema
899
+ });
900
+ const BaseRateSchema = zod.z.object({
901
+ finishLevel: FinishLevelSchema,
902
+ areaFromSqm: zod.z.number().min(0),
903
+ areaToSqm: zod.z.number(),
904
+ rateCentsPerSqm: zod.z.int().positive()
905
+ });
906
+ const PricingRuleSchema = zod.z.object({
907
+ id: zod.z.string(),
908
+ level: RuleLevelSchema,
909
+ categoryId: zod.z.string().nullable(),
910
+ materialTypeId: zod.z.string().nullable(),
911
+ name: LocalizedTextSchema,
912
+ quantityFormula: zod.z.string().nullable(),
913
+ costFormula: zod.z.string().max(500),
914
+ sortOrder: zod.z.int()
915
+ }).refine((rule) => rule.level === "OBJECT" === (rule.quantityFormula === null), { path: ["quantityFormula"] });
916
+ const PricingRoomTypeSchema = zod.z.object({
917
+ id: zod.z.string(),
918
+ code: zod.z.string()
919
+ });
920
+ const PricingCategorySchema = zod.z.object({
921
+ id: zod.z.string(),
922
+ wastePercent: zod.z.number().min(0),
923
+ surface: SurfaceKindSchema
924
+ });
925
+ const PricingMaterialTypeSchema = zod.z.object({
926
+ id: zod.z.string(),
927
+ code: zod.z.string()
928
+ });
929
+ const PricingProductSchema = zod.z.object({
930
+ id: zod.z.string(),
931
+ categoryId: zod.z.string(),
932
+ materialTypeId: zod.z.string(),
933
+ priceCents: CentsSchema,
934
+ unit: ProductUnitSchema,
935
+ wastePercentOverride: zod.z.number().min(0).nullable(),
936
+ heatedFloorCompatible: zod.z.boolean(),
937
+ available: zod.z.boolean()
938
+ });
939
+ const PricingPackageItemSchema = zod.z.object({
940
+ id: zod.z.string(),
941
+ includedInBase: zod.z.boolean(),
942
+ priceCents: CentsSchema.nullable(),
943
+ unit: OptionUnitSchema.nullable()
944
+ });
945
+ const PricingOptionSchema = zod.z.object({
946
+ id: zod.z.string(),
947
+ kind: OptionKindSchema,
948
+ priceCents: CentsSchema,
949
+ unit: OptionUnitSchema,
950
+ roomTypeCodes: zod.z.array(zod.z.string()),
951
+ minQuantity: zod.z.int().nullable(),
952
+ maxQuantity: zod.z.int().nullable(),
953
+ available: zod.z.boolean()
954
+ });
955
+ const PricingDataSchema = zod.z.object({
956
+ version: zod.z.string().regex(/^[0-9a-f]{16}$/),
957
+ exchangeRate: ExchangeRateSchema.nullable(),
958
+ limits: PricingLimitsSchema,
959
+ baseRates: zod.z.array(BaseRateSchema),
960
+ coefficients: zod.z.record(zod.z.string(), zod.z.number()),
961
+ rules: zod.z.array(PricingRuleSchema),
962
+ roomTypes: zod.z.array(PricingRoomTypeSchema),
963
+ categories: zod.z.array(PricingCategorySchema),
964
+ materialTypes: zod.z.array(PricingMaterialTypeSchema),
965
+ products: zod.z.array(PricingProductSchema),
966
+ packageItems: zod.z.array(PricingPackageItemSchema),
967
+ options: zod.z.array(PricingOptionSchema)
968
+ });
969
+ //#endregion
970
+ //#region src/schemas/estimate.ts
971
+ const EstimateMessageSchema = zod.z.object({
972
+ code: zod.z.string(),
973
+ params: zod.z.record(zod.z.string(), zod.z.unknown()).optional()
974
+ });
975
+ const EstimateLineSchema = zod.z.object({
976
+ id: zod.z.string(),
977
+ section: SectionSchema,
978
+ status: LineStatusSchema,
979
+ roomClientId: zod.z.string().nullable(),
980
+ categoryId: zod.z.string().nullable(),
981
+ productId: zod.z.string().nullable(),
982
+ optionId: zod.z.string().nullable(),
983
+ packageItemId: zod.z.string().nullable(),
984
+ ruleId: zod.z.string().nullable(),
985
+ quantity: zod.z.number().nullable(),
986
+ unit: zod.z.string().nullable(),
987
+ unitPriceCents: CentsSchema.nullable(),
988
+ amountCents: CentsSchema,
989
+ amountUahKopecks: CentsSchema.nullable(),
990
+ warnings: zod.z.array(EstimateMessageSchema),
991
+ error: EstimateMessageSchema.nullable()
992
+ });
993
+ const SectionTotalSchema = zod.z.object({
994
+ usdCents: CentsSchema,
995
+ uahKopecks: CentsSchema.nullable()
996
+ });
997
+ const EstimateTotalsSchema = zod.z.object({
998
+ usdCents: CentsSchema,
999
+ uahKopecks: CentsSchema.nullable(),
1000
+ sections: zod.z.record(SectionSchema, SectionTotalSchema)
1001
+ });
1002
+ const EstimateWarningSchema = EstimateMessageSchema.extend({ lineId: zod.z.string().optional() });
1003
+ const EstimateSchema = zod.z.object({
1004
+ pricingVersion: zod.z.string(),
1005
+ currency: CurrencySchema,
1006
+ exchangeRate: ExchangeRateSchema.nullable(),
1007
+ lines: zod.z.array(EstimateLineSchema),
1008
+ totals: EstimateTotalsSchema,
1009
+ warnings: zod.z.array(EstimateWarningSchema),
1010
+ incomplete: zod.z.boolean()
1011
+ });
1012
+ //#endregion
1013
+ //#region src/schemas/preview.ts
1014
+ const RuleSampleSchema = zod.z.object({
1015
+ room: zod.z.object({
1016
+ roomTypeCode: RoomTypeCodeSchema,
1017
+ lengthM: zod.z.number().positive(),
1018
+ widthM: zod.z.number().positive(),
1019
+ heightM: zod.z.number().positive(),
1020
+ heatedFloor: zod.z.boolean()
1021
+ }).optional(),
1022
+ product: zod.z.object({
1023
+ priceCents: CentsSchema,
1024
+ unit: ProductUnitSchema,
1025
+ wastePercent: zod.z.number().min(0)
1026
+ }).optional(),
1027
+ object: zod.z.object({
1028
+ totalAreaSqm: zod.z.number().positive(),
1029
+ balconyAreaSqm: zod.z.number().min(0),
1030
+ balconyTiles: zod.z.boolean(),
1031
+ gas: zod.z.boolean(),
1032
+ kitchenType: KitchenTypeSchema,
1033
+ finishLevel: FinishLevelSchema,
1034
+ roomsCount: zod.z.int().min(0),
1035
+ bathroomsCount: zod.z.int().min(0)
1036
+ })
1037
+ });
1038
+ const RulePreviewSchema = zod.z.object({
1039
+ quantity: zod.z.number().nullable(),
1040
+ amountCents: CentsSchema.nullable(),
1041
+ variables: zod.z.record(zod.z.string(), zod.z.union([
1042
+ zod.z.number(),
1043
+ zod.z.boolean(),
1044
+ zod.z.string()
1045
+ ])),
1046
+ coefficients: zod.z.record(zod.z.string(), zod.z.number()),
1047
+ error: EstimateMessageSchema.nullable()
1048
+ });
1049
+ //#endregion
1050
+ //#region src/estimate/totals.ts
1051
+ function sumTotal(lines, hasRate) {
1052
+ return {
1053
+ usdCents: lines.reduce((sum, line) => sum + line.amountCents, 0),
1054
+ uahKopecks: hasRate ? lines.reduce((sum, line) => sum + (line.amountUahKopecks ?? 0), 0) : null
1055
+ };
1056
+ }
1057
+ function computeTotals(lines, hasRate) {
1058
+ const sections = Object.fromEntries(SectionSchema.options.map((section) => [section, sumTotal(lines.filter((line) => line.section === section), hasRate)]));
1059
+ return {
1060
+ ...sumTotal(lines, hasRate),
1061
+ sections
1062
+ };
1063
+ }
1064
+ function collectWarnings(lines) {
1065
+ return lines.flatMap((line) => line.warnings.map((warning) => ({
1066
+ ...warning,
1067
+ lineId: line.id
1068
+ })));
1069
+ }
1070
+ function isIncomplete(lines) {
1071
+ return lines.some((line) => line.status === "ERROR" || line.status === "NOT_CALCULATED");
1072
+ }
1073
+ //#endregion
1074
+ //#region src/estimate/warnings.ts
1075
+ function roomsAreaWarnings(configuration) {
1076
+ const roomsAreaSqm = Math.round(configuration.rooms.reduce((sum, room) => sum + roomVariables(room).floorArea, 0) * 1e4) / 1e4;
1077
+ return roomsAreaSqm > configuration.totalAreaSqm ? [{
1078
+ code: "ROOMS_AREA_EXCEEDS_TOTAL",
1079
+ params: {
1080
+ roomsAreaSqm,
1081
+ totalAreaSqm: configuration.totalAreaSqm
1082
+ }
1083
+ }] : [];
1084
+ }
1085
+ function estimateWarnings(lines, configuration, rate) {
1086
+ return [
1087
+ ...collectWarnings(lines),
1088
+ ...roomsAreaWarnings(configuration),
1089
+ ...configuration.currency === "UAH" && rate === null ? [{ code: "EXCHANGE_RATE_UNAVAILABLE" }] : []
1090
+ ];
1091
+ }
1092
+ //#endregion
1093
+ //#region src/estimate/estimate.ts
1094
+ function withKopecks(line, rate) {
1095
+ return {
1096
+ ...line,
1097
+ amountUahKopecks: rate === null ? null : convertCentsToKopecks(line.amountCents, rate)
1098
+ };
1099
+ }
1100
+ function estimate(configuration, data) {
1101
+ const rate = data.exchangeRate?.rate ?? null;
1102
+ const lines = [
1103
+ baseLine(configuration, data),
1104
+ ...objectRuleLines(configuration, data),
1105
+ ...materialLines(configuration, data),
1106
+ ...engineeringLines(configuration, data),
1107
+ ...additionalLines(configuration, data)
1108
+ ].map((line) => withKopecks(line, rate));
1109
+ return {
1110
+ pricingVersion: data.version,
1111
+ currency: configuration.currency,
1112
+ exchangeRate: data.exchangeRate,
1113
+ lines,
1114
+ totals: computeTotals(lines, rate !== null),
1115
+ warnings: estimateWarnings(lines, configuration, rate),
1116
+ incomplete: isIncomplete(lines)
1117
+ };
1118
+ }
1119
+ //#endregion
1120
+ //#region src/estimate/limits.ts
1121
+ function checkLimits(configuration, limits, options = []) {
1122
+ const violations = [];
1123
+ const check = (field, value, min, max) => {
1124
+ if (value < min || value > max) violations.push({
1125
+ field,
1126
+ min,
1127
+ max
1128
+ });
1129
+ };
1130
+ check("totalAreaSqm", configuration.totalAreaSqm, limits.totalAreaSqm.min, limits.totalAreaSqm.max);
1131
+ check("balconyAreaSqm", configuration.balconyAreaSqm, 0, configuration.totalAreaSqm);
1132
+ check("rooms", configuration.rooms.length, limits.rooms.min, limits.rooms.max);
1133
+ configuration.rooms.forEach((room, index) => {
1134
+ const path = `rooms[${index}]`;
1135
+ check(`${path}.lengthM`, room.lengthM, limits.roomSideM.min, limits.roomSideM.max);
1136
+ check(`${path}.widthM`, room.widthM, limits.roomSideM.min, limits.roomSideM.max);
1137
+ check(`${path}.heightM`, room.heightM, limits.roomHeightM.min, limits.roomHeightM.max);
1138
+ });
1139
+ configuration.options.forEach((selection, index) => {
1140
+ const option = options.find((candidate) => candidate.id === selection.optionId);
1141
+ if (option?.unit !== "PIECE" || selection.quantity === void 0) return;
1142
+ check(`options[${index}].quantity`, selection.quantity, option.minQuantity ?? 1, option.maxQuantity ?? Number.MAX_SAFE_INTEGER);
1143
+ });
1144
+ return violations;
1145
+ }
1146
+ //#endregion
1147
+ //#region src/formula/preview.ts
1148
+ function collectReferences(node, found) {
1149
+ switch (node.type) {
1150
+ case "Identifier":
1151
+ found.variables.add(node.name);
1152
+ return;
1153
+ case "Member":
1154
+ if (node.object.type === "Identifier" && node.object.name === "coef") found.coefficients.add(node.property.name);
1155
+ return;
1156
+ case "Call":
1157
+ node.args.forEach((argument) => collectReferences(argument, found));
1158
+ return;
1159
+ case "Unary":
1160
+ collectReferences(node.argument, found);
1161
+ return;
1162
+ case "Binary":
1163
+ collectReferences(node.left, found);
1164
+ collectReferences(node.right, found);
1165
+ return;
1166
+ case "Conditional":
1167
+ collectReferences(node.test, found);
1168
+ collectReferences(node.consequent, found);
1169
+ collectReferences(node.alternate, found);
1170
+ return;
1171
+ case "Literal": return;
1172
+ }
1173
+ }
1174
+ function referencesOf(sources) {
1175
+ const found = {
1176
+ variables: /* @__PURE__ */ new Set(),
1177
+ coefficients: /* @__PURE__ */ new Set()
1178
+ };
1179
+ for (const source of sources) {
1180
+ const parsed = source === null ? void 0 : parse(source);
1181
+ if (parsed?.ok) collectReferences(parsed.ast, found);
1182
+ }
1183
+ return found;
1184
+ }
1185
+ function objectScope({ object }) {
1186
+ return {
1187
+ totalArea: object.totalAreaSqm,
1188
+ balconyArea: object.balconyAreaSqm,
1189
+ balconyTiles: object.balconyTiles,
1190
+ gas: object.gas,
1191
+ kitchenType: object.kitchenType,
1192
+ finishLevel: object.finishLevel,
1193
+ roomsCount: object.roomsCount,
1194
+ bathroomsCount: object.bathroomsCount
1195
+ };
1196
+ }
1197
+ function scopeOf(level, sample) {
1198
+ if (level === "OBJECT") return { scope: objectScope(sample) };
1199
+ const { room, product } = sample;
1200
+ if (room === void 0 || product === void 0) return { error: {
1201
+ code: "SAMPLE_INCOMPLETE",
1202
+ params: { missing: room === void 0 ? "room" : "product" }
1203
+ } };
1204
+ return { scope: {
1205
+ ...roomVariables(room),
1206
+ ...productVariablesOf(product, product.wastePercent),
1207
+ ...objectScope(sample)
1208
+ } };
1209
+ }
1210
+ function pick(source, names) {
1211
+ return Object.fromEntries(Object.entries(source).filter(([name]) => names.has(name)));
1212
+ }
1213
+ function previewRule(input) {
1214
+ const { level, quantityFormula, costFormula, coefficients } = input;
1215
+ const references = referencesOf([quantityFormula, costFormula]);
1216
+ const report = (variables, quantity, amountCents, error) => ({
1217
+ quantity,
1218
+ amountCents,
1219
+ variables: pick(variables, references.variables),
1220
+ coefficients: pick(coefficients, references.coefficients),
1221
+ error
1222
+ });
1223
+ if (level === "OBJECT" !== (quantityFormula === null)) return report({}, null, null, { code: "QUANTITY_FORMULA_MISMATCH" });
1224
+ const prepared = scopeOf(level, input.sample);
1225
+ if ("error" in prepared) return report({}, null, null, prepared.error);
1226
+ let variables = prepared.scope;
1227
+ let quantity = null;
1228
+ if (quantityFormula !== null) {
1229
+ const raw = runNumericFormula(quantityFormula, {
1230
+ variables,
1231
+ coefficients
1232
+ });
1233
+ if (!raw.ok) return report(variables, null, null, raw.error);
1234
+ variables = {
1235
+ ...variables,
1236
+ quantity: raw.value
1237
+ };
1238
+ quantity = roundQuantity(raw.value);
1239
+ }
1240
+ const cost = runNumericFormula(costFormula, {
1241
+ variables,
1242
+ coefficients
1243
+ });
1244
+ if (!cost.ok) return report(variables, quantity, null, cost.error);
1245
+ const line = pricedLine({
1246
+ id: "preview",
1247
+ section: "OBJECT"
1248
+ }, cost.value);
1249
+ return line.error === null ? report(variables, quantity, line.amountCents, null) : report(variables, quantity, null, line.error);
1250
+ }
1251
+ //#endregion
1252
+ //#region src/formula/suggest.ts
1253
+ function levenshtein(a, b) {
1254
+ let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
1255
+ for (let i = 1; i <= a.length; i++) {
1256
+ const current = [i];
1257
+ for (let j = 1; j <= b.length; j++) {
1258
+ const substitution = a[i - 1] === b[j - 1] ? 0 : 1;
1259
+ current[j] = Math.min(previous[j] + 1, current[j - 1] + 1, previous[j - 1] + substitution);
1260
+ }
1261
+ previous = current;
1262
+ }
1263
+ return previous[b.length];
1264
+ }
1265
+ function suggestNearest(name, candidates, maxDistance = 2) {
1266
+ let best;
1267
+ let bestDistance = maxDistance + 1;
1268
+ for (const candidate of candidates) {
1269
+ const distance = levenshtein(name, candidate);
1270
+ if (distance < bestDistance || distance === bestDistance && best !== void 0 && candidate < best) {
1271
+ best = candidate;
1272
+ bestDistance = distance;
1273
+ }
1274
+ }
1275
+ return best;
1276
+ }
1277
+ //#endregion
1278
+ //#region src/formula/validate.ts
1279
+ const NUMBER = { kind: "number" };
1280
+ const BOOLEAN = { kind: "boolean" };
1281
+ const STRING = { kind: "string" };
1282
+ const UNKNOWN = { kind: "unknown" };
1283
+ function typeName(type) {
1284
+ return type.kind === "enum" ? type.name : type.kind;
1285
+ }
1286
+ const number = (name) => ({
1287
+ name,
1288
+ kind: "number"
1289
+ });
1290
+ const boolean = (name) => ({
1291
+ name,
1292
+ kind: "boolean"
1293
+ });
1294
+ const enumeration = (name, values) => ({
1295
+ name,
1296
+ kind: "enum",
1297
+ values
1298
+ });
1299
+ const FORMULA_SCOPE_VARIABLES = {
1300
+ room: [
1301
+ number("length"),
1302
+ number("width"),
1303
+ number("height"),
1304
+ number("floorArea"),
1305
+ number("perimeter"),
1306
+ number("wallArea"),
1307
+ boolean("heatedFloor"),
1308
+ enumeration("roomType", RoomTypeCodeSchema.options)
1309
+ ],
1310
+ product: [
1311
+ number("price"),
1312
+ number("waste"),
1313
+ enumeration("unit", ProductUnitSchema.options)
1314
+ ],
1315
+ object: [
1316
+ number("totalArea"),
1317
+ number("balconyArea"),
1318
+ boolean("balconyTiles"),
1319
+ boolean("gas"),
1320
+ enumeration("kitchenType", KitchenTypeSchema.options),
1321
+ enumeration("finishLevel", FinishLevelSchema.options),
1322
+ number("roomsCount"),
1323
+ number("bathroomsCount")
1324
+ ]
1325
+ };
1326
+ const FORMULA_QUANTITY_VARIABLE = number("quantity");
1327
+ const FORMULA_VARIABLES = {
1328
+ OBJECT: FORMULA_SCOPE_VARIABLES.object,
1329
+ CATEGORY: [
1330
+ ...FORMULA_SCOPE_VARIABLES.room,
1331
+ ...FORMULA_SCOPE_VARIABLES.product,
1332
+ ...FORMULA_SCOPE_VARIABLES.object
1333
+ ],
1334
+ MATERIAL_TYPE: [
1335
+ ...FORMULA_SCOPE_VARIABLES.room,
1336
+ ...FORMULA_SCOPE_VARIABLES.product,
1337
+ ...FORMULA_SCOPE_VARIABLES.object
1338
+ ]
1339
+ };
1340
+ const ALL_VARIABLE_NAMES = new Set(FORMULA_VARIABLES.CATEGORY.map((variable) => variable.name));
1341
+ function span(node) {
1342
+ return {
1343
+ position: node.start,
1344
+ length: node.end - node.start
1345
+ };
1346
+ }
1347
+ function validateFormula(input) {
1348
+ const parsed = parse(input.source);
1349
+ if (!parsed.ok) return {
1350
+ valid: false,
1351
+ coefficientKeys: [],
1352
+ errors: [parsed.error]
1353
+ };
1354
+ const variables = FORMULA_VARIABLES[input.level];
1355
+ const variableTypes = new Map(variables.map((variable) => [variable.name, variable.kind === "enum" ? {
1356
+ kind: "enum",
1357
+ name: variable.name,
1358
+ values: variable.values ?? []
1359
+ } : { kind: variable.kind }]));
1360
+ const allowedNames = variables.map((variable) => variable.name);
1361
+ const knownCoefficients = new Set(input.coefficientKeys);
1362
+ const usedCoefficients = /* @__PURE__ */ new Set();
1363
+ const errors = [];
1364
+ const quantityAllowed = input.kind === "cost" && input.level !== "OBJECT";
1365
+ function mismatch(node, expected) {
1366
+ errors.push({
1367
+ code: "FORMULA_SYNTAX",
1368
+ ...span(node),
1369
+ params: {
1370
+ reason: "TYPE_MISMATCH",
1371
+ expected
1372
+ }
1373
+ });
1374
+ }
1375
+ function expectType(node, expected) {
1376
+ const type = infer(node);
1377
+ if (type.kind === "unknown") return false;
1378
+ if (type.kind !== expected) {
1379
+ mismatch(node, expected);
1380
+ return false;
1381
+ }
1382
+ return true;
1383
+ }
1384
+ function identifier(node) {
1385
+ const { name } = node;
1386
+ if (name === FORMULA_QUANTITY_VARIABLE.name) {
1387
+ if (!quantityAllowed) {
1388
+ errors.push({
1389
+ code: "QUANTITY_NOT_ALLOWED",
1390
+ ...span(node)
1391
+ });
1392
+ return UNKNOWN;
1393
+ }
1394
+ return NUMBER;
1395
+ }
1396
+ const type = variableTypes.get(name);
1397
+ if (type !== void 0) return type;
1398
+ if (ALL_VARIABLE_NAMES.has(name)) {
1399
+ errors.push({
1400
+ code: "VARIABLE_NOT_ALLOWED_AT_LEVEL",
1401
+ ...span(node),
1402
+ params: {
1403
+ variable: name,
1404
+ level: input.level
1405
+ }
1406
+ });
1407
+ return UNKNOWN;
1408
+ }
1409
+ const suggestion = suggestNearest(name, allowedNames);
1410
+ errors.push({
1411
+ code: "UNKNOWN_VARIABLE",
1412
+ ...span(node),
1413
+ params: suggestion === void 0 ? { name } : {
1414
+ name,
1415
+ suggestion
1416
+ }
1417
+ });
1418
+ return UNKNOWN;
1419
+ }
1420
+ function member(node) {
1421
+ const { object, property } = node;
1422
+ if (object.type !== "Identifier" || object.name !== "coef") {
1423
+ errors.push({
1424
+ code: "UNKNOWN_VARIABLE",
1425
+ ...span(node),
1426
+ params: { name: input.source.slice(node.start, node.end) }
1427
+ });
1428
+ return UNKNOWN;
1429
+ }
1430
+ usedCoefficients.add(property.name);
1431
+ if (!knownCoefficients.has(property.name)) {
1432
+ errors.push({
1433
+ code: "UNKNOWN_COEFFICIENT",
1434
+ ...span(node),
1435
+ params: { key: property.name }
1436
+ });
1437
+ return UNKNOWN;
1438
+ }
1439
+ return NUMBER;
1440
+ }
1441
+ function call(node) {
1442
+ const { callee } = node;
1443
+ const fn = callee.type === "Identifier" ? FORMULA_FUNCTION_TABLE.get(callee.name) : void 0;
1444
+ if (fn === void 0) {
1445
+ errors.push({
1446
+ code: "UNKNOWN_FUNCTION",
1447
+ ...span(callee),
1448
+ params: { name: input.source.slice(callee.start, callee.end) }
1449
+ });
1450
+ for (const argument of node.args) infer(argument);
1451
+ return UNKNOWN;
1452
+ }
1453
+ let sound = true;
1454
+ if (node.args.length < fn.minArgs || node.args.length > fn.maxArgs) {
1455
+ errors.push({
1456
+ code: "FORMULA_SYNTAX",
1457
+ ...span(node),
1458
+ params: {
1459
+ reason: "ARGUMENT_COUNT",
1460
+ min: fn.minArgs,
1461
+ ...Number.isFinite(fn.maxArgs) ? { max: fn.maxArgs } : {}
1462
+ }
1463
+ });
1464
+ sound = false;
1465
+ }
1466
+ for (const argument of node.args) sound = expectType(argument, "number") && sound;
1467
+ return sound ? NUMBER : UNKNOWN;
1468
+ }
1469
+ function enumLiteral(literal, other) {
1470
+ const type = infer(other);
1471
+ if (type.kind === "unknown") return;
1472
+ if (other.type !== "Identifier" || type.kind !== "enum") {
1473
+ mismatch(other, "enum variable");
1474
+ return;
1475
+ }
1476
+ if (!type.values.includes(literal.value)) errors.push({
1477
+ code: "FORMULA_SYNTAX",
1478
+ ...span(literal),
1479
+ params: {
1480
+ reason: "UNKNOWN_ENUM_VALUE",
1481
+ value: literal.value,
1482
+ variable: type.name
1483
+ }
1484
+ });
1485
+ }
1486
+ function equality(node) {
1487
+ const { left, right } = node;
1488
+ const literal = [left, right].find((side) => side.type === "Literal" && typeof side.value === "string");
1489
+ if (literal !== void 0) {
1490
+ enumLiteral(literal, literal === left ? right : left);
1491
+ return BOOLEAN;
1492
+ }
1493
+ const a = infer(left);
1494
+ const b = infer(right);
1495
+ if (a.kind !== "unknown" && b.kind !== "unknown" && a.kind !== b.kind) mismatch(right, typeName(a));
1496
+ return BOOLEAN;
1497
+ }
1498
+ function binary(node) {
1499
+ const { operator, left, right } = node;
1500
+ if (operator === "==" || operator === "!=") return equality(node);
1501
+ const logical = operator === "&&" || operator === "||";
1502
+ const operand = logical ? "boolean" : "number";
1503
+ const leftSound = expectType(left, operand);
1504
+ const rightSound = expectType(right, operand);
1505
+ if (!(leftSound && rightSound)) return UNKNOWN;
1506
+ const arithmetic = [
1507
+ "+",
1508
+ "-",
1509
+ "*",
1510
+ "/"
1511
+ ].includes(operator);
1512
+ return logical || !arithmetic ? BOOLEAN : NUMBER;
1513
+ }
1514
+ function unary(node) {
1515
+ const operand = node.operator === "!" ? "boolean" : "number";
1516
+ return expectType(node.argument, operand) ? { kind: operand } : UNKNOWN;
1517
+ }
1518
+ function conditional(node) {
1519
+ const testSound = expectType(node.test, "boolean");
1520
+ const consequent = infer(node.consequent);
1521
+ const alternate = infer(node.alternate);
1522
+ if (consequent.kind === "unknown") return alternate;
1523
+ if (alternate.kind === "unknown") return consequent;
1524
+ if (consequent.kind !== alternate.kind) {
1525
+ mismatch(node.alternate, typeName(consequent));
1526
+ return UNKNOWN;
1527
+ }
1528
+ return testSound ? consequent : UNKNOWN;
1529
+ }
1530
+ function infer(node) {
1531
+ switch (node.type) {
1532
+ case "Literal":
1533
+ if (typeof node.value === "string") return STRING;
1534
+ return typeof node.value === "number" ? NUMBER : BOOLEAN;
1535
+ case "Identifier": return identifier(node);
1536
+ case "Member": return member(node);
1537
+ case "Call": return call(node);
1538
+ case "Unary": return unary(node);
1539
+ case "Binary": return binary(node);
1540
+ case "Conditional": return conditional(node);
1541
+ }
1542
+ }
1543
+ const resultType = infer(parsed.ast);
1544
+ if (resultType.kind !== "unknown" && resultType.kind !== "number") mismatch(parsed.ast, "number");
1545
+ return {
1546
+ valid: errors.length === 0,
1547
+ coefficientKeys: [...usedCoefficients],
1548
+ errors
1549
+ };
1550
+ }
1551
+ //#endregion
1552
+ exports.BaseRateSchema = BaseRateSchema;
1553
+ exports.COEFFICIENT_NAMESPACE = COEFFICIENT_NAMESPACE;
1554
+ exports.CentsSchema = CentsSchema;
1555
+ exports.ConfigurationSchema = ConfigurationSchema;
1556
+ exports.CurrencySchema = CurrencySchema;
1557
+ exports.EstimateLineSchema = EstimateLineSchema;
1558
+ exports.EstimateMessageSchema = EstimateMessageSchema;
1559
+ exports.EstimateSchema = EstimateSchema;
1560
+ exports.EstimateTotalsSchema = EstimateTotalsSchema;
1561
+ exports.EstimateWarningSchema = EstimateWarningSchema;
1562
+ exports.ExchangeRateSchema = ExchangeRateSchema;
1563
+ exports.FORMULA_FUNCTIONS = FORMULA_FUNCTIONS;
1564
+ exports.FORMULA_FUNCTION_TABLE = FORMULA_FUNCTION_TABLE;
1565
+ exports.FORMULA_LIMITS = FORMULA_LIMITS;
1566
+ exports.FORMULA_QUANTITY_VARIABLE = FORMULA_QUANTITY_VARIABLE;
1567
+ exports.FORMULA_SCOPE_VARIABLES = FORMULA_SCOPE_VARIABLES;
1568
+ exports.FORMULA_VARIABLES = FORMULA_VARIABLES;
1569
+ exports.FinishLevelSchema = FinishLevelSchema;
1570
+ exports.KitchenTypeSchema = KitchenTypeSchema;
1571
+ exports.LineStatusSchema = LineStatusSchema;
1572
+ exports.LocalizedTextSchema = LocalizedTextSchema;
1573
+ exports.OptionKindSchema = OptionKindSchema;
1574
+ exports.OptionSelectionSchema = OptionSelectionSchema;
1575
+ exports.OptionUnitSchema = OptionUnitSchema;
1576
+ exports.PricingCategorySchema = PricingCategorySchema;
1577
+ exports.PricingDataSchema = PricingDataSchema;
1578
+ exports.PricingLimitsSchema = PricingLimitsSchema;
1579
+ exports.PricingMaterialTypeSchema = PricingMaterialTypeSchema;
1580
+ exports.PricingOptionSchema = PricingOptionSchema;
1581
+ exports.PricingPackageItemSchema = PricingPackageItemSchema;
1582
+ exports.PricingProductSchema = PricingProductSchema;
1583
+ exports.PricingRoomTypeSchema = PricingRoomTypeSchema;
1584
+ exports.PricingRuleSchema = PricingRuleSchema;
1585
+ exports.ProductUnitSchema = ProductUnitSchema;
1586
+ exports.RangeSchema = RangeSchema;
1587
+ exports.RateSourceSchema = RateSourceSchema;
1588
+ exports.RoomConfigurationSchema = RoomConfigurationSchema;
1589
+ exports.RoomSelectionSchema = RoomSelectionSchema;
1590
+ exports.RoomTypeCodeSchema = RoomTypeCodeSchema;
1591
+ exports.RuleLevelSchema = RuleLevelSchema;
1592
+ exports.RulePreviewSchema = RulePreviewSchema;
1593
+ exports.RuleSampleSchema = RuleSampleSchema;
1594
+ exports.SectionSchema = SectionSchema;
1595
+ exports.SectionTotalSchema = SectionTotalSchema;
1596
+ exports.SurfaceKindSchema = SurfaceKindSchema;
1597
+ exports.checkLimits = checkLimits;
1598
+ exports.convertCentsToKopecks = convertCentsToKopecks;
1599
+ exports.estimate = estimate;
1600
+ exports.evaluate = evaluate;
1601
+ exports.levenshtein = levenshtein;
1602
+ exports.objectVariables = objectVariables;
1603
+ exports.parse = parse;
1604
+ exports.previewRule = previewRule;
1605
+ exports.productVariables = productVariables;
1606
+ exports.productVariablesOf = productVariablesOf;
1607
+ exports.roomVariables = roomVariables;
1608
+ exports.suggestNearest = suggestNearest;
1609
+ exports.toCents = toCents;
1610
+ exports.validateFormula = validateFormula;