@roomwise/calc-engine 0.3.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 -25
- package/dist/index.cjs +366 -136
- package/dist/index.d.cts +110 -39
- package/dist/index.d.ts +110 -39
- package/dist/index.js +362 -135
- 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,52 +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
|
-
kitchenType: configuration.kitchenType,
|
|
187
|
-
finishLevel: configuration.finishLevel,
|
|
188
|
-
roomsCount: configuration.rooms.filter((room) => COUNTED_AS_ROOMS.includes(room.roomTypeCode)).length,
|
|
189
|
-
bathroomsCount: configuration.rooms.filter((room) => room.roomTypeCode === "BATHROOM").length
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
//#endregion
|
|
193
|
-
//#region src/variables/product.ts
|
|
194
|
-
function productVariablesOf(product, wastePercent) {
|
|
195
|
-
return {
|
|
196
|
-
price: product.priceCents / 100,
|
|
197
|
-
waste: wastePercent / 100,
|
|
198
|
-
unit: product.unit
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
function productVariables(product, category) {
|
|
202
|
-
return productVariablesOf(product, product.wastePercentOverride ?? category.wastePercent);
|
|
203
|
-
}
|
|
204
|
-
//#endregion
|
|
205
|
-
//#region src/variables/room.ts
|
|
206
|
-
function roomVariables(room) {
|
|
207
|
-
const perimeter = 2 * (room.lengthM + room.widthM);
|
|
208
|
-
return {
|
|
209
|
-
length: room.lengthM,
|
|
210
|
-
width: room.widthM,
|
|
211
|
-
height: room.heightM,
|
|
212
|
-
floorArea: room.lengthM * room.widthM,
|
|
213
|
-
perimeter,
|
|
214
|
-
wallArea: perimeter * room.heightM,
|
|
215
|
-
heatedFloor: room.heatedFloor,
|
|
216
|
-
roomType: room.roomTypeCode
|
|
217
|
-
};
|
|
239
|
+
return [...(configuration.scope === "PARTIAL" ? data.packageItems.filter((item) => !item.includedInBase) : data.packageItems).map((item) => packageLine(configuration, item)), ...optionLines(configuration, data, "ENGINEERING")];
|
|
218
240
|
}
|
|
219
241
|
//#endregion
|
|
220
242
|
//#region src/formula/language.ts
|
|
@@ -648,13 +670,87 @@ function runNumericFormula(source, scope) {
|
|
|
648
670
|
};
|
|
649
671
|
}
|
|
650
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
|
|
651
747
|
//#region src/estimate/materials.ts
|
|
652
748
|
const isMaterialRule = (rule) => rule.quantityFormula !== null;
|
|
653
749
|
function selectRule(data, categoryId, materialTypeId) {
|
|
654
750
|
const rules = data.rules.filter(isMaterialRule);
|
|
655
751
|
return rules.find((rule) => rule.level === "MATERIAL_TYPE" && rule.categoryId === categoryId && rule.materialTypeId === materialTypeId) ?? rules.find((rule) => rule.level === "CATEGORY" && rule.categoryId === categoryId);
|
|
656
752
|
}
|
|
657
|
-
function materialLine(configuration, data, room, categoryId, productId) {
|
|
753
|
+
function materialLine(configuration, data, room, categoryId, productId, objectScope) {
|
|
658
754
|
const init = {
|
|
659
755
|
id: lineId("MATERIALS", room.clientId, categoryId),
|
|
660
756
|
section: "MATERIALS",
|
|
@@ -686,7 +782,7 @@ function materialLine(configuration, data, room, categoryId, productId) {
|
|
|
686
782
|
const variables = {
|
|
687
783
|
...roomVariables(room),
|
|
688
784
|
...productVariables(product, category),
|
|
689
|
-
...
|
|
785
|
+
...objectScope
|
|
690
786
|
};
|
|
691
787
|
const { coefficients } = data;
|
|
692
788
|
const ruled = {
|
|
@@ -707,32 +803,74 @@ function materialLine(configuration, data, room, categoryId, productId) {
|
|
|
707
803
|
coefficients
|
|
708
804
|
});
|
|
709
805
|
if (!cost.ok) return failedLine(ruled, "ERROR", cost.error);
|
|
710
|
-
|
|
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({
|
|
711
842
|
...ruled,
|
|
843
|
+
warnings: lineWarnings,
|
|
712
844
|
quantity: roundQuantity(quantity.value),
|
|
713
845
|
unit: product.unit,
|
|
714
|
-
unitPriceCents: product.priceCents
|
|
715
|
-
|
|
846
|
+
unitPriceCents: product.priceCents,
|
|
847
|
+
amountCents,
|
|
848
|
+
components
|
|
849
|
+
});
|
|
716
850
|
}
|
|
717
|
-
function materialLines(configuration, data) {
|
|
718
|
-
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
|
+
});
|
|
719
856
|
}
|
|
720
857
|
//#endregion
|
|
721
858
|
//#region src/estimate/object-rules.ts
|
|
722
|
-
function objectRuleLines(configuration, data) {
|
|
859
|
+
function objectRuleLines(configuration, data, section, variables) {
|
|
860
|
+
if (section === "BASE" && configuration.scope !== "FULL") return [];
|
|
723
861
|
const scope = {
|
|
724
|
-
variables
|
|
862
|
+
variables,
|
|
725
863
|
coefficients: data.coefficients
|
|
726
864
|
};
|
|
727
|
-
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) => {
|
|
728
866
|
const init = {
|
|
729
|
-
id: lineId(
|
|
730
|
-
section
|
|
867
|
+
id: lineId(section, rule.id),
|
|
868
|
+
section,
|
|
731
869
|
ruleId: rule.id
|
|
732
870
|
};
|
|
733
871
|
const outcome = runNumericFormula(rule.costFormula, scope);
|
|
734
872
|
return outcome.ok ? pricedLine(init, outcome.value) : failedLine(init, "ERROR", outcome.error);
|
|
735
|
-
});
|
|
873
|
+
}).filter((line) => section !== "BASE" || line.status !== "OK" || line.amountCents !== 0);
|
|
736
874
|
}
|
|
737
875
|
//#endregion
|
|
738
876
|
//#region src/schemas/common.ts
|
|
@@ -740,11 +878,17 @@ const LocalizedTextSchema = z.object({
|
|
|
740
878
|
en: z.string(),
|
|
741
879
|
uk: z.string()
|
|
742
880
|
});
|
|
743
|
-
const
|
|
881
|
+
const ScopeSchema = z.enum(["FULL", "PARTIAL"]);
|
|
882
|
+
const FinishLevelSchema = z.enum([
|
|
883
|
+
"ROUGH",
|
|
884
|
+
"PRE_FINISH",
|
|
885
|
+
"LIVED_IN"
|
|
886
|
+
]);
|
|
744
887
|
const RuleLevelSchema = z.enum([
|
|
745
888
|
"OBJECT",
|
|
746
889
|
"CATEGORY",
|
|
747
|
-
"MATERIAL_TYPE"
|
|
890
|
+
"MATERIAL_TYPE",
|
|
891
|
+
"SURFACE"
|
|
748
892
|
]);
|
|
749
893
|
const RateSourceSchema = z.enum(["NBU", "MANUAL"]);
|
|
750
894
|
const SurfaceKindSchema = z.enum([
|
|
@@ -771,9 +915,10 @@ const RoomTypeCodeSchema = z.enum([
|
|
|
771
915
|
"BEDROOM",
|
|
772
916
|
"KITCHEN",
|
|
773
917
|
"KITCHEN_LIVING",
|
|
774
|
-
"BATHROOM"
|
|
918
|
+
"BATHROOM",
|
|
919
|
+
"HALLWAY",
|
|
920
|
+
"STORAGE"
|
|
775
921
|
]);
|
|
776
|
-
const KitchenTypeSchema = z.enum(["SEPARATE", "KITCHEN_LIVING"]);
|
|
777
922
|
const CurrencySchema = z.enum(["USD", "UAH"]);
|
|
778
923
|
const SectionSchema = z.enum([
|
|
779
924
|
"BASE",
|
|
@@ -782,6 +927,11 @@ const SectionSchema = z.enum([
|
|
|
782
927
|
"ENGINEERING",
|
|
783
928
|
"ADDITIONAL"
|
|
784
929
|
]);
|
|
930
|
+
const LineComponentKindSchema = z.enum([
|
|
931
|
+
"MATERIAL",
|
|
932
|
+
"LABOR",
|
|
933
|
+
"PREP"
|
|
934
|
+
]);
|
|
785
935
|
const LineStatusSchema = z.enum([
|
|
786
936
|
"OK",
|
|
787
937
|
"INCLUDED",
|
|
@@ -825,11 +975,11 @@ function duplicateIndexes(items, key) {
|
|
|
825
975
|
});
|
|
826
976
|
}
|
|
827
977
|
const ConfigurationSchema = z.object({
|
|
978
|
+
scope: ScopeSchema.default("FULL"),
|
|
828
979
|
finishLevel: FinishLevelSchema,
|
|
829
|
-
totalAreaSqm: z.number().positive(),
|
|
980
|
+
totalAreaSqm: z.number().positive().nullable(),
|
|
830
981
|
balconyAreaSqm: z.number().min(0),
|
|
831
982
|
balconyTiles: z.boolean(),
|
|
832
|
-
kitchenType: KitchenTypeSchema,
|
|
833
983
|
rooms: z.array(RoomConfigurationSchema),
|
|
834
984
|
options: z.array(OptionSelectionSchema),
|
|
835
985
|
currency: CurrencySchema
|
|
@@ -839,6 +989,11 @@ const ConfigurationSchema = z.object({
|
|
|
839
989
|
path,
|
|
840
990
|
message: "Duplicate"
|
|
841
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
|
+
});
|
|
842
997
|
for (const index of duplicateIndexes(configuration.rooms, (room) => room.clientId)) addDuplicate([
|
|
843
998
|
"rooms",
|
|
844
999
|
index,
|
|
@@ -870,13 +1025,8 @@ const PricingLimitsSchema = z.object({
|
|
|
870
1025
|
totalAreaSqm: RangeSchema,
|
|
871
1026
|
rooms: RangeSchema,
|
|
872
1027
|
roomSideM: RangeSchema,
|
|
873
|
-
roomHeightM: RangeSchema
|
|
874
|
-
|
|
875
|
-
const BaseRateSchema = z.object({
|
|
876
|
-
finishLevel: FinishLevelSchema,
|
|
877
|
-
areaFromSqm: z.number().min(0),
|
|
878
|
-
areaToSqm: z.number(),
|
|
879
|
-
rateCentsPerSqm: z.int().positive()
|
|
1028
|
+
roomHeightM: RangeSchema,
|
|
1029
|
+
missingAreaHintSqm: z.number().min(0)
|
|
880
1030
|
});
|
|
881
1031
|
const PricingRuleSchema = z.object({
|
|
882
1032
|
id: z.string(),
|
|
@@ -885,9 +1035,16 @@ const PricingRuleSchema = z.object({
|
|
|
885
1035
|
materialTypeId: z.string().nullable(),
|
|
886
1036
|
name: LocalizedTextSchema,
|
|
887
1037
|
quantityFormula: z.string().nullable(),
|
|
1038
|
+
surface: z.enum([
|
|
1039
|
+
"FLOOR",
|
|
1040
|
+
"WALLS",
|
|
1041
|
+
"CEILING"
|
|
1042
|
+
]).nullable(),
|
|
1043
|
+
baseComponent: z.boolean(),
|
|
888
1044
|
costFormula: z.string().max(500),
|
|
1045
|
+
laborFormula: z.string().max(500).nullable(),
|
|
889
1046
|
sortOrder: z.int()
|
|
890
|
-
}).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"] });
|
|
891
1048
|
const PricingRoomTypeSchema = z.object({
|
|
892
1049
|
id: z.string(),
|
|
893
1050
|
code: z.string()
|
|
@@ -924,6 +1081,8 @@ const PricingOptionSchema = z.object({
|
|
|
924
1081
|
priceCents: CentsSchema,
|
|
925
1082
|
unit: OptionUnitSchema,
|
|
926
1083
|
roomTypeCodes: z.array(z.string()),
|
|
1084
|
+
priceCoefficientKeys: z.array(z.string()),
|
|
1085
|
+
scopes: z.array(ScopeSchema).min(1),
|
|
927
1086
|
minQuantity: z.int().nullable(),
|
|
928
1087
|
maxQuantity: z.int().nullable(),
|
|
929
1088
|
available: z.boolean()
|
|
@@ -932,7 +1091,6 @@ const PricingDataSchema = z.object({
|
|
|
932
1091
|
version: z.string().regex(/^[0-9a-f]{16}$/),
|
|
933
1092
|
exchangeRate: ExchangeRateSchema.nullable(),
|
|
934
1093
|
limits: PricingLimitsSchema,
|
|
935
|
-
baseRates: z.array(BaseRateSchema),
|
|
936
1094
|
coefficients: z.record(z.string(), z.number()),
|
|
937
1095
|
rules: z.array(PricingRuleSchema),
|
|
938
1096
|
roomTypes: z.array(PricingRoomTypeSchema),
|
|
@@ -948,6 +1106,12 @@ const EstimateMessageSchema = z.object({
|
|
|
948
1106
|
code: z.string(),
|
|
949
1107
|
params: z.record(z.string(), z.unknown()).optional()
|
|
950
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
|
+
});
|
|
951
1115
|
const EstimateLineSchema = z.object({
|
|
952
1116
|
id: z.string(),
|
|
953
1117
|
section: SectionSchema,
|
|
@@ -963,6 +1127,7 @@ const EstimateLineSchema = z.object({
|
|
|
963
1127
|
unitPriceCents: CentsSchema.nullable(),
|
|
964
1128
|
amountCents: CentsSchema,
|
|
965
1129
|
amountUahKopecks: CentsSchema.nullable(),
|
|
1130
|
+
components: z.array(LineComponentSchema).optional(),
|
|
966
1131
|
warnings: z.array(EstimateMessageSchema),
|
|
967
1132
|
error: EstimateMessageSchema.nullable()
|
|
968
1133
|
});
|
|
@@ -1000,19 +1165,32 @@ const RuleSampleSchema = z.object({
|
|
|
1000
1165
|
unit: ProductUnitSchema,
|
|
1001
1166
|
wastePercent: z.number().min(0)
|
|
1002
1167
|
}).optional(),
|
|
1168
|
+
surfaceArea: z.number().min(0).optional(),
|
|
1003
1169
|
object: z.object({
|
|
1004
|
-
|
|
1170
|
+
scope: ScopeSchema,
|
|
1171
|
+
totalAreaSqm: z.number().positive().nullable(),
|
|
1005
1172
|
balconyAreaSqm: z.number().min(0),
|
|
1006
1173
|
balconyTiles: z.boolean(),
|
|
1007
|
-
kitchenType: KitchenTypeSchema,
|
|
1008
1174
|
finishLevel: FinishLevelSchema,
|
|
1009
1175
|
roomsCount: z.int().min(0),
|
|
1010
|
-
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)
|
|
1011
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
|
+
});
|
|
1012
1189
|
});
|
|
1013
1190
|
const RulePreviewSchema = z.object({
|
|
1014
1191
|
quantity: z.number().nullable(),
|
|
1015
1192
|
amountCents: CentsSchema.nullable(),
|
|
1193
|
+
laborCents: CentsSchema.nullable(),
|
|
1016
1194
|
variables: z.record(z.string(), z.union([
|
|
1017
1195
|
z.number(),
|
|
1018
1196
|
z.boolean(),
|
|
@@ -1047,20 +1225,28 @@ function isIncomplete(lines) {
|
|
|
1047
1225
|
}
|
|
1048
1226
|
//#endregion
|
|
1049
1227
|
//#region src/estimate/warnings.ts
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
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 [{
|
|
1053
1234
|
code: "ROOMS_AREA_EXCEEDS_TOTAL",
|
|
1054
1235
|
params: {
|
|
1055
1236
|
roomsAreaSqm,
|
|
1056
|
-
totalAreaSqm
|
|
1237
|
+
totalAreaSqm
|
|
1057
1238
|
}
|
|
1239
|
+
}];
|
|
1240
|
+
const missingSqm = roundArea(totalAreaSqm - roomsAreaSqm);
|
|
1241
|
+
return missingSqm > limits.missingAreaHintSqm ? [{
|
|
1242
|
+
code: "ROOMS_AREA_BELOW_TOTAL",
|
|
1243
|
+
params: { missingSqm }
|
|
1058
1244
|
}] : [];
|
|
1059
1245
|
}
|
|
1060
|
-
function estimateWarnings(lines, configuration, rate) {
|
|
1246
|
+
function estimateWarnings(lines, configuration, rate, limits) {
|
|
1061
1247
|
return [
|
|
1062
1248
|
...collectWarnings(lines),
|
|
1063
|
-
...roomsAreaWarnings(configuration),
|
|
1249
|
+
...roomsAreaWarnings(configuration, limits),
|
|
1064
1250
|
...configuration.currency === "UAH" && rate === null ? [{ code: "EXCHANGE_RATE_UNAVAILABLE" }] : []
|
|
1065
1251
|
];
|
|
1066
1252
|
}
|
|
@@ -1074,10 +1260,11 @@ function withKopecks(line, rate) {
|
|
|
1074
1260
|
}
|
|
1075
1261
|
function estimate(configuration, data) {
|
|
1076
1262
|
const rate = data.exchangeRate?.rate ?? null;
|
|
1263
|
+
const variables = objectVariables(configuration);
|
|
1077
1264
|
const lines = [
|
|
1078
|
-
|
|
1079
|
-
...objectRuleLines(configuration, data),
|
|
1080
|
-
...materialLines(configuration, data),
|
|
1265
|
+
...objectRuleLines(configuration, data, "BASE", variables),
|
|
1266
|
+
...objectRuleLines(configuration, data, "OBJECT", variables),
|
|
1267
|
+
...materialLines(configuration, data, variables),
|
|
1081
1268
|
...engineeringLines(configuration, data),
|
|
1082
1269
|
...additionalLines(configuration, data)
|
|
1083
1270
|
].map((line) => withKopecks(line, rate));
|
|
@@ -1087,7 +1274,7 @@ function estimate(configuration, data) {
|
|
|
1087
1274
|
exchangeRate: data.exchangeRate,
|
|
1088
1275
|
lines,
|
|
1089
1276
|
totals: computeTotals(lines, rate !== null),
|
|
1090
|
-
warnings: estimateWarnings(lines, configuration, rate),
|
|
1277
|
+
warnings: estimateWarnings(lines, configuration, rate, data.limits),
|
|
1091
1278
|
incomplete: isIncomplete(lines)
|
|
1092
1279
|
};
|
|
1093
1280
|
}
|
|
@@ -1102,8 +1289,9 @@ function checkLimits(configuration, limits, options = []) {
|
|
|
1102
1289
|
max
|
|
1103
1290
|
});
|
|
1104
1291
|
};
|
|
1105
|
-
|
|
1106
|
-
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);
|
|
1107
1295
|
check("rooms", configuration.rooms.length, limits.rooms.min, limits.rooms.max);
|
|
1108
1296
|
configuration.rooms.forEach((room, index) => {
|
|
1109
1297
|
const path = `rooms[${index}]`;
|
|
@@ -1159,18 +1347,33 @@ function referencesOf(sources) {
|
|
|
1159
1347
|
}
|
|
1160
1348
|
function objectScope({ object }) {
|
|
1161
1349
|
return {
|
|
1162
|
-
|
|
1350
|
+
scope: object.scope,
|
|
1351
|
+
totalArea: totalAreaOf(object.scope, object.totalAreaSqm, object.roomsFloorArea),
|
|
1163
1352
|
balconyArea: object.balconyAreaSqm,
|
|
1164
1353
|
balconyTiles: object.balconyTiles,
|
|
1165
|
-
kitchenType: object.kitchenType,
|
|
1166
1354
|
finishLevel: object.finishLevel,
|
|
1167
1355
|
roomsCount: object.roomsCount,
|
|
1168
|
-
bathroomsCount: object.bathroomsCount
|
|
1356
|
+
bathroomsCount: object.bathroomsCount,
|
|
1357
|
+
roomsFloorArea: object.roomsFloorArea,
|
|
1358
|
+
roomsWallArea: object.roomsWallArea,
|
|
1359
|
+
roomsPerimeter: object.roomsPerimeter,
|
|
1360
|
+
wetRoomsCount: object.wetRoomsCount
|
|
1169
1361
|
};
|
|
1170
1362
|
}
|
|
1171
1363
|
function scopeOf(level, sample) {
|
|
1172
1364
|
if (level === "OBJECT") return { scope: objectScope(sample) };
|
|
1173
|
-
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
|
+
}
|
|
1174
1377
|
if (room === void 0 || product === void 0) return { error: {
|
|
1175
1378
|
code: "SAMPLE_INCOMPLETE",
|
|
1176
1379
|
params: { missing: room === void 0 ? "room" : "product" }
|
|
@@ -1186,15 +1389,21 @@ function pick(source, names) {
|
|
|
1186
1389
|
}
|
|
1187
1390
|
function previewRule(input) {
|
|
1188
1391
|
const { level, quantityFormula, costFormula, coefficients } = input;
|
|
1189
|
-
const
|
|
1190
|
-
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) => ({
|
|
1191
1399
|
quantity,
|
|
1192
1400
|
amountCents,
|
|
1401
|
+
laborCents,
|
|
1193
1402
|
variables: pick(variables, references.variables),
|
|
1194
1403
|
coefficients: pick(coefficients, references.coefficients),
|
|
1195
1404
|
error
|
|
1196
1405
|
});
|
|
1197
|
-
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" });
|
|
1198
1407
|
const prepared = scopeOf(level, input.sample);
|
|
1199
1408
|
if ("error" in prepared) return report({}, null, null, prepared.error);
|
|
1200
1409
|
let variables = prepared.scope;
|
|
@@ -1220,7 +1429,15 @@ function previewRule(input) {
|
|
|
1220
1429
|
id: "preview",
|
|
1221
1430
|
section: "OBJECT"
|
|
1222
1431
|
}, cost.value);
|
|
1223
|
-
|
|
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);
|
|
1224
1441
|
}
|
|
1225
1442
|
//#endregion
|
|
1226
1443
|
//#region src/formula/suggest.ts
|
|
@@ -1290,13 +1507,18 @@ const FORMULA_SCOPE_VARIABLES = {
|
|
|
1290
1507
|
number("totalArea"),
|
|
1291
1508
|
number("balconyArea"),
|
|
1292
1509
|
boolean("balconyTiles"),
|
|
1293
|
-
enumeration("kitchenType", KitchenTypeSchema.options),
|
|
1294
1510
|
enumeration("finishLevel", FinishLevelSchema.options),
|
|
1295
1511
|
number("roomsCount"),
|
|
1296
|
-
number("bathroomsCount")
|
|
1512
|
+
number("bathroomsCount"),
|
|
1513
|
+
enumeration("scope", ScopeSchema.options),
|
|
1514
|
+
number("roomsFloorArea"),
|
|
1515
|
+
number("roomsWallArea"),
|
|
1516
|
+
number("roomsPerimeter"),
|
|
1517
|
+
number("wetRoomsCount")
|
|
1297
1518
|
]
|
|
1298
1519
|
};
|
|
1299
1520
|
const FORMULA_QUANTITY_VARIABLE = number("quantity");
|
|
1521
|
+
const FORMULA_SURFACE_AREA_VARIABLE = number("surfaceArea");
|
|
1300
1522
|
const FORMULA_VARIABLES = {
|
|
1301
1523
|
OBJECT: FORMULA_SCOPE_VARIABLES.object,
|
|
1302
1524
|
CATEGORY: [
|
|
@@ -1308,9 +1530,14 @@ const FORMULA_VARIABLES = {
|
|
|
1308
1530
|
...FORMULA_SCOPE_VARIABLES.room,
|
|
1309
1531
|
...FORMULA_SCOPE_VARIABLES.product,
|
|
1310
1532
|
...FORMULA_SCOPE_VARIABLES.object
|
|
1533
|
+
],
|
|
1534
|
+
SURFACE: [
|
|
1535
|
+
...FORMULA_SCOPE_VARIABLES.room,
|
|
1536
|
+
...FORMULA_SCOPE_VARIABLES.object,
|
|
1537
|
+
FORMULA_SURFACE_AREA_VARIABLE
|
|
1311
1538
|
]
|
|
1312
1539
|
};
|
|
1313
|
-
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));
|
|
1314
1541
|
function span(node) {
|
|
1315
1542
|
return {
|
|
1316
1543
|
position: node.start,
|
|
@@ -1334,7 +1561,7 @@ function validateFormula(input) {
|
|
|
1334
1561
|
const knownCoefficients = new Set(input.coefficientKeys);
|
|
1335
1562
|
const usedCoefficients = /* @__PURE__ */ new Set();
|
|
1336
1563
|
const errors = [];
|
|
1337
|
-
const quantityAllowed = input.kind
|
|
1564
|
+
const quantityAllowed = input.kind !== "quantity" && (input.level === "CATEGORY" || input.level === "MATERIAL_TYPE");
|
|
1338
1565
|
function mismatch(node, expected) {
|
|
1339
1566
|
errors.push({
|
|
1340
1567
|
code: "FORMULA_SYNTAX",
|
|
@@ -1522,4 +1749,4 @@ function validateFormula(input) {
|
|
|
1522
1749
|
};
|
|
1523
1750
|
}
|
|
1524
1751
|
//#endregion
|
|
1525
|
-
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 };
|