@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/README.md
CHANGED
|
@@ -42,12 +42,12 @@ result.incomplete; // true if any line failed or could not be calculated
|
|
|
42
42
|
|
|
43
43
|
Computes the estimate. It does not validate its input; parse with the schemas and call `checkLimits` first.
|
|
44
44
|
|
|
45
|
-
Lines are produced in this order: base cost, object rules (by `sortOrder`), materials (per room, per selected product), engineering (package items, then selected options), additional options.
|
|
45
|
+
Lines are produced in this order: base cost, object rules (by `sortOrder`), materials (per room, per selected product), engineering (package items, then selected options), additional options. What each part produces depends on the renovation [scope](#renovation-scope).
|
|
46
46
|
|
|
47
|
-
- Base cost:
|
|
48
|
-
- Object rules: `OBJECT` level rules, one line each.
|
|
49
|
-
- Materials: for every room selection with a product, the `MATERIAL_TYPE` rule for the category and the product's material type is used if one exists, otherwise the `CATEGORY` rule. Without either, the line is `NOT_CALCULATED` (`RULE_NOT_FOUND`). Unknown or unavailable products give `UNAVAILABLE`.
|
|
50
|
-
- Engineering and additional: package items marked `includedInBase` become `INCLUDED` lines with a zero amount
|
|
47
|
+
- Base cost: `OBJECT` rules with `baseComponent: true`, one `BASE` line each. Only in `FULL`; a line that is `OK` with a zero amount is left out.
|
|
48
|
+
- Object rules: the other `OBJECT` level rules, one line each.
|
|
49
|
+
- Materials: for every room selection with a product, the `MATERIAL_TYPE` rule for the category and the product's material type is used if one exists, otherwise the `CATEGORY` rule. Without either, the line is `NOT_CALCULATED` (`RULE_NOT_FOUND`). Unknown or unavailable products give `UNAVAILABLE`. Each line carries [components](#line-components).
|
|
50
|
+
- Engineering and additional: package items marked `includedInBase` become `INCLUDED` lines with a zero amount, and are skipped in `PARTIAL`. Options are priced by unit (`PIECE`, `ROOM_SQM`, `ROOM`, `PROJECT`). An option with `priceCoefficientKeys` is priced by the sum of those coefficients in dollars instead of `priceCents`; a missing coefficient gives an `ERROR` line (`UNKNOWN_COEFFICIENT`). An option whose `scopes` do not include the configuration scope is `UNAVAILABLE` with `OPTION_SCOPE_NOT_ALLOWED`.
|
|
51
51
|
|
|
52
52
|
Result fields:
|
|
53
53
|
|
|
@@ -56,7 +56,7 @@ Result fields:
|
|
|
56
56
|
| `pricingVersion` | `data.version` |
|
|
57
57
|
| `currency` | the currency requested in the configuration |
|
|
58
58
|
| `exchangeRate` | `data.exchangeRate` as given |
|
|
59
|
-
| `lines` | `EstimateLine[]` with `id`, `section`, `status`, quantity, unit price, `amountCents`, `amountUahKopecks`, `warnings`, `error` |
|
|
59
|
+
| `lines` | `EstimateLine[]` with `id`, `section`, `status`, quantity, unit price, `amountCents`, `amountUahKopecks`, `components`, `warnings`, `error` |
|
|
60
60
|
| `totals` | `{ usdCents, uahKopecks, sections }`, sums of the line amounts |
|
|
61
61
|
| `warnings` | all line warnings (with `lineId`) plus estimate-level ones |
|
|
62
62
|
| `incomplete` | `true` if any line has status `ERROR` or `NOT_CALCULATED` |
|
|
@@ -67,12 +67,14 @@ Warning codes:
|
|
|
67
67
|
|
|
68
68
|
- `HEATED_FLOOR_INCOMPATIBLE`: a floor product that does not suit a heated floor.
|
|
69
69
|
- `OPTION_ROOM_TYPE_NOT_ALLOWED`: an option applied to a room type it does not support; that room is left out of the quantity.
|
|
70
|
-
- `ROOMS_AREA_EXCEEDS_TOTAL`: the rooms add up to more than the total area.
|
|
70
|
+
- `ROOMS_AREA_EXCEEDS_TOTAL`: the rooms add up to more than the total area. `FULL` only.
|
|
71
|
+
- `ROOMS_AREA_BELOW_TOTAL`: the rooms add up to less than the total area by more than `limits.missingAreaHintSqm`; `params: { missingSqm }`. `FULL` only.
|
|
72
|
+
- `LABOR_RATE_MISSING`: a `PARTIAL` material line whose rule has no `laborFormula`; the labor component is 0.
|
|
71
73
|
- `EXCHANGE_RATE_UNAVAILABLE`: UAH requested but the pricing data has no rate.
|
|
72
74
|
|
|
73
75
|
### `checkLimits(configuration, limits, options?): LimitViolation[]`
|
|
74
76
|
|
|
75
|
-
Returns `{ field, min, max }` for every value outside its range. Checked fields: `totalAreaSqm
|
|
77
|
+
Returns `{ field, min, max }` for every value outside its range. Checked fields: `totalAreaSqm` (`FULL` only), `balconyAreaSqm` (0 to the total area, or to `limits.totalAreaSqm.max` in `PARTIAL`), `rooms` (count), `rooms[i].lengthM`, `rooms[i].widthM`, `rooms[i].heightM`, and `options[i].quantity` for `PIECE` options (`minQuantity` defaults to 1, `maxQuantity` to unbounded). Pass `data.options` to enable the last check.
|
|
76
78
|
|
|
77
79
|
```ts
|
|
78
80
|
const violations = checkLimits(configuration, data.limits, data.options);
|
|
@@ -104,8 +106,8 @@ validateFormula({
|
|
|
104
106
|
// params: { name: 'totlArea', suggestion: 'totalArea' } }]
|
|
105
107
|
```
|
|
106
108
|
|
|
107
|
-
- `level`: `'OBJECT' | 'CATEGORY' | 'MATERIAL_TYPE'`.
|
|
108
|
-
- `kind`: `'quantity'` or `'
|
|
109
|
+
- `level`: `'OBJECT' | 'CATEGORY' | 'MATERIAL_TYPE' | 'SURFACE'`.
|
|
110
|
+
- `kind`: `'quantity'`, `'cost'` or `'labor'`. Only cost and labor formulas of `CATEGORY` and `MATERIAL_TYPE` rules may use `quantity`.
|
|
109
111
|
- `coefficientKeys`: the coefficient keys that exist; `coef.x` with another key is an error.
|
|
110
112
|
- The result's `coefficientKeys` lists the coefficients the formula references.
|
|
111
113
|
- Errors carry `code`, `position`, `length` (a span in the source) and optional `params`.
|
|
@@ -133,26 +135,31 @@ const preview = previewRule({
|
|
|
133
135
|
},
|
|
134
136
|
product: { priceCents: 1800, unit: 'SQM', wastePercent: 5 },
|
|
135
137
|
object: {
|
|
138
|
+
scope: 'FULL',
|
|
136
139
|
totalAreaSqm: 62,
|
|
137
140
|
balconyAreaSqm: 4,
|
|
138
141
|
balconyTiles: true,
|
|
139
|
-
kitchenType: 'SEPARATE',
|
|
140
142
|
finishLevel: 'PRE_FINISH',
|
|
141
143
|
roomsCount: 1,
|
|
142
144
|
bathroomsCount: 1,
|
|
145
|
+
roomsFloorArea: 20,
|
|
146
|
+
roomsWallArea: 48.6,
|
|
147
|
+
roomsPerimeter: 18,
|
|
148
|
+
wetRoomsCount: 0,
|
|
143
149
|
},
|
|
144
150
|
},
|
|
145
151
|
});
|
|
146
152
|
// {
|
|
147
153
|
// quantity: 21,
|
|
148
154
|
// amountCents: 42000,
|
|
155
|
+
// laborCents: null,
|
|
149
156
|
// variables: { floorArea: 20, waste: 0.05, price: 18, quantity: 21 },
|
|
150
157
|
// coefficients: { underlay: 2 },
|
|
151
158
|
// error: null,
|
|
152
159
|
// }
|
|
153
160
|
```
|
|
154
161
|
|
|
155
|
-
`variables` and `coefficients` in the result contain only what the formulas reference. For `OBJECT` rules `quantityFormula` must be `null` and `room`/`product` may be omitted. For
|
|
162
|
+
`variables` and `coefficients` in the result contain only what the formulas reference. For `OBJECT` rules `quantityFormula` must be `null` and `room`/`product` may be omitted. For `SURFACE` rules `quantityFormula` must be `null`, and `sample.room` and `sample.surfaceArea` are required. For `CATEGORY` and `MATERIAL_TYPE` rules `quantityFormula`, `room` and `product` are required. Otherwise `error.code` is `QUANTITY_FORMULA_MISMATCH` or `SAMPLE_INCOMPLETE` (`params.missing` names the absent part, for example `surfaceArea`). `laborFormula` is optional and is evaluated for `CATEGORY` and `MATERIAL_TYPE` rules with the same variables as the cost formula, including `quantity`. Its amount is `laborCents`, which is `null` without a labor formula or when it fails; a labor failure is reported in `error` and `amountCents` keeps the material cost. In a `PARTIAL` sample `totalArea` is `roomsFloorArea`, as in `estimate`, and `sample.object.totalAreaSqm` is required in `FULL` and must be `null` in `PARTIAL`.
|
|
156
163
|
|
|
157
164
|
### Lower-level formula API
|
|
158
165
|
|
|
@@ -171,15 +178,46 @@ const preview = previewRule({
|
|
|
171
178
|
|
|
172
179
|
Every schema is a zod schema with an inferred type of the same name without the `Schema` suffix.
|
|
173
180
|
|
|
174
|
-
- Configuration: `ConfigurationSchema`, `RoomConfigurationSchema`, `RoomSelectionSchema`, `OptionSelectionSchema`. Duplicate room `clientId`s, category ids within a room and option ids are rejected.
|
|
175
|
-
- Pricing data: `PricingDataSchema`, `PricingLimitsSchema`, `
|
|
176
|
-
- Result: `EstimateSchema`, `EstimateLineSchema`, `EstimateTotalsSchema`, `SectionTotalSchema`, `EstimateWarningSchema`, `EstimateMessageSchema`.
|
|
181
|
+
- Configuration: `ConfigurationSchema`, `RoomConfigurationSchema`, `RoomSelectionSchema`, `OptionSelectionSchema`. `totalAreaSqm` is required in `FULL` and must be `null` in `PARTIAL`. Duplicate room `clientId`s, category ids within a room and option ids are rejected.
|
|
182
|
+
- Pricing data: `PricingDataSchema`, `PricingLimitsSchema`, `PricingRuleSchema`, `PricingRoomTypeSchema`, `PricingCategorySchema`, `PricingMaterialTypeSchema`, `PricingProductSchema`, `PricingPackageItemSchema`, `PricingOptionSchema`, `ExchangeRateSchema`.
|
|
183
|
+
- Result: `EstimateSchema`, `EstimateLineSchema`, `LineComponentSchema`, `EstimateTotalsSchema`, `SectionTotalSchema`, `EstimateWarningSchema`, `EstimateMessageSchema`.
|
|
177
184
|
- Preview: `RuleSampleSchema`, `RulePreviewSchema`.
|
|
178
|
-
- Enums and primitives: `FinishLevelSchema`, `RuleLevelSchema`, `RateSourceSchema`, `SurfaceKindSchema`, `ProductUnitSchema`, `OptionUnitSchema`, `OptionKindSchema`, `RoomTypeCodeSchema`, `
|
|
185
|
+
- Enums and primitives: `ScopeSchema`, `LineComponentKindSchema`, `FinishLevelSchema`, `RuleLevelSchema`, `RateSourceSchema`, `SurfaceKindSchema`, `ProductUnitSchema`, `OptionUnitSchema`, `OptionKindSchema`, `RoomTypeCodeSchema`, `CurrencySchema`, `SectionSchema`, `LineStatusSchema`, `LocalizedTextSchema`, `CentsSchema`, `RangeSchema`.
|
|
186
|
+
|
|
187
|
+
## Renovation scope
|
|
188
|
+
|
|
189
|
+
`Configuration.scope` selects what is estimated. It defaults to `FULL`, so configurations saved before the field existed still parse.
|
|
190
|
+
|
|
191
|
+
| | `FULL` | `PARTIAL` |
|
|
192
|
+
| --- | --- | --- |
|
|
193
|
+
| `totalAreaSqm` | required | `null` |
|
|
194
|
+
| `BASE` section | `OBJECT` rules with `baseComponent: true`, one line each | none |
|
|
195
|
+
| Material lines | `MATERIAL` component only | `MATERIAL`, `LABOR` and `PREP` components |
|
|
196
|
+
| Package items with `includedInBase` | `INCLUDED` lines | skipped |
|
|
197
|
+
| `totalArea` variable | `totalAreaSqm` | `roomsFloorArea` |
|
|
198
|
+
| Area warnings | `ROOMS_AREA_EXCEEDS_TOTAL`, `ROOMS_AREA_BELOW_TOTAL` | none |
|
|
199
|
+
|
|
200
|
+
The base cost has no per-area rate table. It is built from rules whose formulas use the volumes of the rooms (`roomsFloorArea`, `roomsWallArea`, `roomsPerimeter`, `wetRoomsCount`) together with `finishLevel`, `scope` and coefficients.
|
|
201
|
+
|
|
202
|
+
### Line components
|
|
203
|
+
|
|
204
|
+
A material line is the sum of its components:
|
|
205
|
+
|
|
206
|
+
- `MATERIAL`: the rule's `costFormula`.
|
|
207
|
+
- `LABOR`: the rule's `laborFormula`, which only `CATEGORY` and `MATERIAL_TYPE` rules can have. It is evaluated in `PARTIAL` only. Without a formula the component is 0 and the line gets `LABOR_RATE_MISSING`.
|
|
208
|
+
- `PREP`: surface preparation, from a `SURFACE` rule.
|
|
209
|
+
|
|
210
|
+
Every component is rounded to whole cents on its own and `amountCents` is the sum of the rounded components. A `FULL` line has a single `MATERIAL` component. Each component carries the `ruleId` of the rule that produced it; ids are plain strings.
|
|
211
|
+
|
|
212
|
+
### Surface preparation
|
|
213
|
+
|
|
214
|
+
A `SURFACE` rule prices the preparation of one surface: `FLOOR`, `WALLS` or `CEILING`. It has a `surface` and no `categoryId` or quantity formula. A category belongs to a surface through `PricingCategory.surface`.
|
|
215
|
+
|
|
216
|
+
In `PARTIAL`, for every room the engine collects the surfaces of the categories with a selected product and evaluates the surface rule once per room and surface, with `surfaceArea` equal to `floorArea` for floor and ceiling and `wallArea` for walls. The result becomes the `PREP` component of the first category on that surface, in the order of the room's `selections`. The other categories on the same surface get a `PREP` component of 0 with `sharedWithCategoryId` pointing at that first category, so the preparation is never counted twice. A surface without a rule gets no `PREP`. If the surface formula fails, every line on that surface fails with that error.
|
|
179
217
|
|
|
180
218
|
## Formula language
|
|
181
219
|
|
|
182
|
-
A formula is a single expression that produces a non-negative number. Each rule has a cost formula; material rules (`CATEGORY`, `MATERIAL_TYPE`) also have a quantity formula, evaluated first. Formulas are written in dollars; the engine converts the result to cents.
|
|
220
|
+
A formula is a single expression that produces a non-negative number. Each rule has a cost formula; material rules (`CATEGORY`, `MATERIAL_TYPE`) also have a quantity formula, evaluated first, and an optional labor formula. Formulas are written in dollars; the engine converts the result to cents.
|
|
183
221
|
|
|
184
222
|
```
|
|
185
223
|
floorArea * (1 + waste)
|
|
@@ -196,23 +234,27 @@ roomType == 'KITCHEN' ? ceil(floorArea / 2) : 0
|
|
|
196
234
|
| room | `length`, `width`, `height` | number, meters |
|
|
197
235
|
| room | `floorArea`, `perimeter`, `wallArea` | number: `length * width`, `2 * (length + width)`, `perimeter * height` |
|
|
198
236
|
| room | `heatedFloor` | boolean |
|
|
199
|
-
| room | `roomType` | `LIVING_ROOM`, `BEDROOM`, `KITCHEN`, `KITCHEN_LIVING`, `BATHROOM` |
|
|
237
|
+
| room | `roomType` | `LIVING_ROOM`, `BEDROOM`, `KITCHEN`, `KITCHEN_LIVING`, `BATHROOM`, `HALLWAY`, `STORAGE` |
|
|
200
238
|
| product | `price` | number, dollars per unit |
|
|
201
239
|
| product | `waste` | number, a fraction (5% is `0.05`): the product override, else the category value |
|
|
202
240
|
| product | `unit` | `SQM`, `LINEAR_M`, `PIECE` |
|
|
203
|
-
| object | `totalArea`, `balconyArea` | number |
|
|
241
|
+
| object | `totalArea`, `balconyArea` | number; `totalArea` is `roomsFloorArea` in `PARTIAL` |
|
|
242
|
+
| object | `scope` | `FULL`, `PARTIAL` |
|
|
243
|
+
| object | `roomsFloorArea`, `roomsWallArea`, `roomsPerimeter` | number: sums over all rooms of `floorArea`, `wallArea` and `perimeter` |
|
|
244
|
+
| object | `wetRoomsCount` | number (bathrooms, kitchens and kitchen-living rooms) |
|
|
204
245
|
| object | `balconyTiles` | boolean |
|
|
205
|
-
| object | `
|
|
206
|
-
| object | `finishLevel` | `ROUGH`, `PRE_FINISH` |
|
|
246
|
+
| object | `finishLevel` | `ROUGH`, `PRE_FINISH`, `LIVED_IN` |
|
|
207
247
|
| object | `roomsCount`, `bathroomsCount` | number (living rooms, bedrooms and kitchen-living rooms count as rooms) |
|
|
208
|
-
|
|
|
248
|
+
| `SURFACE` rules only | `surfaceArea` | number: the area of the surface being prepared |
|
|
249
|
+
| cost and labor formulas of `CATEGORY` and `MATERIAL_TYPE` rules | `quantity` | the result of the quantity formula |
|
|
209
250
|
|
|
210
251
|
Availability by rule level:
|
|
211
252
|
|
|
212
253
|
- `OBJECT`: object variables only. `quantity` is not available.
|
|
213
|
-
- `CATEGORY` and `MATERIAL_TYPE`: room, product and object variables. `quantity` is available in cost formulas
|
|
254
|
+
- `CATEGORY` and `MATERIAL_TYPE`: room, product and object variables. `quantity` is available in cost and labor formulas, not in the quantity formula. `laborFormula` sees the same variables as `costFormula`.
|
|
255
|
+
- `SURFACE`: room and object variables plus `surfaceArea`. `quantity` and the product variables are not available.
|
|
214
256
|
|
|
215
|
-
Coefficients are read as `coef.<key>`; keys come from the pricing data. The cost
|
|
257
|
+
Coefficients are read as `coef.<key>`; keys come from the pricing data. The cost and labor formulas receive `quantity` unrounded, while the line shows it rounded to 3 decimals.
|
|
216
258
|
|
|
217
259
|
### Syntax
|
|
218
260
|
|
|
@@ -240,7 +282,7 @@ Coefficients are read as `coef.<key>`; keys come from the pricing data. The cost
|
|
|
240
282
|
| `VARIABLE_NOT_ALLOWED_AT_LEVEL` | A real variable used on a level that does not provide it, such as `price` in an `OBJECT` rule; `params: { variable, level }` |
|
|
241
283
|
| `UNKNOWN_COEFFICIENT` | `coef.x` with `x` not in `coefficientKeys`; `params: { key }` |
|
|
242
284
|
| `UNKNOWN_FUNCTION` | Unknown function name; `params: { name }` |
|
|
243
|
-
| `QUANTITY_NOT_ALLOWED` | `quantity` in a quantity formula or in an `OBJECT` rule |
|
|
285
|
+
| `QUANTITY_NOT_ALLOWED` | `quantity` in a quantity formula or in an `OBJECT` or `SURFACE` rule |
|
|
244
286
|
|
|
245
287
|
Rejected examples:
|
|
246
288
|
|