@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.cjs
CHANGED
|
@@ -51,6 +51,66 @@ function toCents(dollars) {
|
|
|
51
51
|
return cents;
|
|
52
52
|
}
|
|
53
53
|
//#endregion
|
|
54
|
+
//#region src/variables/room.ts
|
|
55
|
+
function roomVariables(room) {
|
|
56
|
+
const perimeter = 2 * (room.lengthM + room.widthM);
|
|
57
|
+
return {
|
|
58
|
+
length: room.lengthM,
|
|
59
|
+
width: room.widthM,
|
|
60
|
+
height: room.heightM,
|
|
61
|
+
floorArea: room.lengthM * room.widthM,
|
|
62
|
+
perimeter,
|
|
63
|
+
wallArea: perimeter * room.heightM,
|
|
64
|
+
heatedFloor: room.heatedFloor,
|
|
65
|
+
roomType: room.roomTypeCode
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region src/variables/object.ts
|
|
70
|
+
const COUNTED_AS_ROOMS = [
|
|
71
|
+
"LIVING_ROOM",
|
|
72
|
+
"BEDROOM",
|
|
73
|
+
"KITCHEN_LIVING"
|
|
74
|
+
];
|
|
75
|
+
const WET_ROOMS = [
|
|
76
|
+
"BATHROOM",
|
|
77
|
+
"KITCHEN",
|
|
78
|
+
"KITCHEN_LIVING"
|
|
79
|
+
];
|
|
80
|
+
function totalAreaOf(scope, totalAreaSqm, roomsFloorArea) {
|
|
81
|
+
return scope === "PARTIAL" || totalAreaSqm === null ? roomsFloorArea : totalAreaSqm;
|
|
82
|
+
}
|
|
83
|
+
function objectVariables(configuration) {
|
|
84
|
+
const rooms = configuration.rooms.map(roomVariables);
|
|
85
|
+
const sum = (pick) => rooms.reduce((total, room) => total + pick(room), 0);
|
|
86
|
+
const roomsFloorArea = sum((room) => room.floorArea);
|
|
87
|
+
return {
|
|
88
|
+
scope: configuration.scope,
|
|
89
|
+
totalArea: totalAreaOf(configuration.scope, configuration.totalAreaSqm, roomsFloorArea),
|
|
90
|
+
balconyArea: configuration.balconyAreaSqm,
|
|
91
|
+
balconyTiles: configuration.balconyTiles,
|
|
92
|
+
finishLevel: configuration.finishLevel,
|
|
93
|
+
roomsCount: configuration.rooms.filter((room) => COUNTED_AS_ROOMS.includes(room.roomTypeCode)).length,
|
|
94
|
+
bathroomsCount: configuration.rooms.filter((room) => room.roomTypeCode === "BATHROOM").length,
|
|
95
|
+
roomsFloorArea,
|
|
96
|
+
roomsWallArea: sum((room) => room.wallArea),
|
|
97
|
+
roomsPerimeter: sum((room) => room.perimeter),
|
|
98
|
+
wetRoomsCount: rooms.filter((room) => WET_ROOMS.includes(room.roomType)).length
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/variables/product.ts
|
|
103
|
+
function productVariablesOf(product, wastePercent) {
|
|
104
|
+
return {
|
|
105
|
+
price: product.priceCents / 100,
|
|
106
|
+
waste: wastePercent / 100,
|
|
107
|
+
unit: product.unit
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function productVariables(product, category) {
|
|
111
|
+
return productVariablesOf(product, product.wastePercentOverride ?? category.wastePercent);
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
54
114
|
//#region src/estimate/line.ts
|
|
55
115
|
const MAX_LINE_CENTS = 1e10;
|
|
56
116
|
function createLine(init) {
|
|
@@ -80,18 +140,25 @@ function failedLine(init, status, error) {
|
|
|
80
140
|
});
|
|
81
141
|
}
|
|
82
142
|
function pricedLine(init, dollars) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
params: { maxCents: MAX_LINE_CENTS }
|
|
86
|
-
});
|
|
87
|
-
return createLine({
|
|
143
|
+
const amountCents = componentCents(dollars);
|
|
144
|
+
return amountCents === null ? failedLine(init, "ERROR", outOfRange()) : createLine({
|
|
88
145
|
...init,
|
|
89
|
-
amountCents
|
|
146
|
+
amountCents
|
|
90
147
|
});
|
|
91
148
|
}
|
|
92
149
|
function roundQuantity(quantity) {
|
|
93
150
|
return Math.round(quantity * 1e3) / 1e3;
|
|
94
151
|
}
|
|
152
|
+
function componentCents(dollars) {
|
|
153
|
+
if (!Number.isFinite(dollars) || dollars < 0 || dollars * 100 > 1e10) return null;
|
|
154
|
+
return toCents(dollars);
|
|
155
|
+
}
|
|
156
|
+
function outOfRange() {
|
|
157
|
+
return {
|
|
158
|
+
code: "AMOUNT_OUT_OF_RANGE",
|
|
159
|
+
params: { maxCents: MAX_LINE_CENTS }
|
|
160
|
+
};
|
|
161
|
+
}
|
|
95
162
|
//#endregion
|
|
96
163
|
//#region src/estimate/line-id.ts
|
|
97
164
|
function lineId(section, ...parts) {
|
|
@@ -107,16 +174,39 @@ function unitQuantity(unit, rooms, pieces) {
|
|
|
107
174
|
case "ROOM_SQM": return rooms.reduce((sum, room) => sum + room.lengthM * room.widthM, 0);
|
|
108
175
|
}
|
|
109
176
|
}
|
|
110
|
-
function
|
|
177
|
+
function optionUnitPriceCents(option, coefficients) {
|
|
178
|
+
if (option.priceCoefficientKeys.length === 0) return option.priceCents;
|
|
179
|
+
let dollars = 0;
|
|
180
|
+
for (const key of option.priceCoefficientKeys) {
|
|
181
|
+
const value = Object.hasOwn(coefficients, key) ? coefficients[key] : void 0;
|
|
182
|
+
if (value === void 0) return { missingKey: key };
|
|
183
|
+
dollars += value;
|
|
184
|
+
}
|
|
185
|
+
return componentCents(dollars);
|
|
186
|
+
}
|
|
187
|
+
function optionLine(configuration, option, selection, coefficients = {}) {
|
|
111
188
|
const init = {
|
|
112
189
|
id: lineId(option.kind, "option", option.id),
|
|
113
190
|
section: option.kind,
|
|
114
191
|
optionId: option.id
|
|
115
192
|
};
|
|
193
|
+
if (!option.scopes.includes(configuration.scope)) return failedLine(init, "UNAVAILABLE", {
|
|
194
|
+
code: "OPTION_SCOPE_NOT_ALLOWED",
|
|
195
|
+
params: {
|
|
196
|
+
optionId: option.id,
|
|
197
|
+
scope: configuration.scope
|
|
198
|
+
}
|
|
199
|
+
});
|
|
116
200
|
if (!option.available) return failedLine(init, "UNAVAILABLE", {
|
|
117
201
|
code: "PRODUCT_UNAVAILABLE",
|
|
118
202
|
params: { optionId: option.id }
|
|
119
203
|
});
|
|
204
|
+
const unitPriceCents = optionUnitPriceCents(option, coefficients);
|
|
205
|
+
if (unitPriceCents === null) return failedLine(init, "ERROR", outOfRange());
|
|
206
|
+
if (typeof unitPriceCents !== "number") return failedLine(init, "ERROR", {
|
|
207
|
+
code: "UNKNOWN_COEFFICIENT",
|
|
208
|
+
params: { key: unitPriceCents.missingKey }
|
|
209
|
+
});
|
|
120
210
|
const selected = configuration.rooms.filter((room) => selection.roomClientIds?.includes(room.clientId));
|
|
121
211
|
const allowed = option.roomTypeCodes.length === 0 ? selected : selected.filter((room) => option.roomTypeCodes.includes(room.roomTypeCode));
|
|
122
212
|
const rejected = selected.filter((room) => !allowed.includes(room));
|
|
@@ -125,7 +215,7 @@ function optionLine(configuration, option, selection) {
|
|
|
125
215
|
...init,
|
|
126
216
|
quantity: roundQuantity(quantity),
|
|
127
217
|
unit: option.unit,
|
|
128
|
-
unitPriceCents
|
|
218
|
+
unitPriceCents,
|
|
129
219
|
warnings: rejected.map((room) => ({
|
|
130
220
|
code: "OPTION_ROOM_TYPE_NOT_ALLOWED",
|
|
131
221
|
params: {
|
|
@@ -133,12 +223,12 @@ function optionLine(configuration, option, selection) {
|
|
|
133
223
|
roomTypeCode: room.roomTypeCode
|
|
134
224
|
}
|
|
135
225
|
}))
|
|
136
|
-
},
|
|
226
|
+
}, unitPriceCents / 100 * quantity);
|
|
137
227
|
}
|
|
138
228
|
function optionLines(configuration, data, kind) {
|
|
139
229
|
return configuration.options.flatMap((selection) => {
|
|
140
230
|
const option = data.options.find((item) => item.id === selection.optionId);
|
|
141
|
-
return option?.kind === kind ? [optionLine(configuration, option, selection)] : [];
|
|
231
|
+
return option?.kind === kind ? [optionLine(configuration, option, selection, data.coefficients)] : [];
|
|
142
232
|
});
|
|
143
233
|
}
|
|
144
234
|
//#endregion
|
|
@@ -147,29 +237,6 @@ function additionalLines(configuration, data) {
|
|
|
147
237
|
return optionLines(configuration, data, "ADDITIONAL");
|
|
148
238
|
}
|
|
149
239
|
//#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
240
|
//#region src/estimate/engineering.ts
|
|
174
241
|
function packageLine(configuration, item) {
|
|
175
242
|
const init = {
|
|
@@ -194,52 +261,7 @@ function packageLine(configuration, item) {
|
|
|
194
261
|
}, item.priceCents / 100 * quantity);
|
|
195
262
|
}
|
|
196
263
|
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
|
-
kitchenType: configuration.kitchenType,
|
|
212
|
-
finishLevel: configuration.finishLevel,
|
|
213
|
-
roomsCount: configuration.rooms.filter((room) => COUNTED_AS_ROOMS.includes(room.roomTypeCode)).length,
|
|
214
|
-
bathroomsCount: configuration.rooms.filter((room) => room.roomTypeCode === "BATHROOM").length
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
//#endregion
|
|
218
|
-
//#region src/variables/product.ts
|
|
219
|
-
function productVariablesOf(product, wastePercent) {
|
|
220
|
-
return {
|
|
221
|
-
price: product.priceCents / 100,
|
|
222
|
-
waste: wastePercent / 100,
|
|
223
|
-
unit: product.unit
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
function productVariables(product, category) {
|
|
227
|
-
return productVariablesOf(product, product.wastePercentOverride ?? category.wastePercent);
|
|
228
|
-
}
|
|
229
|
-
//#endregion
|
|
230
|
-
//#region src/variables/room.ts
|
|
231
|
-
function roomVariables(room) {
|
|
232
|
-
const perimeter = 2 * (room.lengthM + room.widthM);
|
|
233
|
-
return {
|
|
234
|
-
length: room.lengthM,
|
|
235
|
-
width: room.widthM,
|
|
236
|
-
height: room.heightM,
|
|
237
|
-
floorArea: room.lengthM * room.widthM,
|
|
238
|
-
perimeter,
|
|
239
|
-
wallArea: perimeter * room.heightM,
|
|
240
|
-
heatedFloor: room.heatedFloor,
|
|
241
|
-
roomType: room.roomTypeCode
|
|
242
|
-
};
|
|
264
|
+
return [...(configuration.scope === "PARTIAL" ? data.packageItems.filter((item) => !item.includedInBase) : data.packageItems).map((item) => packageLine(configuration, item)), ...optionLines(configuration, data, "ENGINEERING")];
|
|
243
265
|
}
|
|
244
266
|
//#endregion
|
|
245
267
|
//#region src/formula/language.ts
|
|
@@ -673,13 +695,87 @@ function runNumericFormula(source, scope) {
|
|
|
673
695
|
};
|
|
674
696
|
}
|
|
675
697
|
//#endregion
|
|
698
|
+
//#region src/estimate/surfaces.ts
|
|
699
|
+
function surfaceRuleOf(data, surface) {
|
|
700
|
+
return data.rules.filter((rule) => rule.level === "SURFACE" && rule.surface === surface).sort((a, b) => a.sortOrder - b.sortOrder)[0];
|
|
701
|
+
}
|
|
702
|
+
function failedWith(line, error) {
|
|
703
|
+
return failedLine({
|
|
704
|
+
id: line.id,
|
|
705
|
+
section: line.section,
|
|
706
|
+
roomClientId: line.roomClientId,
|
|
707
|
+
categoryId: line.categoryId,
|
|
708
|
+
productId: line.productId,
|
|
709
|
+
ruleId: line.ruleId,
|
|
710
|
+
warnings: line.warnings
|
|
711
|
+
}, "ERROR", error);
|
|
712
|
+
}
|
|
713
|
+
function withComponent(line, component) {
|
|
714
|
+
const components = [...line.components ?? [], component];
|
|
715
|
+
const amountCents = components.reduce((sum, item) => sum + item.amountCents, 0);
|
|
716
|
+
if (amountCents > 1e10) return failedWith(line, outOfRange());
|
|
717
|
+
return {
|
|
718
|
+
...line,
|
|
719
|
+
components,
|
|
720
|
+
amountCents
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
function applySurfacePrep(data, room, lines, objectScope) {
|
|
724
|
+
const result = [...lines];
|
|
725
|
+
const variables = {
|
|
726
|
+
...roomVariables(room),
|
|
727
|
+
...objectScope
|
|
728
|
+
};
|
|
729
|
+
const areas = {
|
|
730
|
+
FLOOR: variables.floorArea,
|
|
731
|
+
CEILING: variables.floorArea,
|
|
732
|
+
WALLS: variables.wallArea
|
|
733
|
+
};
|
|
734
|
+
const surfaces = /* @__PURE__ */ new Map();
|
|
735
|
+
lines.forEach((line, index) => {
|
|
736
|
+
const surface = data.categories.find((category) => category.id === line.categoryId)?.surface;
|
|
737
|
+
if (line.status === "OK" && surface !== void 0 && surface !== "NONE") surfaces.set(surface, [...surfaces.get(surface) ?? [], index]);
|
|
738
|
+
});
|
|
739
|
+
for (const [surface, indexes] of surfaces) {
|
|
740
|
+
const rule = surfaceRuleOf(data, surface);
|
|
741
|
+
if (rule === void 0) continue;
|
|
742
|
+
const outcome = runNumericFormula(rule.costFormula, {
|
|
743
|
+
variables: {
|
|
744
|
+
...variables,
|
|
745
|
+
surfaceArea: areas[surface]
|
|
746
|
+
},
|
|
747
|
+
coefficients: data.coefficients
|
|
748
|
+
});
|
|
749
|
+
const cents = outcome.ok ? componentCents(outcome.value) : null;
|
|
750
|
+
const carrierCategoryId = lines[indexes[0]]?.categoryId ?? null;
|
|
751
|
+
indexes.forEach((index, position) => {
|
|
752
|
+
const line = lines[index];
|
|
753
|
+
if (!outcome.ok || cents === null) {
|
|
754
|
+
result[index] = failedWith(line, outcome.ok ? outOfRange() : outcome.error);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
result[index] = withComponent(line, position === 0 ? {
|
|
758
|
+
kind: "PREP",
|
|
759
|
+
ruleId: rule.id,
|
|
760
|
+
amountCents: cents
|
|
761
|
+
} : {
|
|
762
|
+
kind: "PREP",
|
|
763
|
+
ruleId: rule.id,
|
|
764
|
+
amountCents: 0,
|
|
765
|
+
sharedWithCategoryId: carrierCategoryId ?? void 0
|
|
766
|
+
});
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
return result;
|
|
770
|
+
}
|
|
771
|
+
//#endregion
|
|
676
772
|
//#region src/estimate/materials.ts
|
|
677
773
|
const isMaterialRule = (rule) => rule.quantityFormula !== null;
|
|
678
774
|
function selectRule(data, categoryId, materialTypeId) {
|
|
679
775
|
const rules = data.rules.filter(isMaterialRule);
|
|
680
776
|
return rules.find((rule) => rule.level === "MATERIAL_TYPE" && rule.categoryId === categoryId && rule.materialTypeId === materialTypeId) ?? rules.find((rule) => rule.level === "CATEGORY" && rule.categoryId === categoryId);
|
|
681
777
|
}
|
|
682
|
-
function materialLine(configuration, data, room, categoryId, productId) {
|
|
778
|
+
function materialLine(configuration, data, room, categoryId, productId, objectScope) {
|
|
683
779
|
const init = {
|
|
684
780
|
id: lineId("MATERIALS", room.clientId, categoryId),
|
|
685
781
|
section: "MATERIALS",
|
|
@@ -711,7 +807,7 @@ function materialLine(configuration, data, room, categoryId, productId) {
|
|
|
711
807
|
const variables = {
|
|
712
808
|
...roomVariables(room),
|
|
713
809
|
...productVariables(product, category),
|
|
714
|
-
...
|
|
810
|
+
...objectScope
|
|
715
811
|
};
|
|
716
812
|
const { coefficients } = data;
|
|
717
813
|
const ruled = {
|
|
@@ -732,32 +828,74 @@ function materialLine(configuration, data, room, categoryId, productId) {
|
|
|
732
828
|
coefficients
|
|
733
829
|
});
|
|
734
830
|
if (!cost.ok) return failedLine(ruled, "ERROR", cost.error);
|
|
735
|
-
|
|
831
|
+
const materialCents = componentCents(cost.value);
|
|
832
|
+
if (materialCents === null) return failedLine(ruled, "ERROR", outOfRange());
|
|
833
|
+
const components = [{
|
|
834
|
+
kind: "MATERIAL",
|
|
835
|
+
ruleId: rule.id,
|
|
836
|
+
amountCents: materialCents
|
|
837
|
+
}];
|
|
838
|
+
const lineWarnings = [...warnings];
|
|
839
|
+
if (configuration.scope === "PARTIAL") {
|
|
840
|
+
let laborCents = 0;
|
|
841
|
+
if (rule.laborFormula === null) lineWarnings.push({
|
|
842
|
+
code: "LABOR_RATE_MISSING",
|
|
843
|
+
params: { categoryId }
|
|
844
|
+
});
|
|
845
|
+
else {
|
|
846
|
+
const labor = runNumericFormula(rule.laborFormula, {
|
|
847
|
+
variables: {
|
|
848
|
+
...variables,
|
|
849
|
+
quantity: quantity.value
|
|
850
|
+
},
|
|
851
|
+
coefficients
|
|
852
|
+
});
|
|
853
|
+
if (!labor.ok) return failedLine(ruled, "ERROR", labor.error);
|
|
854
|
+
const cents = componentCents(labor.value);
|
|
855
|
+
if (cents === null) return failedLine(ruled, "ERROR", outOfRange());
|
|
856
|
+
laborCents = cents;
|
|
857
|
+
}
|
|
858
|
+
components.push({
|
|
859
|
+
kind: "LABOR",
|
|
860
|
+
ruleId: rule.id,
|
|
861
|
+
amountCents: laborCents
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
const amountCents = components.reduce((sum, component) => sum + component.amountCents, 0);
|
|
865
|
+
if (amountCents > 1e10) return failedLine(ruled, "ERROR", outOfRange());
|
|
866
|
+
return createLine({
|
|
736
867
|
...ruled,
|
|
868
|
+
warnings: lineWarnings,
|
|
737
869
|
quantity: roundQuantity(quantity.value),
|
|
738
870
|
unit: product.unit,
|
|
739
|
-
unitPriceCents: product.priceCents
|
|
740
|
-
|
|
871
|
+
unitPriceCents: product.priceCents,
|
|
872
|
+
amountCents,
|
|
873
|
+
components
|
|
874
|
+
});
|
|
741
875
|
}
|
|
742
|
-
function materialLines(configuration, data) {
|
|
743
|
-
return configuration.rooms.flatMap((room) =>
|
|
876
|
+
function materialLines(configuration, data, objectScope) {
|
|
877
|
+
return configuration.rooms.flatMap((room) => {
|
|
878
|
+
const lines = room.selections.flatMap(({ categoryId, productId }) => productId === null ? [] : [materialLine(configuration, data, room, categoryId, productId, objectScope)]);
|
|
879
|
+
return configuration.scope === "PARTIAL" ? applySurfacePrep(data, room, lines, objectScope) : lines;
|
|
880
|
+
});
|
|
744
881
|
}
|
|
745
882
|
//#endregion
|
|
746
883
|
//#region src/estimate/object-rules.ts
|
|
747
|
-
function objectRuleLines(configuration, data) {
|
|
884
|
+
function objectRuleLines(configuration, data, section, variables) {
|
|
885
|
+
if (section === "BASE" && configuration.scope !== "FULL") return [];
|
|
748
886
|
const scope = {
|
|
749
|
-
variables
|
|
887
|
+
variables,
|
|
750
888
|
coefficients: data.coefficients
|
|
751
889
|
};
|
|
752
|
-
return data.rules.filter((rule) => rule.level === "OBJECT").sort((a, b) => a.sortOrder - b.sortOrder).map((rule) => {
|
|
890
|
+
return data.rules.filter((rule) => rule.level === "OBJECT" && rule.baseComponent === (section === "BASE")).sort((a, b) => a.sortOrder - b.sortOrder).map((rule) => {
|
|
753
891
|
const init = {
|
|
754
|
-
id: lineId(
|
|
755
|
-
section
|
|
892
|
+
id: lineId(section, rule.id),
|
|
893
|
+
section,
|
|
756
894
|
ruleId: rule.id
|
|
757
895
|
};
|
|
758
896
|
const outcome = runNumericFormula(rule.costFormula, scope);
|
|
759
897
|
return outcome.ok ? pricedLine(init, outcome.value) : failedLine(init, "ERROR", outcome.error);
|
|
760
|
-
});
|
|
898
|
+
}).filter((line) => section !== "BASE" || line.status !== "OK" || line.amountCents !== 0);
|
|
761
899
|
}
|
|
762
900
|
//#endregion
|
|
763
901
|
//#region src/schemas/common.ts
|
|
@@ -765,11 +903,17 @@ const LocalizedTextSchema = zod.z.object({
|
|
|
765
903
|
en: zod.z.string(),
|
|
766
904
|
uk: zod.z.string()
|
|
767
905
|
});
|
|
768
|
-
const
|
|
906
|
+
const ScopeSchema = zod.z.enum(["FULL", "PARTIAL"]);
|
|
907
|
+
const FinishLevelSchema = zod.z.enum([
|
|
908
|
+
"ROUGH",
|
|
909
|
+
"PRE_FINISH",
|
|
910
|
+
"LIVED_IN"
|
|
911
|
+
]);
|
|
769
912
|
const RuleLevelSchema = zod.z.enum([
|
|
770
913
|
"OBJECT",
|
|
771
914
|
"CATEGORY",
|
|
772
|
-
"MATERIAL_TYPE"
|
|
915
|
+
"MATERIAL_TYPE",
|
|
916
|
+
"SURFACE"
|
|
773
917
|
]);
|
|
774
918
|
const RateSourceSchema = zod.z.enum(["NBU", "MANUAL"]);
|
|
775
919
|
const SurfaceKindSchema = zod.z.enum([
|
|
@@ -796,9 +940,10 @@ const RoomTypeCodeSchema = zod.z.enum([
|
|
|
796
940
|
"BEDROOM",
|
|
797
941
|
"KITCHEN",
|
|
798
942
|
"KITCHEN_LIVING",
|
|
799
|
-
"BATHROOM"
|
|
943
|
+
"BATHROOM",
|
|
944
|
+
"HALLWAY",
|
|
945
|
+
"STORAGE"
|
|
800
946
|
]);
|
|
801
|
-
const KitchenTypeSchema = zod.z.enum(["SEPARATE", "KITCHEN_LIVING"]);
|
|
802
947
|
const CurrencySchema = zod.z.enum(["USD", "UAH"]);
|
|
803
948
|
const SectionSchema = zod.z.enum([
|
|
804
949
|
"BASE",
|
|
@@ -807,6 +952,11 @@ const SectionSchema = zod.z.enum([
|
|
|
807
952
|
"ENGINEERING",
|
|
808
953
|
"ADDITIONAL"
|
|
809
954
|
]);
|
|
955
|
+
const LineComponentKindSchema = zod.z.enum([
|
|
956
|
+
"MATERIAL",
|
|
957
|
+
"LABOR",
|
|
958
|
+
"PREP"
|
|
959
|
+
]);
|
|
810
960
|
const LineStatusSchema = zod.z.enum([
|
|
811
961
|
"OK",
|
|
812
962
|
"INCLUDED",
|
|
@@ -850,11 +1000,11 @@ function duplicateIndexes(items, key) {
|
|
|
850
1000
|
});
|
|
851
1001
|
}
|
|
852
1002
|
const ConfigurationSchema = zod.z.object({
|
|
1003
|
+
scope: ScopeSchema.default("FULL"),
|
|
853
1004
|
finishLevel: FinishLevelSchema,
|
|
854
|
-
totalAreaSqm: zod.z.number().positive(),
|
|
1005
|
+
totalAreaSqm: zod.z.number().positive().nullable(),
|
|
855
1006
|
balconyAreaSqm: zod.z.number().min(0),
|
|
856
1007
|
balconyTiles: zod.z.boolean(),
|
|
857
|
-
kitchenType: KitchenTypeSchema,
|
|
858
1008
|
rooms: zod.z.array(RoomConfigurationSchema),
|
|
859
1009
|
options: zod.z.array(OptionSelectionSchema),
|
|
860
1010
|
currency: CurrencySchema
|
|
@@ -864,6 +1014,11 @@ const ConfigurationSchema = zod.z.object({
|
|
|
864
1014
|
path,
|
|
865
1015
|
message: "Duplicate"
|
|
866
1016
|
});
|
|
1017
|
+
if (configuration.scope === "FULL" !== (configuration.totalAreaSqm !== null)) ctx.addIssue({
|
|
1018
|
+
code: "custom",
|
|
1019
|
+
path: ["totalAreaSqm"],
|
|
1020
|
+
message: configuration.scope === "FULL" ? "Required" : "Not allowed"
|
|
1021
|
+
});
|
|
867
1022
|
for (const index of duplicateIndexes(configuration.rooms, (room) => room.clientId)) addDuplicate([
|
|
868
1023
|
"rooms",
|
|
869
1024
|
index,
|
|
@@ -895,13 +1050,8 @@ const PricingLimitsSchema = zod.z.object({
|
|
|
895
1050
|
totalAreaSqm: RangeSchema,
|
|
896
1051
|
rooms: RangeSchema,
|
|
897
1052
|
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()
|
|
1053
|
+
roomHeightM: RangeSchema,
|
|
1054
|
+
missingAreaHintSqm: zod.z.number().min(0)
|
|
905
1055
|
});
|
|
906
1056
|
const PricingRuleSchema = zod.z.object({
|
|
907
1057
|
id: zod.z.string(),
|
|
@@ -910,9 +1060,16 @@ const PricingRuleSchema = zod.z.object({
|
|
|
910
1060
|
materialTypeId: zod.z.string().nullable(),
|
|
911
1061
|
name: LocalizedTextSchema,
|
|
912
1062
|
quantityFormula: zod.z.string().nullable(),
|
|
1063
|
+
surface: zod.z.enum([
|
|
1064
|
+
"FLOOR",
|
|
1065
|
+
"WALLS",
|
|
1066
|
+
"CEILING"
|
|
1067
|
+
]).nullable(),
|
|
1068
|
+
baseComponent: zod.z.boolean(),
|
|
913
1069
|
costFormula: zod.z.string().max(500),
|
|
1070
|
+
laborFormula: zod.z.string().max(500).nullable(),
|
|
914
1071
|
sortOrder: zod.z.int()
|
|
915
|
-
}).refine((rule) => rule.level === "OBJECT" === (rule.quantityFormula === null), { path: ["quantityFormula"] });
|
|
1072
|
+
}).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"] });
|
|
916
1073
|
const PricingRoomTypeSchema = zod.z.object({
|
|
917
1074
|
id: zod.z.string(),
|
|
918
1075
|
code: zod.z.string()
|
|
@@ -949,6 +1106,8 @@ const PricingOptionSchema = zod.z.object({
|
|
|
949
1106
|
priceCents: CentsSchema,
|
|
950
1107
|
unit: OptionUnitSchema,
|
|
951
1108
|
roomTypeCodes: zod.z.array(zod.z.string()),
|
|
1109
|
+
priceCoefficientKeys: zod.z.array(zod.z.string()),
|
|
1110
|
+
scopes: zod.z.array(ScopeSchema).min(1),
|
|
952
1111
|
minQuantity: zod.z.int().nullable(),
|
|
953
1112
|
maxQuantity: zod.z.int().nullable(),
|
|
954
1113
|
available: zod.z.boolean()
|
|
@@ -957,7 +1116,6 @@ const PricingDataSchema = zod.z.object({
|
|
|
957
1116
|
version: zod.z.string().regex(/^[0-9a-f]{16}$/),
|
|
958
1117
|
exchangeRate: ExchangeRateSchema.nullable(),
|
|
959
1118
|
limits: PricingLimitsSchema,
|
|
960
|
-
baseRates: zod.z.array(BaseRateSchema),
|
|
961
1119
|
coefficients: zod.z.record(zod.z.string(), zod.z.number()),
|
|
962
1120
|
rules: zod.z.array(PricingRuleSchema),
|
|
963
1121
|
roomTypes: zod.z.array(PricingRoomTypeSchema),
|
|
@@ -973,6 +1131,12 @@ const EstimateMessageSchema = zod.z.object({
|
|
|
973
1131
|
code: zod.z.string(),
|
|
974
1132
|
params: zod.z.record(zod.z.string(), zod.z.unknown()).optional()
|
|
975
1133
|
});
|
|
1134
|
+
const LineComponentSchema = zod.z.object({
|
|
1135
|
+
kind: LineComponentKindSchema,
|
|
1136
|
+
ruleId: zod.z.string(),
|
|
1137
|
+
amountCents: zod.z.number().int().min(0),
|
|
1138
|
+
sharedWithCategoryId: zod.z.string().optional()
|
|
1139
|
+
});
|
|
976
1140
|
const EstimateLineSchema = zod.z.object({
|
|
977
1141
|
id: zod.z.string(),
|
|
978
1142
|
section: SectionSchema,
|
|
@@ -988,6 +1152,7 @@ const EstimateLineSchema = zod.z.object({
|
|
|
988
1152
|
unitPriceCents: CentsSchema.nullable(),
|
|
989
1153
|
amountCents: CentsSchema,
|
|
990
1154
|
amountUahKopecks: CentsSchema.nullable(),
|
|
1155
|
+
components: zod.z.array(LineComponentSchema).optional(),
|
|
991
1156
|
warnings: zod.z.array(EstimateMessageSchema),
|
|
992
1157
|
error: EstimateMessageSchema.nullable()
|
|
993
1158
|
});
|
|
@@ -1025,19 +1190,32 @@ const RuleSampleSchema = zod.z.object({
|
|
|
1025
1190
|
unit: ProductUnitSchema,
|
|
1026
1191
|
wastePercent: zod.z.number().min(0)
|
|
1027
1192
|
}).optional(),
|
|
1193
|
+
surfaceArea: zod.z.number().min(0).optional(),
|
|
1028
1194
|
object: zod.z.object({
|
|
1029
|
-
|
|
1195
|
+
scope: ScopeSchema,
|
|
1196
|
+
totalAreaSqm: zod.z.number().positive().nullable(),
|
|
1030
1197
|
balconyAreaSqm: zod.z.number().min(0),
|
|
1031
1198
|
balconyTiles: zod.z.boolean(),
|
|
1032
|
-
kitchenType: KitchenTypeSchema,
|
|
1033
1199
|
finishLevel: FinishLevelSchema,
|
|
1034
1200
|
roomsCount: zod.z.int().min(0),
|
|
1035
|
-
bathroomsCount: zod.z.int().min(0)
|
|
1201
|
+
bathroomsCount: zod.z.int().min(0),
|
|
1202
|
+
roomsFloorArea: zod.z.number().min(0),
|
|
1203
|
+
roomsWallArea: zod.z.number().min(0),
|
|
1204
|
+
roomsPerimeter: zod.z.number().min(0),
|
|
1205
|
+
wetRoomsCount: zod.z.int().min(0)
|
|
1036
1206
|
})
|
|
1207
|
+
}).superRefine((sample, ctx) => {
|
|
1208
|
+
const { scope, totalAreaSqm } = sample.object;
|
|
1209
|
+
if (scope === "FULL" !== (totalAreaSqm !== null)) ctx.addIssue({
|
|
1210
|
+
code: "custom",
|
|
1211
|
+
path: ["object", "totalAreaSqm"],
|
|
1212
|
+
message: scope === "FULL" ? "Required" : "Not allowed"
|
|
1213
|
+
});
|
|
1037
1214
|
});
|
|
1038
1215
|
const RulePreviewSchema = zod.z.object({
|
|
1039
1216
|
quantity: zod.z.number().nullable(),
|
|
1040
1217
|
amountCents: CentsSchema.nullable(),
|
|
1218
|
+
laborCents: CentsSchema.nullable(),
|
|
1041
1219
|
variables: zod.z.record(zod.z.string(), zod.z.union([
|
|
1042
1220
|
zod.z.number(),
|
|
1043
1221
|
zod.z.boolean(),
|
|
@@ -1072,20 +1250,28 @@ function isIncomplete(lines) {
|
|
|
1072
1250
|
}
|
|
1073
1251
|
//#endregion
|
|
1074
1252
|
//#region src/estimate/warnings.ts
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1253
|
+
const roundArea = (value) => Math.round(value * 1e4) / 1e4;
|
|
1254
|
+
function roomsAreaWarnings(configuration, limits) {
|
|
1255
|
+
const totalAreaSqm = configuration.totalAreaSqm;
|
|
1256
|
+
if (configuration.scope !== "FULL" || totalAreaSqm === null) return [];
|
|
1257
|
+
const roomsAreaSqm = roundArea(configuration.rooms.reduce((sum, room) => sum + roomVariables(room).floorArea, 0));
|
|
1258
|
+
if (roomsAreaSqm > totalAreaSqm) return [{
|
|
1078
1259
|
code: "ROOMS_AREA_EXCEEDS_TOTAL",
|
|
1079
1260
|
params: {
|
|
1080
1261
|
roomsAreaSqm,
|
|
1081
|
-
totalAreaSqm
|
|
1262
|
+
totalAreaSqm
|
|
1082
1263
|
}
|
|
1264
|
+
}];
|
|
1265
|
+
const missingSqm = roundArea(totalAreaSqm - roomsAreaSqm);
|
|
1266
|
+
return missingSqm > limits.missingAreaHintSqm ? [{
|
|
1267
|
+
code: "ROOMS_AREA_BELOW_TOTAL",
|
|
1268
|
+
params: { missingSqm }
|
|
1083
1269
|
}] : [];
|
|
1084
1270
|
}
|
|
1085
|
-
function estimateWarnings(lines, configuration, rate) {
|
|
1271
|
+
function estimateWarnings(lines, configuration, rate, limits) {
|
|
1086
1272
|
return [
|
|
1087
1273
|
...collectWarnings(lines),
|
|
1088
|
-
...roomsAreaWarnings(configuration),
|
|
1274
|
+
...roomsAreaWarnings(configuration, limits),
|
|
1089
1275
|
...configuration.currency === "UAH" && rate === null ? [{ code: "EXCHANGE_RATE_UNAVAILABLE" }] : []
|
|
1090
1276
|
];
|
|
1091
1277
|
}
|
|
@@ -1099,10 +1285,11 @@ function withKopecks(line, rate) {
|
|
|
1099
1285
|
}
|
|
1100
1286
|
function estimate(configuration, data) {
|
|
1101
1287
|
const rate = data.exchangeRate?.rate ?? null;
|
|
1288
|
+
const variables = objectVariables(configuration);
|
|
1102
1289
|
const lines = [
|
|
1103
|
-
|
|
1104
|
-
...objectRuleLines(configuration, data),
|
|
1105
|
-
...materialLines(configuration, data),
|
|
1290
|
+
...objectRuleLines(configuration, data, "BASE", variables),
|
|
1291
|
+
...objectRuleLines(configuration, data, "OBJECT", variables),
|
|
1292
|
+
...materialLines(configuration, data, variables),
|
|
1106
1293
|
...engineeringLines(configuration, data),
|
|
1107
1294
|
...additionalLines(configuration, data)
|
|
1108
1295
|
].map((line) => withKopecks(line, rate));
|
|
@@ -1112,7 +1299,7 @@ function estimate(configuration, data) {
|
|
|
1112
1299
|
exchangeRate: data.exchangeRate,
|
|
1113
1300
|
lines,
|
|
1114
1301
|
totals: computeTotals(lines, rate !== null),
|
|
1115
|
-
warnings: estimateWarnings(lines, configuration, rate),
|
|
1302
|
+
warnings: estimateWarnings(lines, configuration, rate, data.limits),
|
|
1116
1303
|
incomplete: isIncomplete(lines)
|
|
1117
1304
|
};
|
|
1118
1305
|
}
|
|
@@ -1127,8 +1314,9 @@ function checkLimits(configuration, limits, options = []) {
|
|
|
1127
1314
|
max
|
|
1128
1315
|
});
|
|
1129
1316
|
};
|
|
1130
|
-
|
|
1131
|
-
check("
|
|
1317
|
+
const isFull = configuration.scope === "FULL";
|
|
1318
|
+
if (isFull && configuration.totalAreaSqm !== null) check("totalAreaSqm", configuration.totalAreaSqm, limits.totalAreaSqm.min, limits.totalAreaSqm.max);
|
|
1319
|
+
check("balconyAreaSqm", configuration.balconyAreaSqm, 0, isFull && configuration.totalAreaSqm !== null ? configuration.totalAreaSqm : limits.totalAreaSqm.max);
|
|
1132
1320
|
check("rooms", configuration.rooms.length, limits.rooms.min, limits.rooms.max);
|
|
1133
1321
|
configuration.rooms.forEach((room, index) => {
|
|
1134
1322
|
const path = `rooms[${index}]`;
|
|
@@ -1184,18 +1372,33 @@ function referencesOf(sources) {
|
|
|
1184
1372
|
}
|
|
1185
1373
|
function objectScope({ object }) {
|
|
1186
1374
|
return {
|
|
1187
|
-
|
|
1375
|
+
scope: object.scope,
|
|
1376
|
+
totalArea: totalAreaOf(object.scope, object.totalAreaSqm, object.roomsFloorArea),
|
|
1188
1377
|
balconyArea: object.balconyAreaSqm,
|
|
1189
1378
|
balconyTiles: object.balconyTiles,
|
|
1190
|
-
kitchenType: object.kitchenType,
|
|
1191
1379
|
finishLevel: object.finishLevel,
|
|
1192
1380
|
roomsCount: object.roomsCount,
|
|
1193
|
-
bathroomsCount: object.bathroomsCount
|
|
1381
|
+
bathroomsCount: object.bathroomsCount,
|
|
1382
|
+
roomsFloorArea: object.roomsFloorArea,
|
|
1383
|
+
roomsWallArea: object.roomsWallArea,
|
|
1384
|
+
roomsPerimeter: object.roomsPerimeter,
|
|
1385
|
+
wetRoomsCount: object.wetRoomsCount
|
|
1194
1386
|
};
|
|
1195
1387
|
}
|
|
1196
1388
|
function scopeOf(level, sample) {
|
|
1197
1389
|
if (level === "OBJECT") return { scope: objectScope(sample) };
|
|
1198
|
-
const { room, product } = sample;
|
|
1390
|
+
const { room, product, surfaceArea } = sample;
|
|
1391
|
+
if (level === "SURFACE") {
|
|
1392
|
+
if (room === void 0 || surfaceArea === void 0) return { error: {
|
|
1393
|
+
code: "SAMPLE_INCOMPLETE",
|
|
1394
|
+
params: { missing: room === void 0 ? "room" : "surfaceArea" }
|
|
1395
|
+
} };
|
|
1396
|
+
return { scope: {
|
|
1397
|
+
...roomVariables(room),
|
|
1398
|
+
...objectScope(sample),
|
|
1399
|
+
surfaceArea
|
|
1400
|
+
} };
|
|
1401
|
+
}
|
|
1199
1402
|
if (room === void 0 || product === void 0) return { error: {
|
|
1200
1403
|
code: "SAMPLE_INCOMPLETE",
|
|
1201
1404
|
params: { missing: room === void 0 ? "room" : "product" }
|
|
@@ -1211,15 +1414,21 @@ function pick(source, names) {
|
|
|
1211
1414
|
}
|
|
1212
1415
|
function previewRule(input) {
|
|
1213
1416
|
const { level, quantityFormula, costFormula, coefficients } = input;
|
|
1214
|
-
const
|
|
1215
|
-
const
|
|
1417
|
+
const laborFormula = quantityFormula === null ? null : input.laborFormula ?? null;
|
|
1418
|
+
const references = referencesOf([
|
|
1419
|
+
quantityFormula,
|
|
1420
|
+
costFormula,
|
|
1421
|
+
laborFormula
|
|
1422
|
+
]);
|
|
1423
|
+
const report = (variables, quantity, amountCents, error, laborCents = null) => ({
|
|
1216
1424
|
quantity,
|
|
1217
1425
|
amountCents,
|
|
1426
|
+
laborCents,
|
|
1218
1427
|
variables: pick(variables, references.variables),
|
|
1219
1428
|
coefficients: pick(coefficients, references.coefficients),
|
|
1220
1429
|
error
|
|
1221
1430
|
});
|
|
1222
|
-
if (level === "OBJECT" !== (quantityFormula === null)) return report({}, null, null, { code: "QUANTITY_FORMULA_MISMATCH" });
|
|
1431
|
+
if ((level === "OBJECT" || level === "SURFACE") !== (quantityFormula === null)) return report({}, null, null, { code: "QUANTITY_FORMULA_MISMATCH" });
|
|
1223
1432
|
const prepared = scopeOf(level, input.sample);
|
|
1224
1433
|
if ("error" in prepared) return report({}, null, null, prepared.error);
|
|
1225
1434
|
let variables = prepared.scope;
|
|
@@ -1245,7 +1454,15 @@ function previewRule(input) {
|
|
|
1245
1454
|
id: "preview",
|
|
1246
1455
|
section: "OBJECT"
|
|
1247
1456
|
}, cost.value);
|
|
1248
|
-
|
|
1457
|
+
if (line.error !== null) return report(variables, quantity, null, line.error);
|
|
1458
|
+
if (laborFormula === null) return report(variables, quantity, line.amountCents, null);
|
|
1459
|
+
const labor = runNumericFormula(laborFormula, {
|
|
1460
|
+
variables,
|
|
1461
|
+
coefficients
|
|
1462
|
+
});
|
|
1463
|
+
if (!labor.ok) return report(variables, quantity, line.amountCents, labor.error);
|
|
1464
|
+
const laborCents = componentCents(labor.value);
|
|
1465
|
+
return laborCents === null ? report(variables, quantity, line.amountCents, outOfRange()) : report(variables, quantity, line.amountCents, null, laborCents);
|
|
1249
1466
|
}
|
|
1250
1467
|
//#endregion
|
|
1251
1468
|
//#region src/formula/suggest.ts
|
|
@@ -1315,13 +1532,18 @@ const FORMULA_SCOPE_VARIABLES = {
|
|
|
1315
1532
|
number("totalArea"),
|
|
1316
1533
|
number("balconyArea"),
|
|
1317
1534
|
boolean("balconyTiles"),
|
|
1318
|
-
enumeration("kitchenType", KitchenTypeSchema.options),
|
|
1319
1535
|
enumeration("finishLevel", FinishLevelSchema.options),
|
|
1320
1536
|
number("roomsCount"),
|
|
1321
|
-
number("bathroomsCount")
|
|
1537
|
+
number("bathroomsCount"),
|
|
1538
|
+
enumeration("scope", ScopeSchema.options),
|
|
1539
|
+
number("roomsFloorArea"),
|
|
1540
|
+
number("roomsWallArea"),
|
|
1541
|
+
number("roomsPerimeter"),
|
|
1542
|
+
number("wetRoomsCount")
|
|
1322
1543
|
]
|
|
1323
1544
|
};
|
|
1324
1545
|
const FORMULA_QUANTITY_VARIABLE = number("quantity");
|
|
1546
|
+
const FORMULA_SURFACE_AREA_VARIABLE = number("surfaceArea");
|
|
1325
1547
|
const FORMULA_VARIABLES = {
|
|
1326
1548
|
OBJECT: FORMULA_SCOPE_VARIABLES.object,
|
|
1327
1549
|
CATEGORY: [
|
|
@@ -1333,9 +1555,14 @@ const FORMULA_VARIABLES = {
|
|
|
1333
1555
|
...FORMULA_SCOPE_VARIABLES.room,
|
|
1334
1556
|
...FORMULA_SCOPE_VARIABLES.product,
|
|
1335
1557
|
...FORMULA_SCOPE_VARIABLES.object
|
|
1558
|
+
],
|
|
1559
|
+
SURFACE: [
|
|
1560
|
+
...FORMULA_SCOPE_VARIABLES.room,
|
|
1561
|
+
...FORMULA_SCOPE_VARIABLES.object,
|
|
1562
|
+
FORMULA_SURFACE_AREA_VARIABLE
|
|
1336
1563
|
]
|
|
1337
1564
|
};
|
|
1338
|
-
const ALL_VARIABLE_NAMES = new Set(FORMULA_VARIABLES.CATEGORY.map((variable) => variable.name));
|
|
1565
|
+
const ALL_VARIABLE_NAMES = new Set([...FORMULA_VARIABLES.CATEGORY, ...FORMULA_VARIABLES.SURFACE].map((variable) => variable.name));
|
|
1339
1566
|
function span(node) {
|
|
1340
1567
|
return {
|
|
1341
1568
|
position: node.start,
|
|
@@ -1359,7 +1586,7 @@ function validateFormula(input) {
|
|
|
1359
1586
|
const knownCoefficients = new Set(input.coefficientKeys);
|
|
1360
1587
|
const usedCoefficients = /* @__PURE__ */ new Set();
|
|
1361
1588
|
const errors = [];
|
|
1362
|
-
const quantityAllowed = input.kind
|
|
1589
|
+
const quantityAllowed = input.kind !== "quantity" && (input.level === "CATEGORY" || input.level === "MATERIAL_TYPE");
|
|
1363
1590
|
function mismatch(node, expected) {
|
|
1364
1591
|
errors.push({
|
|
1365
1592
|
code: "FORMULA_SYNTAX",
|
|
@@ -1547,7 +1774,6 @@ function validateFormula(input) {
|
|
|
1547
1774
|
};
|
|
1548
1775
|
}
|
|
1549
1776
|
//#endregion
|
|
1550
|
-
exports.BaseRateSchema = BaseRateSchema;
|
|
1551
1777
|
exports.COEFFICIENT_NAMESPACE = COEFFICIENT_NAMESPACE;
|
|
1552
1778
|
exports.CentsSchema = CentsSchema;
|
|
1553
1779
|
exports.ConfigurationSchema = ConfigurationSchema;
|
|
@@ -1563,9 +1789,11 @@ exports.FORMULA_FUNCTION_TABLE = FORMULA_FUNCTION_TABLE;
|
|
|
1563
1789
|
exports.FORMULA_LIMITS = FORMULA_LIMITS;
|
|
1564
1790
|
exports.FORMULA_QUANTITY_VARIABLE = FORMULA_QUANTITY_VARIABLE;
|
|
1565
1791
|
exports.FORMULA_SCOPE_VARIABLES = FORMULA_SCOPE_VARIABLES;
|
|
1792
|
+
exports.FORMULA_SURFACE_AREA_VARIABLE = FORMULA_SURFACE_AREA_VARIABLE;
|
|
1566
1793
|
exports.FORMULA_VARIABLES = FORMULA_VARIABLES;
|
|
1567
1794
|
exports.FinishLevelSchema = FinishLevelSchema;
|
|
1568
|
-
exports.
|
|
1795
|
+
exports.LineComponentKindSchema = LineComponentKindSchema;
|
|
1796
|
+
exports.LineComponentSchema = LineComponentSchema;
|
|
1569
1797
|
exports.LineStatusSchema = LineStatusSchema;
|
|
1570
1798
|
exports.LocalizedTextSchema = LocalizedTextSchema;
|
|
1571
1799
|
exports.OptionCodeSchema = OptionCodeSchema;
|
|
@@ -1590,6 +1818,7 @@ exports.RoomTypeCodeSchema = RoomTypeCodeSchema;
|
|
|
1590
1818
|
exports.RuleLevelSchema = RuleLevelSchema;
|
|
1591
1819
|
exports.RulePreviewSchema = RulePreviewSchema;
|
|
1592
1820
|
exports.RuleSampleSchema = RuleSampleSchema;
|
|
1821
|
+
exports.ScopeSchema = ScopeSchema;
|
|
1593
1822
|
exports.SectionSchema = SectionSchema;
|
|
1594
1823
|
exports.SectionTotalSchema = SectionTotalSchema;
|
|
1595
1824
|
exports.SurfaceKindSchema = SurfaceKindSchema;
|
|
@@ -1606,4 +1835,5 @@ exports.productVariablesOf = productVariablesOf;
|
|
|
1606
1835
|
exports.roomVariables = roomVariables;
|
|
1607
1836
|
exports.suggestNearest = suggestNearest;
|
|
1608
1837
|
exports.toCents = toCents;
|
|
1838
|
+
exports.totalAreaOf = totalAreaOf;
|
|
1609
1839
|
exports.validateFormula = validateFormula;
|