@roomwise/calc-engine 0.4.0 → 0.5.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/README.md +67 -23
- package/dist/index.cjs +366 -129
- package/dist/index.d.cts +110 -25
- package/dist/index.d.ts +110 -25
- package/dist/index.js +362 -129
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -26,6 +26,66 @@ function toCents(dollars) {
|
|
|
26
26
|
return cents;
|
|
27
27
|
}
|
|
28
28
|
//#endregion
|
|
29
|
+
//#region src/variables/room.ts
|
|
30
|
+
function roomVariables(room) {
|
|
31
|
+
const perimeter = 2 * (room.lengthM + room.widthM);
|
|
32
|
+
return {
|
|
33
|
+
length: room.lengthM,
|
|
34
|
+
width: room.widthM,
|
|
35
|
+
height: room.heightM,
|
|
36
|
+
floorArea: room.lengthM * room.widthM,
|
|
37
|
+
perimeter,
|
|
38
|
+
wallArea: perimeter * room.heightM,
|
|
39
|
+
heatedFloor: room.heatedFloor,
|
|
40
|
+
roomType: room.roomTypeCode
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/variables/object.ts
|
|
45
|
+
const COUNTED_AS_ROOMS = [
|
|
46
|
+
"LIVING_ROOM",
|
|
47
|
+
"BEDROOM",
|
|
48
|
+
"KITCHEN_LIVING"
|
|
49
|
+
];
|
|
50
|
+
const WET_ROOMS = [
|
|
51
|
+
"BATHROOM",
|
|
52
|
+
"KITCHEN",
|
|
53
|
+
"KITCHEN_LIVING"
|
|
54
|
+
];
|
|
55
|
+
function totalAreaOf(scope, totalAreaSqm, roomsFloorArea) {
|
|
56
|
+
return scope === "PARTIAL" || totalAreaSqm === null ? roomsFloorArea : totalAreaSqm;
|
|
57
|
+
}
|
|
58
|
+
function objectVariables(configuration) {
|
|
59
|
+
const rooms = configuration.rooms.map(roomVariables);
|
|
60
|
+
const sum = (pick) => rooms.reduce((total, room) => total + pick(room), 0);
|
|
61
|
+
const roomsFloorArea = sum((room) => room.floorArea);
|
|
62
|
+
return {
|
|
63
|
+
scope: configuration.scope,
|
|
64
|
+
totalArea: totalAreaOf(configuration.scope, configuration.totalAreaSqm, roomsFloorArea),
|
|
65
|
+
balconyArea: configuration.balconyAreaSqm,
|
|
66
|
+
balconyTiles: configuration.balconyTiles,
|
|
67
|
+
finishLevel: configuration.finishLevel,
|
|
68
|
+
roomsCount: configuration.rooms.filter((room) => COUNTED_AS_ROOMS.includes(room.roomTypeCode)).length,
|
|
69
|
+
bathroomsCount: configuration.rooms.filter((room) => room.roomTypeCode === "BATHROOM").length,
|
|
70
|
+
roomsFloorArea,
|
|
71
|
+
roomsWallArea: sum((room) => room.wallArea),
|
|
72
|
+
roomsPerimeter: sum((room) => room.perimeter),
|
|
73
|
+
wetRoomsCount: rooms.filter((room) => WET_ROOMS.includes(room.roomType)).length
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region src/variables/product.ts
|
|
78
|
+
function productVariablesOf(product, wastePercent) {
|
|
79
|
+
return {
|
|
80
|
+
price: product.priceCents / 100,
|
|
81
|
+
waste: wastePercent / 100,
|
|
82
|
+
unit: product.unit
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function productVariables(product, category) {
|
|
86
|
+
return productVariablesOf(product, product.wastePercentOverride ?? category.wastePercent);
|
|
87
|
+
}
|
|
88
|
+
//#endregion
|
|
29
89
|
//#region src/estimate/line.ts
|
|
30
90
|
const MAX_LINE_CENTS = 1e10;
|
|
31
91
|
function createLine(init) {
|
|
@@ -55,18 +115,25 @@ function failedLine(init, status, error) {
|
|
|
55
115
|
});
|
|
56
116
|
}
|
|
57
117
|
function pricedLine(init, dollars) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
params: { maxCents: MAX_LINE_CENTS }
|
|
61
|
-
});
|
|
62
|
-
return createLine({
|
|
118
|
+
const amountCents = componentCents(dollars);
|
|
119
|
+
return amountCents === null ? failedLine(init, "ERROR", outOfRange()) : createLine({
|
|
63
120
|
...init,
|
|
64
|
-
amountCents
|
|
121
|
+
amountCents
|
|
65
122
|
});
|
|
66
123
|
}
|
|
67
124
|
function roundQuantity(quantity) {
|
|
68
125
|
return Math.round(quantity * 1e3) / 1e3;
|
|
69
126
|
}
|
|
127
|
+
function componentCents(dollars) {
|
|
128
|
+
if (!Number.isFinite(dollars) || dollars < 0 || dollars * 100 > 1e10) return null;
|
|
129
|
+
return toCents(dollars);
|
|
130
|
+
}
|
|
131
|
+
function outOfRange() {
|
|
132
|
+
return {
|
|
133
|
+
code: "AMOUNT_OUT_OF_RANGE",
|
|
134
|
+
params: { maxCents: MAX_LINE_CENTS }
|
|
135
|
+
};
|
|
136
|
+
}
|
|
70
137
|
//#endregion
|
|
71
138
|
//#region src/estimate/line-id.ts
|
|
72
139
|
function lineId(section, ...parts) {
|
|
@@ -82,16 +149,39 @@ function unitQuantity(unit, rooms, pieces) {
|
|
|
82
149
|
case "ROOM_SQM": return rooms.reduce((sum, room) => sum + room.lengthM * room.widthM, 0);
|
|
83
150
|
}
|
|
84
151
|
}
|
|
85
|
-
function
|
|
152
|
+
function optionUnitPriceCents(option, coefficients) {
|
|
153
|
+
if (option.priceCoefficientKeys.length === 0) return option.priceCents;
|
|
154
|
+
let dollars = 0;
|
|
155
|
+
for (const key of option.priceCoefficientKeys) {
|
|
156
|
+
const value = Object.hasOwn(coefficients, key) ? coefficients[key] : void 0;
|
|
157
|
+
if (value === void 0) return { missingKey: key };
|
|
158
|
+
dollars += value;
|
|
159
|
+
}
|
|
160
|
+
return componentCents(dollars);
|
|
161
|
+
}
|
|
162
|
+
function optionLine(configuration, option, selection, coefficients = {}) {
|
|
86
163
|
const init = {
|
|
87
164
|
id: lineId(option.kind, "option", option.id),
|
|
88
165
|
section: option.kind,
|
|
89
166
|
optionId: option.id
|
|
90
167
|
};
|
|
168
|
+
if (!option.scopes.includes(configuration.scope)) return failedLine(init, "UNAVAILABLE", {
|
|
169
|
+
code: "OPTION_SCOPE_NOT_ALLOWED",
|
|
170
|
+
params: {
|
|
171
|
+
optionId: option.id,
|
|
172
|
+
scope: configuration.scope
|
|
173
|
+
}
|
|
174
|
+
});
|
|
91
175
|
if (!option.available) return failedLine(init, "UNAVAILABLE", {
|
|
92
176
|
code: "PRODUCT_UNAVAILABLE",
|
|
93
177
|
params: { optionId: option.id }
|
|
94
178
|
});
|
|
179
|
+
const unitPriceCents = optionUnitPriceCents(option, coefficients);
|
|
180
|
+
if (unitPriceCents === null) return failedLine(init, "ERROR", outOfRange());
|
|
181
|
+
if (typeof unitPriceCents !== "number") return failedLine(init, "ERROR", {
|
|
182
|
+
code: "UNKNOWN_COEFFICIENT",
|
|
183
|
+
params: { key: unitPriceCents.missingKey }
|
|
184
|
+
});
|
|
95
185
|
const selected = configuration.rooms.filter((room) => selection.roomClientIds?.includes(room.clientId));
|
|
96
186
|
const allowed = option.roomTypeCodes.length === 0 ? selected : selected.filter((room) => option.roomTypeCodes.includes(room.roomTypeCode));
|
|
97
187
|
const rejected = selected.filter((room) => !allowed.includes(room));
|
|
@@ -100,7 +190,7 @@ function optionLine(configuration, option, selection) {
|
|
|
100
190
|
...init,
|
|
101
191
|
quantity: roundQuantity(quantity),
|
|
102
192
|
unit: option.unit,
|
|
103
|
-
unitPriceCents
|
|
193
|
+
unitPriceCents,
|
|
104
194
|
warnings: rejected.map((room) => ({
|
|
105
195
|
code: "OPTION_ROOM_TYPE_NOT_ALLOWED",
|
|
106
196
|
params: {
|
|
@@ -108,12 +198,12 @@ function optionLine(configuration, option, selection) {
|
|
|
108
198
|
roomTypeCode: room.roomTypeCode
|
|
109
199
|
}
|
|
110
200
|
}))
|
|
111
|
-
},
|
|
201
|
+
}, unitPriceCents / 100 * quantity);
|
|
112
202
|
}
|
|
113
203
|
function optionLines(configuration, data, kind) {
|
|
114
204
|
return configuration.options.flatMap((selection) => {
|
|
115
205
|
const option = data.options.find((item) => item.id === selection.optionId);
|
|
116
|
-
return option?.kind === kind ? [optionLine(configuration, option, selection)] : [];
|
|
206
|
+
return option?.kind === kind ? [optionLine(configuration, option, selection, data.coefficients)] : [];
|
|
117
207
|
});
|
|
118
208
|
}
|
|
119
209
|
//#endregion
|
|
@@ -122,29 +212,6 @@ function additionalLines(configuration, data) {
|
|
|
122
212
|
return optionLines(configuration, data, "ADDITIONAL");
|
|
123
213
|
}
|
|
124
214
|
//#endregion
|
|
125
|
-
//#region src/estimate/base.ts
|
|
126
|
-
function baseLine(configuration, data) {
|
|
127
|
-
const init = {
|
|
128
|
-
id: lineId("BASE"),
|
|
129
|
-
section: "BASE"
|
|
130
|
-
};
|
|
131
|
-
const area = configuration.totalAreaSqm;
|
|
132
|
-
const rate = data.baseRates.find((candidate) => candidate.finishLevel === configuration.finishLevel && candidate.areaFromSqm <= area && area < candidate.areaToSqm);
|
|
133
|
-
if (rate === void 0) return failedLine(init, "NOT_CALCULATED", {
|
|
134
|
-
code: "BASE_RATE_NOT_FOUND",
|
|
135
|
-
params: {
|
|
136
|
-
totalAreaSqm: area,
|
|
137
|
-
finishLevel: configuration.finishLevel
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
return pricedLine({
|
|
141
|
-
...init,
|
|
142
|
-
quantity: roundQuantity(area),
|
|
143
|
-
unit: "SQM",
|
|
144
|
-
unitPriceCents: rate.rateCentsPerSqm
|
|
145
|
-
}, rate.rateCentsPerSqm / 100 * area);
|
|
146
|
-
}
|
|
147
|
-
//#endregion
|
|
148
215
|
//#region src/estimate/engineering.ts
|
|
149
216
|
function packageLine(configuration, item) {
|
|
150
217
|
const init = {
|
|
@@ -169,51 +236,7 @@ function packageLine(configuration, item) {
|
|
|
169
236
|
}, item.priceCents / 100 * quantity);
|
|
170
237
|
}
|
|
171
238
|
function engineeringLines(configuration, data) {
|
|
172
|
-
return [...data.packageItems.map((item) => packageLine(configuration, item)), ...optionLines(configuration, data, "ENGINEERING")];
|
|
173
|
-
}
|
|
174
|
-
//#endregion
|
|
175
|
-
//#region src/variables/object.ts
|
|
176
|
-
const COUNTED_AS_ROOMS = [
|
|
177
|
-
"LIVING_ROOM",
|
|
178
|
-
"BEDROOM",
|
|
179
|
-
"KITCHEN_LIVING"
|
|
180
|
-
];
|
|
181
|
-
function objectVariables(configuration) {
|
|
182
|
-
return {
|
|
183
|
-
totalArea: configuration.totalAreaSqm,
|
|
184
|
-
balconyArea: configuration.balconyAreaSqm,
|
|
185
|
-
balconyTiles: configuration.balconyTiles,
|
|
186
|
-
finishLevel: configuration.finishLevel,
|
|
187
|
-
roomsCount: configuration.rooms.filter((room) => COUNTED_AS_ROOMS.includes(room.roomTypeCode)).length,
|
|
188
|
-
bathroomsCount: configuration.rooms.filter((room) => room.roomTypeCode === "BATHROOM").length
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
//#endregion
|
|
192
|
-
//#region src/variables/product.ts
|
|
193
|
-
function productVariablesOf(product, wastePercent) {
|
|
194
|
-
return {
|
|
195
|
-
price: product.priceCents / 100,
|
|
196
|
-
waste: wastePercent / 100,
|
|
197
|
-
unit: product.unit
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
function productVariables(product, category) {
|
|
201
|
-
return productVariablesOf(product, product.wastePercentOverride ?? category.wastePercent);
|
|
202
|
-
}
|
|
203
|
-
//#endregion
|
|
204
|
-
//#region src/variables/room.ts
|
|
205
|
-
function roomVariables(room) {
|
|
206
|
-
const perimeter = 2 * (room.lengthM + room.widthM);
|
|
207
|
-
return {
|
|
208
|
-
length: room.lengthM,
|
|
209
|
-
width: room.widthM,
|
|
210
|
-
height: room.heightM,
|
|
211
|
-
floorArea: room.lengthM * room.widthM,
|
|
212
|
-
perimeter,
|
|
213
|
-
wallArea: perimeter * room.heightM,
|
|
214
|
-
heatedFloor: room.heatedFloor,
|
|
215
|
-
roomType: room.roomTypeCode
|
|
216
|
-
};
|
|
239
|
+
return [...(configuration.scope === "PARTIAL" ? data.packageItems.filter((item) => !item.includedInBase) : data.packageItems).map((item) => packageLine(configuration, item)), ...optionLines(configuration, data, "ENGINEERING")];
|
|
217
240
|
}
|
|
218
241
|
//#endregion
|
|
219
242
|
//#region src/formula/language.ts
|
|
@@ -647,13 +670,87 @@ function runNumericFormula(source, scope) {
|
|
|
647
670
|
};
|
|
648
671
|
}
|
|
649
672
|
//#endregion
|
|
673
|
+
//#region src/estimate/surfaces.ts
|
|
674
|
+
function surfaceRuleOf(data, surface) {
|
|
675
|
+
return data.rules.filter((rule) => rule.level === "SURFACE" && rule.surface === surface).sort((a, b) => a.sortOrder - b.sortOrder)[0];
|
|
676
|
+
}
|
|
677
|
+
function failedWith(line, error) {
|
|
678
|
+
return failedLine({
|
|
679
|
+
id: line.id,
|
|
680
|
+
section: line.section,
|
|
681
|
+
roomClientId: line.roomClientId,
|
|
682
|
+
categoryId: line.categoryId,
|
|
683
|
+
productId: line.productId,
|
|
684
|
+
ruleId: line.ruleId,
|
|
685
|
+
warnings: line.warnings
|
|
686
|
+
}, "ERROR", error);
|
|
687
|
+
}
|
|
688
|
+
function withComponent(line, component) {
|
|
689
|
+
const components = [...line.components ?? [], component];
|
|
690
|
+
const amountCents = components.reduce((sum, item) => sum + item.amountCents, 0);
|
|
691
|
+
if (amountCents > 1e10) return failedWith(line, outOfRange());
|
|
692
|
+
return {
|
|
693
|
+
...line,
|
|
694
|
+
components,
|
|
695
|
+
amountCents
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
function applySurfacePrep(data, room, lines, objectScope) {
|
|
699
|
+
const result = [...lines];
|
|
700
|
+
const variables = {
|
|
701
|
+
...roomVariables(room),
|
|
702
|
+
...objectScope
|
|
703
|
+
};
|
|
704
|
+
const areas = {
|
|
705
|
+
FLOOR: variables.floorArea,
|
|
706
|
+
CEILING: variables.floorArea,
|
|
707
|
+
WALLS: variables.wallArea
|
|
708
|
+
};
|
|
709
|
+
const surfaces = /* @__PURE__ */ new Map();
|
|
710
|
+
lines.forEach((line, index) => {
|
|
711
|
+
const surface = data.categories.find((category) => category.id === line.categoryId)?.surface;
|
|
712
|
+
if (line.status === "OK" && surface !== void 0 && surface !== "NONE") surfaces.set(surface, [...surfaces.get(surface) ?? [], index]);
|
|
713
|
+
});
|
|
714
|
+
for (const [surface, indexes] of surfaces) {
|
|
715
|
+
const rule = surfaceRuleOf(data, surface);
|
|
716
|
+
if (rule === void 0) continue;
|
|
717
|
+
const outcome = runNumericFormula(rule.costFormula, {
|
|
718
|
+
variables: {
|
|
719
|
+
...variables,
|
|
720
|
+
surfaceArea: areas[surface]
|
|
721
|
+
},
|
|
722
|
+
coefficients: data.coefficients
|
|
723
|
+
});
|
|
724
|
+
const cents = outcome.ok ? componentCents(outcome.value) : null;
|
|
725
|
+
const carrierCategoryId = lines[indexes[0]]?.categoryId ?? null;
|
|
726
|
+
indexes.forEach((index, position) => {
|
|
727
|
+
const line = lines[index];
|
|
728
|
+
if (!outcome.ok || cents === null) {
|
|
729
|
+
result[index] = failedWith(line, outcome.ok ? outOfRange() : outcome.error);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
result[index] = withComponent(line, position === 0 ? {
|
|
733
|
+
kind: "PREP",
|
|
734
|
+
ruleId: rule.id,
|
|
735
|
+
amountCents: cents
|
|
736
|
+
} : {
|
|
737
|
+
kind: "PREP",
|
|
738
|
+
ruleId: rule.id,
|
|
739
|
+
amountCents: 0,
|
|
740
|
+
sharedWithCategoryId: carrierCategoryId ?? void 0
|
|
741
|
+
});
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
return result;
|
|
745
|
+
}
|
|
746
|
+
//#endregion
|
|
650
747
|
//#region src/estimate/materials.ts
|
|
651
748
|
const isMaterialRule = (rule) => rule.quantityFormula !== null;
|
|
652
749
|
function selectRule(data, categoryId, materialTypeId) {
|
|
653
750
|
const rules = data.rules.filter(isMaterialRule);
|
|
654
751
|
return rules.find((rule) => rule.level === "MATERIAL_TYPE" && rule.categoryId === categoryId && rule.materialTypeId === materialTypeId) ?? rules.find((rule) => rule.level === "CATEGORY" && rule.categoryId === categoryId);
|
|
655
752
|
}
|
|
656
|
-
function materialLine(configuration, data, room, categoryId, productId) {
|
|
753
|
+
function materialLine(configuration, data, room, categoryId, productId, objectScope) {
|
|
657
754
|
const init = {
|
|
658
755
|
id: lineId("MATERIALS", room.clientId, categoryId),
|
|
659
756
|
section: "MATERIALS",
|
|
@@ -685,7 +782,7 @@ function materialLine(configuration, data, room, categoryId, productId) {
|
|
|
685
782
|
const variables = {
|
|
686
783
|
...roomVariables(room),
|
|
687
784
|
...productVariables(product, category),
|
|
688
|
-
...
|
|
785
|
+
...objectScope
|
|
689
786
|
};
|
|
690
787
|
const { coefficients } = data;
|
|
691
788
|
const ruled = {
|
|
@@ -706,32 +803,74 @@ function materialLine(configuration, data, room, categoryId, productId) {
|
|
|
706
803
|
coefficients
|
|
707
804
|
});
|
|
708
805
|
if (!cost.ok) return failedLine(ruled, "ERROR", cost.error);
|
|
709
|
-
|
|
806
|
+
const materialCents = componentCents(cost.value);
|
|
807
|
+
if (materialCents === null) return failedLine(ruled, "ERROR", outOfRange());
|
|
808
|
+
const components = [{
|
|
809
|
+
kind: "MATERIAL",
|
|
810
|
+
ruleId: rule.id,
|
|
811
|
+
amountCents: materialCents
|
|
812
|
+
}];
|
|
813
|
+
const lineWarnings = [...warnings];
|
|
814
|
+
if (configuration.scope === "PARTIAL") {
|
|
815
|
+
let laborCents = 0;
|
|
816
|
+
if (rule.laborFormula === null) lineWarnings.push({
|
|
817
|
+
code: "LABOR_RATE_MISSING",
|
|
818
|
+
params: { categoryId }
|
|
819
|
+
});
|
|
820
|
+
else {
|
|
821
|
+
const labor = runNumericFormula(rule.laborFormula, {
|
|
822
|
+
variables: {
|
|
823
|
+
...variables,
|
|
824
|
+
quantity: quantity.value
|
|
825
|
+
},
|
|
826
|
+
coefficients
|
|
827
|
+
});
|
|
828
|
+
if (!labor.ok) return failedLine(ruled, "ERROR", labor.error);
|
|
829
|
+
const cents = componentCents(labor.value);
|
|
830
|
+
if (cents === null) return failedLine(ruled, "ERROR", outOfRange());
|
|
831
|
+
laborCents = cents;
|
|
832
|
+
}
|
|
833
|
+
components.push({
|
|
834
|
+
kind: "LABOR",
|
|
835
|
+
ruleId: rule.id,
|
|
836
|
+
amountCents: laborCents
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
const amountCents = components.reduce((sum, component) => sum + component.amountCents, 0);
|
|
840
|
+
if (amountCents > 1e10) return failedLine(ruled, "ERROR", outOfRange());
|
|
841
|
+
return createLine({
|
|
710
842
|
...ruled,
|
|
843
|
+
warnings: lineWarnings,
|
|
711
844
|
quantity: roundQuantity(quantity.value),
|
|
712
845
|
unit: product.unit,
|
|
713
|
-
unitPriceCents: product.priceCents
|
|
714
|
-
|
|
846
|
+
unitPriceCents: product.priceCents,
|
|
847
|
+
amountCents,
|
|
848
|
+
components
|
|
849
|
+
});
|
|
715
850
|
}
|
|
716
|
-
function materialLines(configuration, data) {
|
|
717
|
-
return configuration.rooms.flatMap((room) =>
|
|
851
|
+
function materialLines(configuration, data, objectScope) {
|
|
852
|
+
return configuration.rooms.flatMap((room) => {
|
|
853
|
+
const lines = room.selections.flatMap(({ categoryId, productId }) => productId === null ? [] : [materialLine(configuration, data, room, categoryId, productId, objectScope)]);
|
|
854
|
+
return configuration.scope === "PARTIAL" ? applySurfacePrep(data, room, lines, objectScope) : lines;
|
|
855
|
+
});
|
|
718
856
|
}
|
|
719
857
|
//#endregion
|
|
720
858
|
//#region src/estimate/object-rules.ts
|
|
721
|
-
function objectRuleLines(configuration, data) {
|
|
859
|
+
function objectRuleLines(configuration, data, section, variables) {
|
|
860
|
+
if (section === "BASE" && configuration.scope !== "FULL") return [];
|
|
722
861
|
const scope = {
|
|
723
|
-
variables
|
|
862
|
+
variables,
|
|
724
863
|
coefficients: data.coefficients
|
|
725
864
|
};
|
|
726
|
-
return data.rules.filter((rule) => rule.level === "OBJECT").sort((a, b) => a.sortOrder - b.sortOrder).map((rule) => {
|
|
865
|
+
return data.rules.filter((rule) => rule.level === "OBJECT" && rule.baseComponent === (section === "BASE")).sort((a, b) => a.sortOrder - b.sortOrder).map((rule) => {
|
|
727
866
|
const init = {
|
|
728
|
-
id: lineId(
|
|
729
|
-
section
|
|
867
|
+
id: lineId(section, rule.id),
|
|
868
|
+
section,
|
|
730
869
|
ruleId: rule.id
|
|
731
870
|
};
|
|
732
871
|
const outcome = runNumericFormula(rule.costFormula, scope);
|
|
733
872
|
return outcome.ok ? pricedLine(init, outcome.value) : failedLine(init, "ERROR", outcome.error);
|
|
734
|
-
});
|
|
873
|
+
}).filter((line) => section !== "BASE" || line.status !== "OK" || line.amountCents !== 0);
|
|
735
874
|
}
|
|
736
875
|
//#endregion
|
|
737
876
|
//#region src/schemas/common.ts
|
|
@@ -739,11 +878,17 @@ const LocalizedTextSchema = z.object({
|
|
|
739
878
|
en: z.string(),
|
|
740
879
|
uk: z.string()
|
|
741
880
|
});
|
|
742
|
-
const
|
|
881
|
+
const ScopeSchema = z.enum(["FULL", "PARTIAL"]);
|
|
882
|
+
const FinishLevelSchema = z.enum([
|
|
883
|
+
"ROUGH",
|
|
884
|
+
"PRE_FINISH",
|
|
885
|
+
"LIVED_IN"
|
|
886
|
+
]);
|
|
743
887
|
const RuleLevelSchema = z.enum([
|
|
744
888
|
"OBJECT",
|
|
745
889
|
"CATEGORY",
|
|
746
|
-
"MATERIAL_TYPE"
|
|
890
|
+
"MATERIAL_TYPE",
|
|
891
|
+
"SURFACE"
|
|
747
892
|
]);
|
|
748
893
|
const RateSourceSchema = z.enum(["NBU", "MANUAL"]);
|
|
749
894
|
const SurfaceKindSchema = z.enum([
|
|
@@ -770,7 +915,9 @@ const RoomTypeCodeSchema = z.enum([
|
|
|
770
915
|
"BEDROOM",
|
|
771
916
|
"KITCHEN",
|
|
772
917
|
"KITCHEN_LIVING",
|
|
773
|
-
"BATHROOM"
|
|
918
|
+
"BATHROOM",
|
|
919
|
+
"HALLWAY",
|
|
920
|
+
"STORAGE"
|
|
774
921
|
]);
|
|
775
922
|
const CurrencySchema = z.enum(["USD", "UAH"]);
|
|
776
923
|
const SectionSchema = z.enum([
|
|
@@ -780,6 +927,11 @@ const SectionSchema = z.enum([
|
|
|
780
927
|
"ENGINEERING",
|
|
781
928
|
"ADDITIONAL"
|
|
782
929
|
]);
|
|
930
|
+
const LineComponentKindSchema = z.enum([
|
|
931
|
+
"MATERIAL",
|
|
932
|
+
"LABOR",
|
|
933
|
+
"PREP"
|
|
934
|
+
]);
|
|
783
935
|
const LineStatusSchema = z.enum([
|
|
784
936
|
"OK",
|
|
785
937
|
"INCLUDED",
|
|
@@ -823,8 +975,9 @@ function duplicateIndexes(items, key) {
|
|
|
823
975
|
});
|
|
824
976
|
}
|
|
825
977
|
const ConfigurationSchema = z.object({
|
|
978
|
+
scope: ScopeSchema.default("FULL"),
|
|
826
979
|
finishLevel: FinishLevelSchema,
|
|
827
|
-
totalAreaSqm: z.number().positive(),
|
|
980
|
+
totalAreaSqm: z.number().positive().nullable(),
|
|
828
981
|
balconyAreaSqm: z.number().min(0),
|
|
829
982
|
balconyTiles: z.boolean(),
|
|
830
983
|
rooms: z.array(RoomConfigurationSchema),
|
|
@@ -836,6 +989,11 @@ const ConfigurationSchema = z.object({
|
|
|
836
989
|
path,
|
|
837
990
|
message: "Duplicate"
|
|
838
991
|
});
|
|
992
|
+
if (configuration.scope === "FULL" !== (configuration.totalAreaSqm !== null)) ctx.addIssue({
|
|
993
|
+
code: "custom",
|
|
994
|
+
path: ["totalAreaSqm"],
|
|
995
|
+
message: configuration.scope === "FULL" ? "Required" : "Not allowed"
|
|
996
|
+
});
|
|
839
997
|
for (const index of duplicateIndexes(configuration.rooms, (room) => room.clientId)) addDuplicate([
|
|
840
998
|
"rooms",
|
|
841
999
|
index,
|
|
@@ -867,13 +1025,8 @@ const PricingLimitsSchema = z.object({
|
|
|
867
1025
|
totalAreaSqm: RangeSchema,
|
|
868
1026
|
rooms: RangeSchema,
|
|
869
1027
|
roomSideM: RangeSchema,
|
|
870
|
-
roomHeightM: RangeSchema
|
|
871
|
-
|
|
872
|
-
const BaseRateSchema = z.object({
|
|
873
|
-
finishLevel: FinishLevelSchema,
|
|
874
|
-
areaFromSqm: z.number().min(0),
|
|
875
|
-
areaToSqm: z.number(),
|
|
876
|
-
rateCentsPerSqm: z.int().positive()
|
|
1028
|
+
roomHeightM: RangeSchema,
|
|
1029
|
+
missingAreaHintSqm: z.number().min(0)
|
|
877
1030
|
});
|
|
878
1031
|
const PricingRuleSchema = z.object({
|
|
879
1032
|
id: z.string(),
|
|
@@ -882,9 +1035,16 @@ const PricingRuleSchema = z.object({
|
|
|
882
1035
|
materialTypeId: z.string().nullable(),
|
|
883
1036
|
name: LocalizedTextSchema,
|
|
884
1037
|
quantityFormula: z.string().nullable(),
|
|
1038
|
+
surface: z.enum([
|
|
1039
|
+
"FLOOR",
|
|
1040
|
+
"WALLS",
|
|
1041
|
+
"CEILING"
|
|
1042
|
+
]).nullable(),
|
|
1043
|
+
baseComponent: z.boolean(),
|
|
885
1044
|
costFormula: z.string().max(500),
|
|
1045
|
+
laborFormula: z.string().max(500).nullable(),
|
|
886
1046
|
sortOrder: z.int()
|
|
887
|
-
}).refine((rule) => rule.level === "OBJECT" === (rule.quantityFormula === null), { path: ["quantityFormula"] });
|
|
1047
|
+
}).refine((rule) => (rule.level === "OBJECT" || rule.level === "SURFACE") === (rule.quantityFormula === null), { path: ["quantityFormula"] }).refine((rule) => rule.level === "SURFACE" === (rule.surface !== null), { path: ["surface"] }).refine((rule) => rule.level !== "SURFACE" || rule.categoryId === null, { path: ["categoryId"] }).refine((rule) => !rule.baseComponent || rule.level === "OBJECT", { path: ["baseComponent"] }).refine((rule) => rule.laborFormula === null || rule.level === "CATEGORY" || rule.level === "MATERIAL_TYPE", { path: ["laborFormula"] });
|
|
888
1048
|
const PricingRoomTypeSchema = z.object({
|
|
889
1049
|
id: z.string(),
|
|
890
1050
|
code: z.string()
|
|
@@ -921,6 +1081,8 @@ const PricingOptionSchema = z.object({
|
|
|
921
1081
|
priceCents: CentsSchema,
|
|
922
1082
|
unit: OptionUnitSchema,
|
|
923
1083
|
roomTypeCodes: z.array(z.string()),
|
|
1084
|
+
priceCoefficientKeys: z.array(z.string()),
|
|
1085
|
+
scopes: z.array(ScopeSchema).min(1),
|
|
924
1086
|
minQuantity: z.int().nullable(),
|
|
925
1087
|
maxQuantity: z.int().nullable(),
|
|
926
1088
|
available: z.boolean()
|
|
@@ -929,7 +1091,6 @@ const PricingDataSchema = z.object({
|
|
|
929
1091
|
version: z.string().regex(/^[0-9a-f]{16}$/),
|
|
930
1092
|
exchangeRate: ExchangeRateSchema.nullable(),
|
|
931
1093
|
limits: PricingLimitsSchema,
|
|
932
|
-
baseRates: z.array(BaseRateSchema),
|
|
933
1094
|
coefficients: z.record(z.string(), z.number()),
|
|
934
1095
|
rules: z.array(PricingRuleSchema),
|
|
935
1096
|
roomTypes: z.array(PricingRoomTypeSchema),
|
|
@@ -945,6 +1106,12 @@ const EstimateMessageSchema = z.object({
|
|
|
945
1106
|
code: z.string(),
|
|
946
1107
|
params: z.record(z.string(), z.unknown()).optional()
|
|
947
1108
|
});
|
|
1109
|
+
const LineComponentSchema = z.object({
|
|
1110
|
+
kind: LineComponentKindSchema,
|
|
1111
|
+
ruleId: z.string(),
|
|
1112
|
+
amountCents: z.number().int().min(0),
|
|
1113
|
+
sharedWithCategoryId: z.string().optional()
|
|
1114
|
+
});
|
|
948
1115
|
const EstimateLineSchema = z.object({
|
|
949
1116
|
id: z.string(),
|
|
950
1117
|
section: SectionSchema,
|
|
@@ -960,6 +1127,7 @@ const EstimateLineSchema = z.object({
|
|
|
960
1127
|
unitPriceCents: CentsSchema.nullable(),
|
|
961
1128
|
amountCents: CentsSchema,
|
|
962
1129
|
amountUahKopecks: CentsSchema.nullable(),
|
|
1130
|
+
components: z.array(LineComponentSchema).optional(),
|
|
963
1131
|
warnings: z.array(EstimateMessageSchema),
|
|
964
1132
|
error: EstimateMessageSchema.nullable()
|
|
965
1133
|
});
|
|
@@ -997,18 +1165,32 @@ const RuleSampleSchema = z.object({
|
|
|
997
1165
|
unit: ProductUnitSchema,
|
|
998
1166
|
wastePercent: z.number().min(0)
|
|
999
1167
|
}).optional(),
|
|
1168
|
+
surfaceArea: z.number().min(0).optional(),
|
|
1000
1169
|
object: z.object({
|
|
1001
|
-
|
|
1170
|
+
scope: ScopeSchema,
|
|
1171
|
+
totalAreaSqm: z.number().positive().nullable(),
|
|
1002
1172
|
balconyAreaSqm: z.number().min(0),
|
|
1003
1173
|
balconyTiles: z.boolean(),
|
|
1004
1174
|
finishLevel: FinishLevelSchema,
|
|
1005
1175
|
roomsCount: z.int().min(0),
|
|
1006
|
-
bathroomsCount: z.int().min(0)
|
|
1176
|
+
bathroomsCount: z.int().min(0),
|
|
1177
|
+
roomsFloorArea: z.number().min(0),
|
|
1178
|
+
roomsWallArea: z.number().min(0),
|
|
1179
|
+
roomsPerimeter: z.number().min(0),
|
|
1180
|
+
wetRoomsCount: z.int().min(0)
|
|
1007
1181
|
})
|
|
1182
|
+
}).superRefine((sample, ctx) => {
|
|
1183
|
+
const { scope, totalAreaSqm } = sample.object;
|
|
1184
|
+
if (scope === "FULL" !== (totalAreaSqm !== null)) ctx.addIssue({
|
|
1185
|
+
code: "custom",
|
|
1186
|
+
path: ["object", "totalAreaSqm"],
|
|
1187
|
+
message: scope === "FULL" ? "Required" : "Not allowed"
|
|
1188
|
+
});
|
|
1008
1189
|
});
|
|
1009
1190
|
const RulePreviewSchema = z.object({
|
|
1010
1191
|
quantity: z.number().nullable(),
|
|
1011
1192
|
amountCents: CentsSchema.nullable(),
|
|
1193
|
+
laborCents: CentsSchema.nullable(),
|
|
1012
1194
|
variables: z.record(z.string(), z.union([
|
|
1013
1195
|
z.number(),
|
|
1014
1196
|
z.boolean(),
|
|
@@ -1043,20 +1225,28 @@ function isIncomplete(lines) {
|
|
|
1043
1225
|
}
|
|
1044
1226
|
//#endregion
|
|
1045
1227
|
//#region src/estimate/warnings.ts
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1228
|
+
const roundArea = (value) => Math.round(value * 1e4) / 1e4;
|
|
1229
|
+
function roomsAreaWarnings(configuration, limits) {
|
|
1230
|
+
const totalAreaSqm = configuration.totalAreaSqm;
|
|
1231
|
+
if (configuration.scope !== "FULL" || totalAreaSqm === null) return [];
|
|
1232
|
+
const roomsAreaSqm = roundArea(configuration.rooms.reduce((sum, room) => sum + roomVariables(room).floorArea, 0));
|
|
1233
|
+
if (roomsAreaSqm > totalAreaSqm) return [{
|
|
1049
1234
|
code: "ROOMS_AREA_EXCEEDS_TOTAL",
|
|
1050
1235
|
params: {
|
|
1051
1236
|
roomsAreaSqm,
|
|
1052
|
-
totalAreaSqm
|
|
1237
|
+
totalAreaSqm
|
|
1053
1238
|
}
|
|
1239
|
+
}];
|
|
1240
|
+
const missingSqm = roundArea(totalAreaSqm - roomsAreaSqm);
|
|
1241
|
+
return missingSqm > limits.missingAreaHintSqm ? [{
|
|
1242
|
+
code: "ROOMS_AREA_BELOW_TOTAL",
|
|
1243
|
+
params: { missingSqm }
|
|
1054
1244
|
}] : [];
|
|
1055
1245
|
}
|
|
1056
|
-
function estimateWarnings(lines, configuration, rate) {
|
|
1246
|
+
function estimateWarnings(lines, configuration, rate, limits) {
|
|
1057
1247
|
return [
|
|
1058
1248
|
...collectWarnings(lines),
|
|
1059
|
-
...roomsAreaWarnings(configuration),
|
|
1249
|
+
...roomsAreaWarnings(configuration, limits),
|
|
1060
1250
|
...configuration.currency === "UAH" && rate === null ? [{ code: "EXCHANGE_RATE_UNAVAILABLE" }] : []
|
|
1061
1251
|
];
|
|
1062
1252
|
}
|
|
@@ -1070,10 +1260,11 @@ function withKopecks(line, rate) {
|
|
|
1070
1260
|
}
|
|
1071
1261
|
function estimate(configuration, data) {
|
|
1072
1262
|
const rate = data.exchangeRate?.rate ?? null;
|
|
1263
|
+
const variables = objectVariables(configuration);
|
|
1073
1264
|
const lines = [
|
|
1074
|
-
|
|
1075
|
-
...objectRuleLines(configuration, data),
|
|
1076
|
-
...materialLines(configuration, data),
|
|
1265
|
+
...objectRuleLines(configuration, data, "BASE", variables),
|
|
1266
|
+
...objectRuleLines(configuration, data, "OBJECT", variables),
|
|
1267
|
+
...materialLines(configuration, data, variables),
|
|
1077
1268
|
...engineeringLines(configuration, data),
|
|
1078
1269
|
...additionalLines(configuration, data)
|
|
1079
1270
|
].map((line) => withKopecks(line, rate));
|
|
@@ -1083,7 +1274,7 @@ function estimate(configuration, data) {
|
|
|
1083
1274
|
exchangeRate: data.exchangeRate,
|
|
1084
1275
|
lines,
|
|
1085
1276
|
totals: computeTotals(lines, rate !== null),
|
|
1086
|
-
warnings: estimateWarnings(lines, configuration, rate),
|
|
1277
|
+
warnings: estimateWarnings(lines, configuration, rate, data.limits),
|
|
1087
1278
|
incomplete: isIncomplete(lines)
|
|
1088
1279
|
};
|
|
1089
1280
|
}
|
|
@@ -1098,8 +1289,9 @@ function checkLimits(configuration, limits, options = []) {
|
|
|
1098
1289
|
max
|
|
1099
1290
|
});
|
|
1100
1291
|
};
|
|
1101
|
-
|
|
1102
|
-
check("
|
|
1292
|
+
const isFull = configuration.scope === "FULL";
|
|
1293
|
+
if (isFull && configuration.totalAreaSqm !== null) check("totalAreaSqm", configuration.totalAreaSqm, limits.totalAreaSqm.min, limits.totalAreaSqm.max);
|
|
1294
|
+
check("balconyAreaSqm", configuration.balconyAreaSqm, 0, isFull && configuration.totalAreaSqm !== null ? configuration.totalAreaSqm : limits.totalAreaSqm.max);
|
|
1103
1295
|
check("rooms", configuration.rooms.length, limits.rooms.min, limits.rooms.max);
|
|
1104
1296
|
configuration.rooms.forEach((room, index) => {
|
|
1105
1297
|
const path = `rooms[${index}]`;
|
|
@@ -1155,17 +1347,33 @@ function referencesOf(sources) {
|
|
|
1155
1347
|
}
|
|
1156
1348
|
function objectScope({ object }) {
|
|
1157
1349
|
return {
|
|
1158
|
-
|
|
1350
|
+
scope: object.scope,
|
|
1351
|
+
totalArea: totalAreaOf(object.scope, object.totalAreaSqm, object.roomsFloorArea),
|
|
1159
1352
|
balconyArea: object.balconyAreaSqm,
|
|
1160
1353
|
balconyTiles: object.balconyTiles,
|
|
1161
1354
|
finishLevel: object.finishLevel,
|
|
1162
1355
|
roomsCount: object.roomsCount,
|
|
1163
|
-
bathroomsCount: object.bathroomsCount
|
|
1356
|
+
bathroomsCount: object.bathroomsCount,
|
|
1357
|
+
roomsFloorArea: object.roomsFloorArea,
|
|
1358
|
+
roomsWallArea: object.roomsWallArea,
|
|
1359
|
+
roomsPerimeter: object.roomsPerimeter,
|
|
1360
|
+
wetRoomsCount: object.wetRoomsCount
|
|
1164
1361
|
};
|
|
1165
1362
|
}
|
|
1166
1363
|
function scopeOf(level, sample) {
|
|
1167
1364
|
if (level === "OBJECT") return { scope: objectScope(sample) };
|
|
1168
|
-
const { room, product } = sample;
|
|
1365
|
+
const { room, product, surfaceArea } = sample;
|
|
1366
|
+
if (level === "SURFACE") {
|
|
1367
|
+
if (room === void 0 || surfaceArea === void 0) return { error: {
|
|
1368
|
+
code: "SAMPLE_INCOMPLETE",
|
|
1369
|
+
params: { missing: room === void 0 ? "room" : "surfaceArea" }
|
|
1370
|
+
} };
|
|
1371
|
+
return { scope: {
|
|
1372
|
+
...roomVariables(room),
|
|
1373
|
+
...objectScope(sample),
|
|
1374
|
+
surfaceArea
|
|
1375
|
+
} };
|
|
1376
|
+
}
|
|
1169
1377
|
if (room === void 0 || product === void 0) return { error: {
|
|
1170
1378
|
code: "SAMPLE_INCOMPLETE",
|
|
1171
1379
|
params: { missing: room === void 0 ? "room" : "product" }
|
|
@@ -1181,15 +1389,21 @@ function pick(source, names) {
|
|
|
1181
1389
|
}
|
|
1182
1390
|
function previewRule(input) {
|
|
1183
1391
|
const { level, quantityFormula, costFormula, coefficients } = input;
|
|
1184
|
-
const
|
|
1185
|
-
const
|
|
1392
|
+
const laborFormula = quantityFormula === null ? null : input.laborFormula ?? null;
|
|
1393
|
+
const references = referencesOf([
|
|
1394
|
+
quantityFormula,
|
|
1395
|
+
costFormula,
|
|
1396
|
+
laborFormula
|
|
1397
|
+
]);
|
|
1398
|
+
const report = (variables, quantity, amountCents, error, laborCents = null) => ({
|
|
1186
1399
|
quantity,
|
|
1187
1400
|
amountCents,
|
|
1401
|
+
laborCents,
|
|
1188
1402
|
variables: pick(variables, references.variables),
|
|
1189
1403
|
coefficients: pick(coefficients, references.coefficients),
|
|
1190
1404
|
error
|
|
1191
1405
|
});
|
|
1192
|
-
if (level === "OBJECT" !== (quantityFormula === null)) return report({}, null, null, { code: "QUANTITY_FORMULA_MISMATCH" });
|
|
1406
|
+
if ((level === "OBJECT" || level === "SURFACE") !== (quantityFormula === null)) return report({}, null, null, { code: "QUANTITY_FORMULA_MISMATCH" });
|
|
1193
1407
|
const prepared = scopeOf(level, input.sample);
|
|
1194
1408
|
if ("error" in prepared) return report({}, null, null, prepared.error);
|
|
1195
1409
|
let variables = prepared.scope;
|
|
@@ -1215,7 +1429,15 @@ function previewRule(input) {
|
|
|
1215
1429
|
id: "preview",
|
|
1216
1430
|
section: "OBJECT"
|
|
1217
1431
|
}, cost.value);
|
|
1218
|
-
|
|
1432
|
+
if (line.error !== null) return report(variables, quantity, null, line.error);
|
|
1433
|
+
if (laborFormula === null) return report(variables, quantity, line.amountCents, null);
|
|
1434
|
+
const labor = runNumericFormula(laborFormula, {
|
|
1435
|
+
variables,
|
|
1436
|
+
coefficients
|
|
1437
|
+
});
|
|
1438
|
+
if (!labor.ok) return report(variables, quantity, line.amountCents, labor.error);
|
|
1439
|
+
const laborCents = componentCents(labor.value);
|
|
1440
|
+
return laborCents === null ? report(variables, quantity, line.amountCents, outOfRange()) : report(variables, quantity, line.amountCents, null, laborCents);
|
|
1219
1441
|
}
|
|
1220
1442
|
//#endregion
|
|
1221
1443
|
//#region src/formula/suggest.ts
|
|
@@ -1287,10 +1509,16 @@ const FORMULA_SCOPE_VARIABLES = {
|
|
|
1287
1509
|
boolean("balconyTiles"),
|
|
1288
1510
|
enumeration("finishLevel", FinishLevelSchema.options),
|
|
1289
1511
|
number("roomsCount"),
|
|
1290
|
-
number("bathroomsCount")
|
|
1512
|
+
number("bathroomsCount"),
|
|
1513
|
+
enumeration("scope", ScopeSchema.options),
|
|
1514
|
+
number("roomsFloorArea"),
|
|
1515
|
+
number("roomsWallArea"),
|
|
1516
|
+
number("roomsPerimeter"),
|
|
1517
|
+
number("wetRoomsCount")
|
|
1291
1518
|
]
|
|
1292
1519
|
};
|
|
1293
1520
|
const FORMULA_QUANTITY_VARIABLE = number("quantity");
|
|
1521
|
+
const FORMULA_SURFACE_AREA_VARIABLE = number("surfaceArea");
|
|
1294
1522
|
const FORMULA_VARIABLES = {
|
|
1295
1523
|
OBJECT: FORMULA_SCOPE_VARIABLES.object,
|
|
1296
1524
|
CATEGORY: [
|
|
@@ -1302,9 +1530,14 @@ const FORMULA_VARIABLES = {
|
|
|
1302
1530
|
...FORMULA_SCOPE_VARIABLES.room,
|
|
1303
1531
|
...FORMULA_SCOPE_VARIABLES.product,
|
|
1304
1532
|
...FORMULA_SCOPE_VARIABLES.object
|
|
1533
|
+
],
|
|
1534
|
+
SURFACE: [
|
|
1535
|
+
...FORMULA_SCOPE_VARIABLES.room,
|
|
1536
|
+
...FORMULA_SCOPE_VARIABLES.object,
|
|
1537
|
+
FORMULA_SURFACE_AREA_VARIABLE
|
|
1305
1538
|
]
|
|
1306
1539
|
};
|
|
1307
|
-
const ALL_VARIABLE_NAMES = new Set(FORMULA_VARIABLES.CATEGORY.map((variable) => variable.name));
|
|
1540
|
+
const ALL_VARIABLE_NAMES = new Set([...FORMULA_VARIABLES.CATEGORY, ...FORMULA_VARIABLES.SURFACE].map((variable) => variable.name));
|
|
1308
1541
|
function span(node) {
|
|
1309
1542
|
return {
|
|
1310
1543
|
position: node.start,
|
|
@@ -1328,7 +1561,7 @@ function validateFormula(input) {
|
|
|
1328
1561
|
const knownCoefficients = new Set(input.coefficientKeys);
|
|
1329
1562
|
const usedCoefficients = /* @__PURE__ */ new Set();
|
|
1330
1563
|
const errors = [];
|
|
1331
|
-
const quantityAllowed = input.kind
|
|
1564
|
+
const quantityAllowed = input.kind !== "quantity" && (input.level === "CATEGORY" || input.level === "MATERIAL_TYPE");
|
|
1332
1565
|
function mismatch(node, expected) {
|
|
1333
1566
|
errors.push({
|
|
1334
1567
|
code: "FORMULA_SYNTAX",
|
|
@@ -1516,4 +1749,4 @@ function validateFormula(input) {
|
|
|
1516
1749
|
};
|
|
1517
1750
|
}
|
|
1518
1751
|
//#endregion
|
|
1519
|
-
export {
|
|
1752
|
+
export { COEFFICIENT_NAMESPACE, CentsSchema, ConfigurationSchema, CurrencySchema, EstimateLineSchema, EstimateMessageSchema, EstimateSchema, EstimateTotalsSchema, EstimateWarningSchema, ExchangeRateSchema, FORMULA_FUNCTIONS, FORMULA_FUNCTION_TABLE, FORMULA_LIMITS, FORMULA_QUANTITY_VARIABLE, FORMULA_SCOPE_VARIABLES, FORMULA_SURFACE_AREA_VARIABLE, FORMULA_VARIABLES, FinishLevelSchema, LineComponentKindSchema, LineComponentSchema, LineStatusSchema, LocalizedTextSchema, OptionCodeSchema, OptionKindSchema, OptionSelectionSchema, OptionUnitSchema, PricingCategorySchema, PricingDataSchema, PricingLimitsSchema, PricingMaterialTypeSchema, PricingOptionSchema, PricingPackageItemSchema, PricingProductSchema, PricingRoomTypeSchema, PricingRuleSchema, ProductUnitSchema, RangeSchema, RateSourceSchema, RoomConfigurationSchema, RoomSelectionSchema, RoomTypeCodeSchema, RuleLevelSchema, RulePreviewSchema, RuleSampleSchema, ScopeSchema, SectionSchema, SectionTotalSchema, SurfaceKindSchema, checkLimits, convertCentsToKopecks, estimate, evaluate, levenshtein, objectVariables, parse, previewRule, productVariables, productVariablesOf, roomVariables, suggestNearest, toCents, totalAreaOf, validateFormula };
|