@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.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,51 +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
|
-
finishLevel: configuration.finishLevel,
|
|
212
|
-
roomsCount: configuration.rooms.filter((room) => COUNTED_AS_ROOMS.includes(room.roomTypeCode)).length,
|
|
213
|
-
bathroomsCount: configuration.rooms.filter((room) => room.roomTypeCode === "BATHROOM").length
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
//#endregion
|
|
217
|
-
//#region src/variables/product.ts
|
|
218
|
-
function productVariablesOf(product, wastePercent) {
|
|
219
|
-
return {
|
|
220
|
-
price: product.priceCents / 100,
|
|
221
|
-
waste: wastePercent / 100,
|
|
222
|
-
unit: product.unit
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
function productVariables(product, category) {
|
|
226
|
-
return productVariablesOf(product, product.wastePercentOverride ?? category.wastePercent);
|
|
227
|
-
}
|
|
228
|
-
//#endregion
|
|
229
|
-
//#region src/variables/room.ts
|
|
230
|
-
function roomVariables(room) {
|
|
231
|
-
const perimeter = 2 * (room.lengthM + room.widthM);
|
|
232
|
-
return {
|
|
233
|
-
length: room.lengthM,
|
|
234
|
-
width: room.widthM,
|
|
235
|
-
height: room.heightM,
|
|
236
|
-
floorArea: room.lengthM * room.widthM,
|
|
237
|
-
perimeter,
|
|
238
|
-
wallArea: perimeter * room.heightM,
|
|
239
|
-
heatedFloor: room.heatedFloor,
|
|
240
|
-
roomType: room.roomTypeCode
|
|
241
|
-
};
|
|
264
|
+
return [...(configuration.scope === "PARTIAL" ? data.packageItems.filter((item) => !item.includedInBase) : data.packageItems).map((item) => packageLine(configuration, item)), ...optionLines(configuration, data, "ENGINEERING")];
|
|
242
265
|
}
|
|
243
266
|
//#endregion
|
|
244
267
|
//#region src/formula/language.ts
|
|
@@ -672,13 +695,87 @@ function runNumericFormula(source, scope) {
|
|
|
672
695
|
};
|
|
673
696
|
}
|
|
674
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
|
|
675
772
|
//#region src/estimate/materials.ts
|
|
676
773
|
const isMaterialRule = (rule) => rule.quantityFormula !== null;
|
|
677
774
|
function selectRule(data, categoryId, materialTypeId) {
|
|
678
775
|
const rules = data.rules.filter(isMaterialRule);
|
|
679
776
|
return rules.find((rule) => rule.level === "MATERIAL_TYPE" && rule.categoryId === categoryId && rule.materialTypeId === materialTypeId) ?? rules.find((rule) => rule.level === "CATEGORY" && rule.categoryId === categoryId);
|
|
680
777
|
}
|
|
681
|
-
function materialLine(configuration, data, room, categoryId, productId) {
|
|
778
|
+
function materialLine(configuration, data, room, categoryId, productId, objectScope) {
|
|
682
779
|
const init = {
|
|
683
780
|
id: lineId("MATERIALS", room.clientId, categoryId),
|
|
684
781
|
section: "MATERIALS",
|
|
@@ -710,7 +807,7 @@ function materialLine(configuration, data, room, categoryId, productId) {
|
|
|
710
807
|
const variables = {
|
|
711
808
|
...roomVariables(room),
|
|
712
809
|
...productVariables(product, category),
|
|
713
|
-
...
|
|
810
|
+
...objectScope
|
|
714
811
|
};
|
|
715
812
|
const { coefficients } = data;
|
|
716
813
|
const ruled = {
|
|
@@ -731,32 +828,74 @@ function materialLine(configuration, data, room, categoryId, productId) {
|
|
|
731
828
|
coefficients
|
|
732
829
|
});
|
|
733
830
|
if (!cost.ok) return failedLine(ruled, "ERROR", cost.error);
|
|
734
|
-
|
|
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({
|
|
735
867
|
...ruled,
|
|
868
|
+
warnings: lineWarnings,
|
|
736
869
|
quantity: roundQuantity(quantity.value),
|
|
737
870
|
unit: product.unit,
|
|
738
|
-
unitPriceCents: product.priceCents
|
|
739
|
-
|
|
871
|
+
unitPriceCents: product.priceCents,
|
|
872
|
+
amountCents,
|
|
873
|
+
components
|
|
874
|
+
});
|
|
740
875
|
}
|
|
741
|
-
function materialLines(configuration, data) {
|
|
742
|
-
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
|
+
});
|
|
743
881
|
}
|
|
744
882
|
//#endregion
|
|
745
883
|
//#region src/estimate/object-rules.ts
|
|
746
|
-
function objectRuleLines(configuration, data) {
|
|
884
|
+
function objectRuleLines(configuration, data, section, variables) {
|
|
885
|
+
if (section === "BASE" && configuration.scope !== "FULL") return [];
|
|
747
886
|
const scope = {
|
|
748
|
-
variables
|
|
887
|
+
variables,
|
|
749
888
|
coefficients: data.coefficients
|
|
750
889
|
};
|
|
751
|
-
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) => {
|
|
752
891
|
const init = {
|
|
753
|
-
id: lineId(
|
|
754
|
-
section
|
|
892
|
+
id: lineId(section, rule.id),
|
|
893
|
+
section,
|
|
755
894
|
ruleId: rule.id
|
|
756
895
|
};
|
|
757
896
|
const outcome = runNumericFormula(rule.costFormula, scope);
|
|
758
897
|
return outcome.ok ? pricedLine(init, outcome.value) : failedLine(init, "ERROR", outcome.error);
|
|
759
|
-
});
|
|
898
|
+
}).filter((line) => section !== "BASE" || line.status !== "OK" || line.amountCents !== 0);
|
|
760
899
|
}
|
|
761
900
|
//#endregion
|
|
762
901
|
//#region src/schemas/common.ts
|
|
@@ -764,11 +903,17 @@ const LocalizedTextSchema = zod.z.object({
|
|
|
764
903
|
en: zod.z.string(),
|
|
765
904
|
uk: zod.z.string()
|
|
766
905
|
});
|
|
767
|
-
const
|
|
906
|
+
const ScopeSchema = zod.z.enum(["FULL", "PARTIAL"]);
|
|
907
|
+
const FinishLevelSchema = zod.z.enum([
|
|
908
|
+
"ROUGH",
|
|
909
|
+
"PRE_FINISH",
|
|
910
|
+
"LIVED_IN"
|
|
911
|
+
]);
|
|
768
912
|
const RuleLevelSchema = zod.z.enum([
|
|
769
913
|
"OBJECT",
|
|
770
914
|
"CATEGORY",
|
|
771
|
-
"MATERIAL_TYPE"
|
|
915
|
+
"MATERIAL_TYPE",
|
|
916
|
+
"SURFACE"
|
|
772
917
|
]);
|
|
773
918
|
const RateSourceSchema = zod.z.enum(["NBU", "MANUAL"]);
|
|
774
919
|
const SurfaceKindSchema = zod.z.enum([
|
|
@@ -795,7 +940,9 @@ const RoomTypeCodeSchema = zod.z.enum([
|
|
|
795
940
|
"BEDROOM",
|
|
796
941
|
"KITCHEN",
|
|
797
942
|
"KITCHEN_LIVING",
|
|
798
|
-
"BATHROOM"
|
|
943
|
+
"BATHROOM",
|
|
944
|
+
"HALLWAY",
|
|
945
|
+
"STORAGE"
|
|
799
946
|
]);
|
|
800
947
|
const CurrencySchema = zod.z.enum(["USD", "UAH"]);
|
|
801
948
|
const SectionSchema = zod.z.enum([
|
|
@@ -805,6 +952,11 @@ const SectionSchema = zod.z.enum([
|
|
|
805
952
|
"ENGINEERING",
|
|
806
953
|
"ADDITIONAL"
|
|
807
954
|
]);
|
|
955
|
+
const LineComponentKindSchema = zod.z.enum([
|
|
956
|
+
"MATERIAL",
|
|
957
|
+
"LABOR",
|
|
958
|
+
"PREP"
|
|
959
|
+
]);
|
|
808
960
|
const LineStatusSchema = zod.z.enum([
|
|
809
961
|
"OK",
|
|
810
962
|
"INCLUDED",
|
|
@@ -848,8 +1000,9 @@ function duplicateIndexes(items, key) {
|
|
|
848
1000
|
});
|
|
849
1001
|
}
|
|
850
1002
|
const ConfigurationSchema = zod.z.object({
|
|
1003
|
+
scope: ScopeSchema.default("FULL"),
|
|
851
1004
|
finishLevel: FinishLevelSchema,
|
|
852
|
-
totalAreaSqm: zod.z.number().positive(),
|
|
1005
|
+
totalAreaSqm: zod.z.number().positive().nullable(),
|
|
853
1006
|
balconyAreaSqm: zod.z.number().min(0),
|
|
854
1007
|
balconyTiles: zod.z.boolean(),
|
|
855
1008
|
rooms: zod.z.array(RoomConfigurationSchema),
|
|
@@ -861,6 +1014,11 @@ const ConfigurationSchema = zod.z.object({
|
|
|
861
1014
|
path,
|
|
862
1015
|
message: "Duplicate"
|
|
863
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
|
+
});
|
|
864
1022
|
for (const index of duplicateIndexes(configuration.rooms, (room) => room.clientId)) addDuplicate([
|
|
865
1023
|
"rooms",
|
|
866
1024
|
index,
|
|
@@ -892,13 +1050,8 @@ const PricingLimitsSchema = zod.z.object({
|
|
|
892
1050
|
totalAreaSqm: RangeSchema,
|
|
893
1051
|
rooms: RangeSchema,
|
|
894
1052
|
roomSideM: RangeSchema,
|
|
895
|
-
roomHeightM: RangeSchema
|
|
896
|
-
|
|
897
|
-
const BaseRateSchema = zod.z.object({
|
|
898
|
-
finishLevel: FinishLevelSchema,
|
|
899
|
-
areaFromSqm: zod.z.number().min(0),
|
|
900
|
-
areaToSqm: zod.z.number(),
|
|
901
|
-
rateCentsPerSqm: zod.z.int().positive()
|
|
1053
|
+
roomHeightM: RangeSchema,
|
|
1054
|
+
missingAreaHintSqm: zod.z.number().min(0)
|
|
902
1055
|
});
|
|
903
1056
|
const PricingRuleSchema = zod.z.object({
|
|
904
1057
|
id: zod.z.string(),
|
|
@@ -907,9 +1060,16 @@ const PricingRuleSchema = zod.z.object({
|
|
|
907
1060
|
materialTypeId: zod.z.string().nullable(),
|
|
908
1061
|
name: LocalizedTextSchema,
|
|
909
1062
|
quantityFormula: zod.z.string().nullable(),
|
|
1063
|
+
surface: zod.z.enum([
|
|
1064
|
+
"FLOOR",
|
|
1065
|
+
"WALLS",
|
|
1066
|
+
"CEILING"
|
|
1067
|
+
]).nullable(),
|
|
1068
|
+
baseComponent: zod.z.boolean(),
|
|
910
1069
|
costFormula: zod.z.string().max(500),
|
|
1070
|
+
laborFormula: zod.z.string().max(500).nullable(),
|
|
911
1071
|
sortOrder: zod.z.int()
|
|
912
|
-
}).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"] });
|
|
913
1073
|
const PricingRoomTypeSchema = zod.z.object({
|
|
914
1074
|
id: zod.z.string(),
|
|
915
1075
|
code: zod.z.string()
|
|
@@ -946,6 +1106,8 @@ const PricingOptionSchema = zod.z.object({
|
|
|
946
1106
|
priceCents: CentsSchema,
|
|
947
1107
|
unit: OptionUnitSchema,
|
|
948
1108
|
roomTypeCodes: zod.z.array(zod.z.string()),
|
|
1109
|
+
priceCoefficientKeys: zod.z.array(zod.z.string()),
|
|
1110
|
+
scopes: zod.z.array(ScopeSchema).min(1),
|
|
949
1111
|
minQuantity: zod.z.int().nullable(),
|
|
950
1112
|
maxQuantity: zod.z.int().nullable(),
|
|
951
1113
|
available: zod.z.boolean()
|
|
@@ -954,7 +1116,6 @@ const PricingDataSchema = zod.z.object({
|
|
|
954
1116
|
version: zod.z.string().regex(/^[0-9a-f]{16}$/),
|
|
955
1117
|
exchangeRate: ExchangeRateSchema.nullable(),
|
|
956
1118
|
limits: PricingLimitsSchema,
|
|
957
|
-
baseRates: zod.z.array(BaseRateSchema),
|
|
958
1119
|
coefficients: zod.z.record(zod.z.string(), zod.z.number()),
|
|
959
1120
|
rules: zod.z.array(PricingRuleSchema),
|
|
960
1121
|
roomTypes: zod.z.array(PricingRoomTypeSchema),
|
|
@@ -970,6 +1131,12 @@ const EstimateMessageSchema = zod.z.object({
|
|
|
970
1131
|
code: zod.z.string(),
|
|
971
1132
|
params: zod.z.record(zod.z.string(), zod.z.unknown()).optional()
|
|
972
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
|
+
});
|
|
973
1140
|
const EstimateLineSchema = zod.z.object({
|
|
974
1141
|
id: zod.z.string(),
|
|
975
1142
|
section: SectionSchema,
|
|
@@ -985,6 +1152,7 @@ const EstimateLineSchema = zod.z.object({
|
|
|
985
1152
|
unitPriceCents: CentsSchema.nullable(),
|
|
986
1153
|
amountCents: CentsSchema,
|
|
987
1154
|
amountUahKopecks: CentsSchema.nullable(),
|
|
1155
|
+
components: zod.z.array(LineComponentSchema).optional(),
|
|
988
1156
|
warnings: zod.z.array(EstimateMessageSchema),
|
|
989
1157
|
error: EstimateMessageSchema.nullable()
|
|
990
1158
|
});
|
|
@@ -1022,18 +1190,32 @@ const RuleSampleSchema = zod.z.object({
|
|
|
1022
1190
|
unit: ProductUnitSchema,
|
|
1023
1191
|
wastePercent: zod.z.number().min(0)
|
|
1024
1192
|
}).optional(),
|
|
1193
|
+
surfaceArea: zod.z.number().min(0).optional(),
|
|
1025
1194
|
object: zod.z.object({
|
|
1026
|
-
|
|
1195
|
+
scope: ScopeSchema,
|
|
1196
|
+
totalAreaSqm: zod.z.number().positive().nullable(),
|
|
1027
1197
|
balconyAreaSqm: zod.z.number().min(0),
|
|
1028
1198
|
balconyTiles: zod.z.boolean(),
|
|
1029
1199
|
finishLevel: FinishLevelSchema,
|
|
1030
1200
|
roomsCount: zod.z.int().min(0),
|
|
1031
|
-
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)
|
|
1032
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
|
+
});
|
|
1033
1214
|
});
|
|
1034
1215
|
const RulePreviewSchema = zod.z.object({
|
|
1035
1216
|
quantity: zod.z.number().nullable(),
|
|
1036
1217
|
amountCents: CentsSchema.nullable(),
|
|
1218
|
+
laborCents: CentsSchema.nullable(),
|
|
1037
1219
|
variables: zod.z.record(zod.z.string(), zod.z.union([
|
|
1038
1220
|
zod.z.number(),
|
|
1039
1221
|
zod.z.boolean(),
|
|
@@ -1068,20 +1250,28 @@ function isIncomplete(lines) {
|
|
|
1068
1250
|
}
|
|
1069
1251
|
//#endregion
|
|
1070
1252
|
//#region src/estimate/warnings.ts
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
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 [{
|
|
1074
1259
|
code: "ROOMS_AREA_EXCEEDS_TOTAL",
|
|
1075
1260
|
params: {
|
|
1076
1261
|
roomsAreaSqm,
|
|
1077
|
-
totalAreaSqm
|
|
1262
|
+
totalAreaSqm
|
|
1078
1263
|
}
|
|
1264
|
+
}];
|
|
1265
|
+
const missingSqm = roundArea(totalAreaSqm - roomsAreaSqm);
|
|
1266
|
+
return missingSqm > limits.missingAreaHintSqm ? [{
|
|
1267
|
+
code: "ROOMS_AREA_BELOW_TOTAL",
|
|
1268
|
+
params: { missingSqm }
|
|
1079
1269
|
}] : [];
|
|
1080
1270
|
}
|
|
1081
|
-
function estimateWarnings(lines, configuration, rate) {
|
|
1271
|
+
function estimateWarnings(lines, configuration, rate, limits) {
|
|
1082
1272
|
return [
|
|
1083
1273
|
...collectWarnings(lines),
|
|
1084
|
-
...roomsAreaWarnings(configuration),
|
|
1274
|
+
...roomsAreaWarnings(configuration, limits),
|
|
1085
1275
|
...configuration.currency === "UAH" && rate === null ? [{ code: "EXCHANGE_RATE_UNAVAILABLE" }] : []
|
|
1086
1276
|
];
|
|
1087
1277
|
}
|
|
@@ -1095,10 +1285,11 @@ function withKopecks(line, rate) {
|
|
|
1095
1285
|
}
|
|
1096
1286
|
function estimate(configuration, data) {
|
|
1097
1287
|
const rate = data.exchangeRate?.rate ?? null;
|
|
1288
|
+
const variables = objectVariables(configuration);
|
|
1098
1289
|
const lines = [
|
|
1099
|
-
|
|
1100
|
-
...objectRuleLines(configuration, data),
|
|
1101
|
-
...materialLines(configuration, data),
|
|
1290
|
+
...objectRuleLines(configuration, data, "BASE", variables),
|
|
1291
|
+
...objectRuleLines(configuration, data, "OBJECT", variables),
|
|
1292
|
+
...materialLines(configuration, data, variables),
|
|
1102
1293
|
...engineeringLines(configuration, data),
|
|
1103
1294
|
...additionalLines(configuration, data)
|
|
1104
1295
|
].map((line) => withKopecks(line, rate));
|
|
@@ -1108,7 +1299,7 @@ function estimate(configuration, data) {
|
|
|
1108
1299
|
exchangeRate: data.exchangeRate,
|
|
1109
1300
|
lines,
|
|
1110
1301
|
totals: computeTotals(lines, rate !== null),
|
|
1111
|
-
warnings: estimateWarnings(lines, configuration, rate),
|
|
1302
|
+
warnings: estimateWarnings(lines, configuration, rate, data.limits),
|
|
1112
1303
|
incomplete: isIncomplete(lines)
|
|
1113
1304
|
};
|
|
1114
1305
|
}
|
|
@@ -1123,8 +1314,9 @@ function checkLimits(configuration, limits, options = []) {
|
|
|
1123
1314
|
max
|
|
1124
1315
|
});
|
|
1125
1316
|
};
|
|
1126
|
-
|
|
1127
|
-
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);
|
|
1128
1320
|
check("rooms", configuration.rooms.length, limits.rooms.min, limits.rooms.max);
|
|
1129
1321
|
configuration.rooms.forEach((room, index) => {
|
|
1130
1322
|
const path = `rooms[${index}]`;
|
|
@@ -1180,17 +1372,33 @@ function referencesOf(sources) {
|
|
|
1180
1372
|
}
|
|
1181
1373
|
function objectScope({ object }) {
|
|
1182
1374
|
return {
|
|
1183
|
-
|
|
1375
|
+
scope: object.scope,
|
|
1376
|
+
totalArea: totalAreaOf(object.scope, object.totalAreaSqm, object.roomsFloorArea),
|
|
1184
1377
|
balconyArea: object.balconyAreaSqm,
|
|
1185
1378
|
balconyTiles: object.balconyTiles,
|
|
1186
1379
|
finishLevel: object.finishLevel,
|
|
1187
1380
|
roomsCount: object.roomsCount,
|
|
1188
|
-
bathroomsCount: object.bathroomsCount
|
|
1381
|
+
bathroomsCount: object.bathroomsCount,
|
|
1382
|
+
roomsFloorArea: object.roomsFloorArea,
|
|
1383
|
+
roomsWallArea: object.roomsWallArea,
|
|
1384
|
+
roomsPerimeter: object.roomsPerimeter,
|
|
1385
|
+
wetRoomsCount: object.wetRoomsCount
|
|
1189
1386
|
};
|
|
1190
1387
|
}
|
|
1191
1388
|
function scopeOf(level, sample) {
|
|
1192
1389
|
if (level === "OBJECT") return { scope: objectScope(sample) };
|
|
1193
|
-
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
|
+
}
|
|
1194
1402
|
if (room === void 0 || product === void 0) return { error: {
|
|
1195
1403
|
code: "SAMPLE_INCOMPLETE",
|
|
1196
1404
|
params: { missing: room === void 0 ? "room" : "product" }
|
|
@@ -1206,15 +1414,21 @@ function pick(source, names) {
|
|
|
1206
1414
|
}
|
|
1207
1415
|
function previewRule(input) {
|
|
1208
1416
|
const { level, quantityFormula, costFormula, coefficients } = input;
|
|
1209
|
-
const
|
|
1210
|
-
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) => ({
|
|
1211
1424
|
quantity,
|
|
1212
1425
|
amountCents,
|
|
1426
|
+
laborCents,
|
|
1213
1427
|
variables: pick(variables, references.variables),
|
|
1214
1428
|
coefficients: pick(coefficients, references.coefficients),
|
|
1215
1429
|
error
|
|
1216
1430
|
});
|
|
1217
|
-
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" });
|
|
1218
1432
|
const prepared = scopeOf(level, input.sample);
|
|
1219
1433
|
if ("error" in prepared) return report({}, null, null, prepared.error);
|
|
1220
1434
|
let variables = prepared.scope;
|
|
@@ -1240,7 +1454,15 @@ function previewRule(input) {
|
|
|
1240
1454
|
id: "preview",
|
|
1241
1455
|
section: "OBJECT"
|
|
1242
1456
|
}, cost.value);
|
|
1243
|
-
|
|
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);
|
|
1244
1466
|
}
|
|
1245
1467
|
//#endregion
|
|
1246
1468
|
//#region src/formula/suggest.ts
|
|
@@ -1312,10 +1534,16 @@ const FORMULA_SCOPE_VARIABLES = {
|
|
|
1312
1534
|
boolean("balconyTiles"),
|
|
1313
1535
|
enumeration("finishLevel", FinishLevelSchema.options),
|
|
1314
1536
|
number("roomsCount"),
|
|
1315
|
-
number("bathroomsCount")
|
|
1537
|
+
number("bathroomsCount"),
|
|
1538
|
+
enumeration("scope", ScopeSchema.options),
|
|
1539
|
+
number("roomsFloorArea"),
|
|
1540
|
+
number("roomsWallArea"),
|
|
1541
|
+
number("roomsPerimeter"),
|
|
1542
|
+
number("wetRoomsCount")
|
|
1316
1543
|
]
|
|
1317
1544
|
};
|
|
1318
1545
|
const FORMULA_QUANTITY_VARIABLE = number("quantity");
|
|
1546
|
+
const FORMULA_SURFACE_AREA_VARIABLE = number("surfaceArea");
|
|
1319
1547
|
const FORMULA_VARIABLES = {
|
|
1320
1548
|
OBJECT: FORMULA_SCOPE_VARIABLES.object,
|
|
1321
1549
|
CATEGORY: [
|
|
@@ -1327,9 +1555,14 @@ const FORMULA_VARIABLES = {
|
|
|
1327
1555
|
...FORMULA_SCOPE_VARIABLES.room,
|
|
1328
1556
|
...FORMULA_SCOPE_VARIABLES.product,
|
|
1329
1557
|
...FORMULA_SCOPE_VARIABLES.object
|
|
1558
|
+
],
|
|
1559
|
+
SURFACE: [
|
|
1560
|
+
...FORMULA_SCOPE_VARIABLES.room,
|
|
1561
|
+
...FORMULA_SCOPE_VARIABLES.object,
|
|
1562
|
+
FORMULA_SURFACE_AREA_VARIABLE
|
|
1330
1563
|
]
|
|
1331
1564
|
};
|
|
1332
|
-
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));
|
|
1333
1566
|
function span(node) {
|
|
1334
1567
|
return {
|
|
1335
1568
|
position: node.start,
|
|
@@ -1353,7 +1586,7 @@ function validateFormula(input) {
|
|
|
1353
1586
|
const knownCoefficients = new Set(input.coefficientKeys);
|
|
1354
1587
|
const usedCoefficients = /* @__PURE__ */ new Set();
|
|
1355
1588
|
const errors = [];
|
|
1356
|
-
const quantityAllowed = input.kind
|
|
1589
|
+
const quantityAllowed = input.kind !== "quantity" && (input.level === "CATEGORY" || input.level === "MATERIAL_TYPE");
|
|
1357
1590
|
function mismatch(node, expected) {
|
|
1358
1591
|
errors.push({
|
|
1359
1592
|
code: "FORMULA_SYNTAX",
|
|
@@ -1541,7 +1774,6 @@ function validateFormula(input) {
|
|
|
1541
1774
|
};
|
|
1542
1775
|
}
|
|
1543
1776
|
//#endregion
|
|
1544
|
-
exports.BaseRateSchema = BaseRateSchema;
|
|
1545
1777
|
exports.COEFFICIENT_NAMESPACE = COEFFICIENT_NAMESPACE;
|
|
1546
1778
|
exports.CentsSchema = CentsSchema;
|
|
1547
1779
|
exports.ConfigurationSchema = ConfigurationSchema;
|
|
@@ -1557,8 +1789,11 @@ exports.FORMULA_FUNCTION_TABLE = FORMULA_FUNCTION_TABLE;
|
|
|
1557
1789
|
exports.FORMULA_LIMITS = FORMULA_LIMITS;
|
|
1558
1790
|
exports.FORMULA_QUANTITY_VARIABLE = FORMULA_QUANTITY_VARIABLE;
|
|
1559
1791
|
exports.FORMULA_SCOPE_VARIABLES = FORMULA_SCOPE_VARIABLES;
|
|
1792
|
+
exports.FORMULA_SURFACE_AREA_VARIABLE = FORMULA_SURFACE_AREA_VARIABLE;
|
|
1560
1793
|
exports.FORMULA_VARIABLES = FORMULA_VARIABLES;
|
|
1561
1794
|
exports.FinishLevelSchema = FinishLevelSchema;
|
|
1795
|
+
exports.LineComponentKindSchema = LineComponentKindSchema;
|
|
1796
|
+
exports.LineComponentSchema = LineComponentSchema;
|
|
1562
1797
|
exports.LineStatusSchema = LineStatusSchema;
|
|
1563
1798
|
exports.LocalizedTextSchema = LocalizedTextSchema;
|
|
1564
1799
|
exports.OptionCodeSchema = OptionCodeSchema;
|
|
@@ -1583,6 +1818,7 @@ exports.RoomTypeCodeSchema = RoomTypeCodeSchema;
|
|
|
1583
1818
|
exports.RuleLevelSchema = RuleLevelSchema;
|
|
1584
1819
|
exports.RulePreviewSchema = RulePreviewSchema;
|
|
1585
1820
|
exports.RuleSampleSchema = RuleSampleSchema;
|
|
1821
|
+
exports.ScopeSchema = ScopeSchema;
|
|
1586
1822
|
exports.SectionSchema = SectionSchema;
|
|
1587
1823
|
exports.SectionTotalSchema = SectionTotalSchema;
|
|
1588
1824
|
exports.SurfaceKindSchema = SurfaceKindSchema;
|
|
@@ -1599,4 +1835,5 @@ exports.productVariablesOf = productVariablesOf;
|
|
|
1599
1835
|
exports.roomVariables = roomVariables;
|
|
1600
1836
|
exports.suggestNearest = suggestNearest;
|
|
1601
1837
|
exports.toCents = toCents;
|
|
1838
|
+
exports.totalAreaOf = totalAreaOf;
|
|
1602
1839
|
exports.validateFormula = validateFormula;
|