@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 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: the base rate matching the finish level with `areaFromSqm <= totalAreaSqm < areaToSqm`, times the total area. No matching rate gives a `NOT_CALCULATED` line with code `BASE_RATE_NOT_FOUND`.
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. Options are priced by unit (`PIECE`, `ROOM_SQM`, `ROOM`, `PROJECT`).
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`, `balconyAreaSqm` (0 to the total area), `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.
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 `'cost'`. Only cost formulas of non-object rules may use `quantity`.
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 the other levels `quantityFormula`, `room` and `product` are required; otherwise `error.code` is `QUANTITY_FORMULA_MISMATCH` or `SAMPLE_INCOMPLETE`.
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`, `BaseRateSchema`, `PricingRuleSchema`, `PricingRoomTypeSchema`, `PricingCategorySchema`, `PricingMaterialTypeSchema`, `PricingProductSchema`, `PricingPackageItemSchema`, `PricingOptionSchema`, `ExchangeRateSchema`.
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`, `KitchenTypeSchema`, `CurrencySchema`, `SectionSchema`, `LineStatusSchema`, `LocalizedTextSchema`, `CentsSchema`, `RangeSchema`.
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 | `kitchenType` | `SEPARATE`, `KITCHEN_LIVING` |
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
- | cost formulas only | `quantity` | the result of the quantity formula |
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 only.
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 formula receives `quantity` unrounded, while the line shows it rounded to 3 decimals.
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