@roomwise/calc-engine 0.5.0 → 0.7.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
@@ -47,7 +47,7 @@ Lines are produced in this order: base cost, object rules (by `sortOrder`), mate
47
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
48
  - Object rules: the other `OBJECT` level rules, one line each.
49
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`.
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 has exactly one price source: `priceCents` with no `priceCoefficientKeys`, or `priceCents: null` with keys, in which case the unit price is the sum of those coefficients in dollars (`PricingDataSchema` rejects both and neither; read the price from the line's `unitPriceCents`). 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
 
@@ -197,7 +197,7 @@ Every schema is a zod schema with an inferred type of the same name without the
197
197
  | `totalArea` variable | `totalAreaSqm` | `roomsFloorArea` |
198
198
  | Area warnings | `ROOMS_AREA_EXCEEDS_TOTAL`, `ROOMS_AREA_BELOW_TOTAL` | none |
199
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.
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`, `roomsPuttyWallArea`, `roomsPerimeter`, `wetRoomsCount`) together with `finishLevel`, `scope` and coefficients.
201
201
 
202
202
  ### Line components
203
203
 
@@ -241,18 +241,20 @@ roomType == 'KITCHEN' ? ceil(floorArea / 2) : 0
241
241
  | object | `totalArea`, `balconyArea` | number; `totalArea` is `roomsFloorArea` in `PARTIAL` |
242
242
  | object | `scope` | `FULL`, `PARTIAL` |
243
243
  | object | `roomsFloorArea`, `roomsWallArea`, `roomsPerimeter` | number: sums over all rooms of `floorArea`, `wallArea` and `perimeter` |
244
+ | object | `roomsPuttyWallArea` | number: sum of `wallArea` over rooms whose wall selections need putty (rooms without wall selections count) |
244
245
  | object | `wetRoomsCount` | number (bathrooms, kitchens and kitchen-living rooms) |
245
246
  | object | `balconyTiles` | boolean |
246
247
  | object | `finishLevel` | `ROUGH`, `PRE_FINISH`, `LIVED_IN` |
247
248
  | object | `roomsCount`, `bathroomsCount` | number (living rooms, bedrooms and kitchen-living rooms count as rooms) |
248
249
  | `SURFACE` rules only | `surfaceArea` | number: the area of the surface being prepared |
250
+ | `SURFACE` rules only | `puttyRequired` | boolean: `true` when no category is selected on the surface or at least one selected category has `requiresPutty` (default `true`), taken from the selections, not from line status; booleans do not coerce, write `puttyRequired ? a : 0` |
249
251
  | cost and labor formulas of `CATEGORY` and `MATERIAL_TYPE` rules | `quantity` | the result of the quantity formula |
250
252
 
251
253
  Availability by rule level:
252
254
 
253
255
  - `OBJECT`: object variables only. `quantity` is not available.
254
256
  - `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.
257
+ - `SURFACE`: room and object variables plus `surfaceArea` and `puttyRequired`. `quantity` and the product variables are not available.
256
258
 
257
259
  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.
258
260
 
package/dist/index.cjs CHANGED
@@ -52,6 +52,9 @@ function toCents(dollars) {
52
52
  }
53
53
  //#endregion
54
54
  //#region src/variables/room.ts
55
+ function surfaceAreaOf(variables, surface) {
56
+ return surface === "WALLS" ? variables.wallArea : variables.floorArea;
57
+ }
55
58
  function roomVariables(room) {
56
59
  const perimeter = 2 * (room.lengthM + room.widthM);
57
60
  return {
@@ -66,6 +69,63 @@ function roomVariables(room) {
66
69
  };
67
70
  }
68
71
  //#endregion
72
+ //#region src/variables/putty.ts
73
+ function puttyRequiredOn(room, surface, categories) {
74
+ const selected = categories.filter((category) => category.surface === surface && room.selections.some((selection) => selection.categoryId === category.id));
75
+ return selected.length === 0 || selected.some((category) => category.requiresPutty);
76
+ }
77
+ //#endregion
78
+ //#region src/variables/share.ts
79
+ function isShareSurface(surface) {
80
+ return surface === "WALLS" || surface === "FLOOR";
81
+ }
82
+ function surfaceShares(room, surface, categories) {
83
+ const selections = room.selections.filter((selection) => selection.productId !== null && categories.some((category) => category.id === selection.categoryId && category.surface === surface));
84
+ return {
85
+ selections,
86
+ shared: selections.some((selection) => selection.areaShare !== void 0),
87
+ complete: selections.every((selection) => selection.areaShare !== void 0),
88
+ totalPercent: selections.reduce((sum, selection) => sum + (selection.areaShare ?? 0), 0)
89
+ };
90
+ }
91
+ function areaShareOf(selection) {
92
+ return selection.areaShare === void 0 ? 1 : selection.areaShare / 100;
93
+ }
94
+ function areaShareError(room, selection, categories) {
95
+ const category = categories.find((item) => item.id === selection.categoryId);
96
+ if (category === void 0) return null;
97
+ if (!isShareSurface(category.surface)) return selection.areaShare === void 0 ? null : {
98
+ code: "AREA_SHARE_NOT_ALLOWED",
99
+ params: {
100
+ categoryId: category.id,
101
+ surface: category.surface
102
+ }
103
+ };
104
+ const shares = surfaceShares(room, category.surface, categories);
105
+ const params = {
106
+ roomClientId: room.clientId,
107
+ surface: category.surface
108
+ };
109
+ if (shares.shared && !shares.complete) return {
110
+ code: "AREA_SHARE_MISSING",
111
+ params
112
+ };
113
+ if (shares.totalPercent > 100) return {
114
+ code: "AREA_SHARE_EXCEEDS_SURFACE",
115
+ params: {
116
+ ...params,
117
+ percent: shares.totalPercent
118
+ }
119
+ };
120
+ return null;
121
+ }
122
+ function puttyShareOn(room, surface, categories) {
123
+ const shares = surface === "WALLS" ? surfaceShares(room, surface, categories) : null;
124
+ if (shares === null || !shares.shared) return puttyRequiredOn(room, surface, categories) ? 1 : 0;
125
+ const needing = shares.selections.filter((selection) => categories.find((category) => category.id === selection.categoryId)?.requiresPutty);
126
+ return Math.min(1, needing.reduce((sum, selection) => sum + areaShareOf(selection), 0));
127
+ }
128
+ //#endregion
69
129
  //#region src/variables/object.ts
70
130
  const COUNTED_AS_ROOMS = [
71
131
  "LIVING_ROOM",
@@ -80,7 +140,7 @@ const WET_ROOMS = [
80
140
  function totalAreaOf(scope, totalAreaSqm, roomsFloorArea) {
81
141
  return scope === "PARTIAL" || totalAreaSqm === null ? roomsFloorArea : totalAreaSqm;
82
142
  }
83
- function objectVariables(configuration) {
143
+ function objectVariables(configuration, categories) {
84
144
  const rooms = configuration.rooms.map(roomVariables);
85
145
  const sum = (pick) => rooms.reduce((total, room) => total + pick(room), 0);
86
146
  const roomsFloorArea = sum((room) => room.floorArea);
@@ -94,6 +154,7 @@ function objectVariables(configuration) {
94
154
  bathroomsCount: configuration.rooms.filter((room) => room.roomTypeCode === "BATHROOM").length,
95
155
  roomsFloorArea,
96
156
  roomsWallArea: sum((room) => room.wallArea),
157
+ roomsPuttyWallArea: configuration.rooms.reduce((total, room) => total + roomVariables(room).wallArea * puttyShareOn(room, "WALLS", categories), 0),
97
158
  roomsPerimeter: sum((room) => room.perimeter),
98
159
  wetRoomsCount: rooms.filter((room) => WET_ROOMS.includes(room.roomType)).length
99
160
  };
@@ -726,23 +787,25 @@ function applySurfacePrep(data, room, lines, objectScope) {
726
787
  ...roomVariables(room),
727
788
  ...objectScope
728
789
  };
729
- const areas = {
730
- FLOOR: variables.floorArea,
731
- CEILING: variables.floorArea,
732
- WALLS: variables.wallArea
733
- };
734
790
  const surfaces = /* @__PURE__ */ new Map();
735
791
  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]);
792
+ const category = data.categories.find((item) => item.id === line.categoryId);
793
+ if (line.status === "OK" && category !== void 0 && category.surface !== "NONE") {
794
+ const surface = category.surface;
795
+ surfaces.set(surface, [...surfaces.get(surface) ?? [], index]);
796
+ }
738
797
  });
739
798
  for (const [surface, indexes] of surfaces) {
740
799
  const rule = surfaceRuleOf(data, surface);
741
800
  if (rule === void 0) continue;
801
+ const surfaceArea = surfaceAreaOf(variables, surface);
802
+ const puttyShare = puttyShareOn(room, surface, data.categories);
742
803
  const outcome = runNumericFormula(rule.costFormula, {
743
804
  variables: {
744
805
  ...variables,
745
- surfaceArea: areas[surface]
806
+ surfaceArea,
807
+ puttyRequired: puttyShare > 0,
808
+ puttyArea: surfaceArea * puttyShare
746
809
  },
747
810
  coefficients: data.coefficients
748
811
  });
@@ -775,7 +838,9 @@ function selectRule(data, categoryId, materialTypeId) {
775
838
  const rules = data.rules.filter(isMaterialRule);
776
839
  return rules.find((rule) => rule.level === "MATERIAL_TYPE" && rule.categoryId === categoryId && rule.materialTypeId === materialTypeId) ?? rules.find((rule) => rule.level === "CATEGORY" && rule.categoryId === categoryId);
777
840
  }
778
- function materialLine(configuration, data, room, categoryId, productId, objectScope) {
841
+ function materialLine(configuration, data, room, selection, objectScope) {
842
+ const { categoryId } = selection;
843
+ const productId = selection.productId;
779
844
  const init = {
780
845
  id: lineId("MATERIALS", room.clientId, categoryId),
781
846
  section: "MATERIALS",
@@ -783,6 +848,8 @@ function materialLine(configuration, data, room, categoryId, productId, objectSc
783
848
  categoryId,
784
849
  productId
785
850
  };
851
+ const shareError = areaShareError(room, selection, data.categories);
852
+ if (shareError !== null) return failedLine(init, "ERROR", shareError);
786
853
  const category = data.categories.find((item) => item.id === categoryId);
787
854
  const product = data.products.find((item) => item.id === productId && item.categoryId === categoryId);
788
855
  if (product === void 0 || category === void 0 || !product.available) return failedLine(init, "UNAVAILABLE", {
@@ -804,8 +871,12 @@ function materialLine(configuration, data, room, categoryId, productId, objectSc
804
871
  materialTypeId: product.materialTypeId
805
872
  }
806
873
  });
874
+ const base = roomVariables(room);
875
+ const share = areaShareOf(selection);
807
876
  const variables = {
808
- ...roomVariables(room),
877
+ ...base,
878
+ wallArea: category.surface === "WALLS" ? base.wallArea * share : base.wallArea,
879
+ floorArea: category.surface === "FLOOR" ? base.floorArea * share : base.floorArea,
809
880
  ...productVariables(product, category),
810
881
  ...objectScope
811
882
  };
@@ -875,7 +946,7 @@ function materialLine(configuration, data, room, categoryId, productId, objectSc
875
946
  }
876
947
  function materialLines(configuration, data, objectScope) {
877
948
  return configuration.rooms.flatMap((room) => {
878
- const lines = room.selections.flatMap(({ categoryId, productId }) => productId === null ? [] : [materialLine(configuration, data, room, categoryId, productId, objectScope)]);
949
+ const lines = room.selections.flatMap((selection) => selection.productId === null ? [] : [materialLine(configuration, data, room, selection, objectScope)]);
879
950
  return configuration.scope === "PARTIAL" ? applySurfacePrep(data, room, lines, objectScope) : lines;
880
951
  });
881
952
  }
@@ -973,7 +1044,8 @@ const RangeSchema = zod.z.object({
973
1044
  //#region src/schemas/configuration.ts
974
1045
  const RoomSelectionSchema = zod.z.object({
975
1046
  categoryId: zod.z.string(),
976
- productId: zod.z.string().nullable()
1047
+ productId: zod.z.string().nullable(),
1048
+ areaShare: zod.z.int().min(1).max(100).optional()
977
1049
  });
978
1050
  const RoomConfigurationSchema = zod.z.object({
979
1051
  clientId: zod.z.string(),
@@ -1077,7 +1149,8 @@ const PricingRoomTypeSchema = zod.z.object({
1077
1149
  const PricingCategorySchema = zod.z.object({
1078
1150
  id: zod.z.string(),
1079
1151
  wastePercent: zod.z.number().min(0),
1080
- surface: SurfaceKindSchema
1152
+ surface: SurfaceKindSchema,
1153
+ requiresPutty: zod.z.boolean().default(true)
1081
1154
  });
1082
1155
  const PricingMaterialTypeSchema = zod.z.object({
1083
1156
  id: zod.z.string(),
@@ -1099,11 +1172,11 @@ const PricingPackageItemSchema = zod.z.object({
1099
1172
  priceCents: CentsSchema.nullable(),
1100
1173
  unit: OptionUnitSchema.nullable()
1101
1174
  });
1102
- const PricingOptionSchema = zod.z.object({
1175
+ const PricingOptionObjectSchema = zod.z.object({
1103
1176
  id: zod.z.string(),
1104
1177
  code: OptionCodeSchema.nullable(),
1105
1178
  kind: OptionKindSchema,
1106
- priceCents: CentsSchema,
1179
+ priceCents: CentsSchema.nullable(),
1107
1180
  unit: OptionUnitSchema,
1108
1181
  roomTypeCodes: zod.z.array(zod.z.string()),
1109
1182
  priceCoefficientKeys: zod.z.array(zod.z.string()),
@@ -1112,6 +1185,26 @@ const PricingOptionSchema = zod.z.object({
1112
1185
  maxQuantity: zod.z.int().nullable(),
1113
1186
  available: zod.z.boolean()
1114
1187
  });
1188
+ function findOptionPriceSourceViolation(priceCents, priceCoefficientKeys) {
1189
+ const hasKeys = priceCoefficientKeys.length > 0;
1190
+ if (priceCents !== null && hasKeys) return {
1191
+ path: "priceCents",
1192
+ message: "priceCents must be null when priceCoefficientKeys is set"
1193
+ };
1194
+ if (priceCents === null && !hasKeys) return {
1195
+ path: "priceCoefficientKeys",
1196
+ message: "priceCoefficientKeys is required when priceCents is null"
1197
+ };
1198
+ return null;
1199
+ }
1200
+ const PricingOptionSchema = PricingOptionObjectSchema.superRefine((option, context) => {
1201
+ const violation = findOptionPriceSourceViolation(option.priceCents, option.priceCoefficientKeys);
1202
+ if (violation !== null) context.addIssue({
1203
+ code: "custom",
1204
+ path: [violation.path],
1205
+ message: violation.message
1206
+ });
1207
+ });
1115
1208
  const PricingDataSchema = zod.z.object({
1116
1209
  version: zod.z.string().regex(/^[0-9a-f]{16}$/),
1117
1210
  exchangeRate: ExchangeRateSchema.nullable(),
@@ -1191,6 +1284,8 @@ const RuleSampleSchema = zod.z.object({
1191
1284
  wastePercent: zod.z.number().min(0)
1192
1285
  }).optional(),
1193
1286
  surfaceArea: zod.z.number().min(0).optional(),
1287
+ puttyRequired: zod.z.boolean().optional(),
1288
+ puttyArea: zod.z.number().min(0).optional(),
1194
1289
  object: zod.z.object({
1195
1290
  scope: ScopeSchema,
1196
1291
  totalAreaSqm: zod.z.number().positive().nullable(),
@@ -1201,6 +1296,7 @@ const RuleSampleSchema = zod.z.object({
1201
1296
  bathroomsCount: zod.z.int().min(0),
1202
1297
  roomsFloorArea: zod.z.number().min(0),
1203
1298
  roomsWallArea: zod.z.number().min(0),
1299
+ roomsPuttyWallArea: zod.z.number().min(0).optional(),
1204
1300
  roomsPerimeter: zod.z.number().min(0),
1205
1301
  wetRoomsCount: zod.z.int().min(0)
1206
1302
  })
@@ -1268,10 +1364,32 @@ function roomsAreaWarnings(configuration, limits) {
1268
1364
  params: { missingSqm }
1269
1365
  }] : [];
1270
1366
  }
1271
- function estimateWarnings(lines, configuration, rate, limits) {
1367
+ const SHARE_SURFACES = ["WALLS", "FLOOR"];
1368
+ function areaShareWarnings(configuration, categories) {
1369
+ return configuration.rooms.flatMap((room) => {
1370
+ const variables = roomVariables(room);
1371
+ return SHARE_SURFACES.flatMap((surface) => {
1372
+ const shares = surfaceShares(room, surface, categories);
1373
+ if (!shares.shared || !shares.complete || shares.totalPercent >= 100) return [];
1374
+ const percent = 100 - shares.totalPercent;
1375
+ const area = surfaceAreaOf(variables, surface);
1376
+ return [{
1377
+ code: "AREA_SHARE_UNALLOCATED",
1378
+ params: {
1379
+ roomClientId: room.clientId,
1380
+ surface,
1381
+ percent,
1382
+ sqm: roundArea(area * percent / 100)
1383
+ }
1384
+ }];
1385
+ });
1386
+ });
1387
+ }
1388
+ function estimateWarnings(lines, configuration, rate, data) {
1272
1389
  return [
1273
1390
  ...collectWarnings(lines),
1274
- ...roomsAreaWarnings(configuration, limits),
1391
+ ...roomsAreaWarnings(configuration, data.limits),
1392
+ ...areaShareWarnings(configuration, data.categories),
1275
1393
  ...configuration.currency === "UAH" && rate === null ? [{ code: "EXCHANGE_RATE_UNAVAILABLE" }] : []
1276
1394
  ];
1277
1395
  }
@@ -1285,7 +1403,7 @@ function withKopecks(line, rate) {
1285
1403
  }
1286
1404
  function estimate(configuration, data) {
1287
1405
  const rate = data.exchangeRate?.rate ?? null;
1288
- const variables = objectVariables(configuration);
1406
+ const variables = objectVariables(configuration, data.categories);
1289
1407
  const lines = [
1290
1408
  ...objectRuleLines(configuration, data, "BASE", variables),
1291
1409
  ...objectRuleLines(configuration, data, "OBJECT", variables),
@@ -1299,7 +1417,7 @@ function estimate(configuration, data) {
1299
1417
  exchangeRate: data.exchangeRate,
1300
1418
  lines,
1301
1419
  totals: computeTotals(lines, rate !== null),
1302
- warnings: estimateWarnings(lines, configuration, rate, data.limits),
1420
+ warnings: estimateWarnings(lines, configuration, rate, data),
1303
1421
  incomplete: isIncomplete(lines)
1304
1422
  };
1305
1423
  }
@@ -1381,6 +1499,7 @@ function objectScope({ object }) {
1381
1499
  bathroomsCount: object.bathroomsCount,
1382
1500
  roomsFloorArea: object.roomsFloorArea,
1383
1501
  roomsWallArea: object.roomsWallArea,
1502
+ roomsPuttyWallArea: object.roomsPuttyWallArea ?? object.roomsWallArea,
1384
1503
  roomsPerimeter: object.roomsPerimeter,
1385
1504
  wetRoomsCount: object.wetRoomsCount
1386
1505
  };
@@ -1393,10 +1512,13 @@ function scopeOf(level, sample) {
1393
1512
  code: "SAMPLE_INCOMPLETE",
1394
1513
  params: { missing: room === void 0 ? "room" : "surfaceArea" }
1395
1514
  } };
1515
+ const puttyRequired = sample.puttyRequired ?? true;
1396
1516
  return { scope: {
1397
1517
  ...roomVariables(room),
1398
1518
  ...objectScope(sample),
1399
- surfaceArea
1519
+ surfaceArea,
1520
+ puttyRequired,
1521
+ puttyArea: sample.puttyArea ?? (puttyRequired ? surfaceArea : 0)
1400
1522
  } };
1401
1523
  }
1402
1524
  if (room === void 0 || product === void 0) return { error: {
@@ -1538,12 +1660,15 @@ const FORMULA_SCOPE_VARIABLES = {
1538
1660
  enumeration("scope", ScopeSchema.options),
1539
1661
  number("roomsFloorArea"),
1540
1662
  number("roomsWallArea"),
1663
+ number("roomsPuttyWallArea"),
1541
1664
  number("roomsPerimeter"),
1542
1665
  number("wetRoomsCount")
1543
1666
  ]
1544
1667
  };
1545
1668
  const FORMULA_QUANTITY_VARIABLE = number("quantity");
1546
1669
  const FORMULA_SURFACE_AREA_VARIABLE = number("surfaceArea");
1670
+ const FORMULA_PUTTY_AREA_VARIABLE = number("puttyArea");
1671
+ const FORMULA_PUTTY_REQUIRED_VARIABLE = boolean("puttyRequired");
1547
1672
  const FORMULA_VARIABLES = {
1548
1673
  OBJECT: FORMULA_SCOPE_VARIABLES.object,
1549
1674
  CATEGORY: [
@@ -1559,7 +1684,9 @@ const FORMULA_VARIABLES = {
1559
1684
  SURFACE: [
1560
1685
  ...FORMULA_SCOPE_VARIABLES.room,
1561
1686
  ...FORMULA_SCOPE_VARIABLES.object,
1562
- FORMULA_SURFACE_AREA_VARIABLE
1687
+ FORMULA_SURFACE_AREA_VARIABLE,
1688
+ FORMULA_PUTTY_REQUIRED_VARIABLE,
1689
+ FORMULA_PUTTY_AREA_VARIABLE
1563
1690
  ]
1564
1691
  };
1565
1692
  const ALL_VARIABLE_NAMES = new Set([...FORMULA_VARIABLES.CATEGORY, ...FORMULA_VARIABLES.SURFACE].map((variable) => variable.name));
@@ -1787,6 +1914,8 @@ exports.ExchangeRateSchema = ExchangeRateSchema;
1787
1914
  exports.FORMULA_FUNCTIONS = FORMULA_FUNCTIONS;
1788
1915
  exports.FORMULA_FUNCTION_TABLE = FORMULA_FUNCTION_TABLE;
1789
1916
  exports.FORMULA_LIMITS = FORMULA_LIMITS;
1917
+ exports.FORMULA_PUTTY_AREA_VARIABLE = FORMULA_PUTTY_AREA_VARIABLE;
1918
+ exports.FORMULA_PUTTY_REQUIRED_VARIABLE = FORMULA_PUTTY_REQUIRED_VARIABLE;
1790
1919
  exports.FORMULA_QUANTITY_VARIABLE = FORMULA_QUANTITY_VARIABLE;
1791
1920
  exports.FORMULA_SCOPE_VARIABLES = FORMULA_SCOPE_VARIABLES;
1792
1921
  exports.FORMULA_SURFACE_AREA_VARIABLE = FORMULA_SURFACE_AREA_VARIABLE;
@@ -1822,18 +1951,26 @@ exports.ScopeSchema = ScopeSchema;
1822
1951
  exports.SectionSchema = SectionSchema;
1823
1952
  exports.SectionTotalSchema = SectionTotalSchema;
1824
1953
  exports.SurfaceKindSchema = SurfaceKindSchema;
1954
+ exports.areaShareError = areaShareError;
1955
+ exports.areaShareOf = areaShareOf;
1825
1956
  exports.checkLimits = checkLimits;
1826
1957
  exports.convertCentsToKopecks = convertCentsToKopecks;
1827
1958
  exports.estimate = estimate;
1828
1959
  exports.evaluate = evaluate;
1960
+ exports.findOptionPriceSourceViolation = findOptionPriceSourceViolation;
1961
+ exports.isShareSurface = isShareSurface;
1829
1962
  exports.levenshtein = levenshtein;
1830
1963
  exports.objectVariables = objectVariables;
1831
1964
  exports.parse = parse;
1832
1965
  exports.previewRule = previewRule;
1833
1966
  exports.productVariables = productVariables;
1834
1967
  exports.productVariablesOf = productVariablesOf;
1968
+ exports.puttyRequiredOn = puttyRequiredOn;
1969
+ exports.puttyShareOn = puttyShareOn;
1835
1970
  exports.roomVariables = roomVariables;
1836
1971
  exports.suggestNearest = suggestNearest;
1972
+ exports.surfaceAreaOf = surfaceAreaOf;
1973
+ exports.surfaceShares = surfaceShares;
1837
1974
  exports.toCents = toCents;
1838
1975
  exports.totalAreaOf = totalAreaOf;
1839
1976
  exports.validateFormula = validateFormula;
package/dist/index.d.cts CHANGED
@@ -29,10 +29,10 @@ export declare const RateSourceSchema: z.ZodEnum<{
29
29
  }>;
30
30
  export type RateSource = z.infer<typeof RateSourceSchema>;
31
31
  export declare const SurfaceKindSchema: z.ZodEnum<{
32
+ NONE: "NONE";
32
33
  FLOOR: "FLOOR";
33
34
  WALLS: "WALLS";
34
35
  CEILING: "CEILING";
35
- NONE: "NONE";
36
36
  }>;
37
37
  export type SurfaceKind = z.infer<typeof SurfaceKindSchema>;
38
38
  export declare const ProductUnitSchema: z.ZodEnum<{
@@ -106,6 +106,7 @@ export type Range = z.infer<typeof RangeSchema>;
106
106
  export declare const RoomSelectionSchema: z.ZodObject<{
107
107
  categoryId: z.ZodString;
108
108
  productId: z.ZodNullable<z.ZodString>;
109
+ areaShare: z.ZodOptional<z.ZodInt>;
109
110
  }, z.core.$strip>;
110
111
  export type RoomSelection = z.infer<typeof RoomSelectionSchema>;
111
112
  export declare const RoomConfigurationSchema: z.ZodObject<{
@@ -127,6 +128,7 @@ export declare const RoomConfigurationSchema: z.ZodObject<{
127
128
  selections: z.ZodArray<z.ZodObject<{
128
129
  categoryId: z.ZodString;
129
130
  productId: z.ZodNullable<z.ZodString>;
131
+ areaShare: z.ZodOptional<z.ZodInt>;
130
132
  }, z.core.$strip>>;
131
133
  }, z.core.$strip>;
132
134
  export type RoomConfiguration = z.infer<typeof RoomConfigurationSchema>;
@@ -168,6 +170,7 @@ export declare const ConfigurationSchema: z.ZodObject<{
168
170
  selections: z.ZodArray<z.ZodObject<{
169
171
  categoryId: z.ZodString;
170
172
  productId: z.ZodNullable<z.ZodString>;
173
+ areaShare: z.ZodOptional<z.ZodInt>;
171
174
  }, z.core.$strip>>;
172
175
  }, z.core.$strip>>;
173
176
  options: z.ZodArray<z.ZodObject<{
@@ -382,6 +385,8 @@ export declare const RuleSampleSchema: z.ZodObject<{
382
385
  wastePercent: z.ZodNumber;
383
386
  }, z.core.$strip>>;
384
387
  surfaceArea: z.ZodOptional<z.ZodNumber>;
388
+ puttyRequired: z.ZodOptional<z.ZodBoolean>;
389
+ puttyArea: z.ZodOptional<z.ZodNumber>;
385
390
  object: z.ZodObject<{
386
391
  scope: z.ZodEnum<{
387
392
  FULL: "FULL";
@@ -399,6 +404,7 @@ export declare const RuleSampleSchema: z.ZodObject<{
399
404
  bathroomsCount: z.ZodInt;
400
405
  roomsFloorArea: z.ZodNumber;
401
406
  roomsWallArea: z.ZodNumber;
407
+ roomsPuttyWallArea: z.ZodOptional<z.ZodNumber>;
402
408
  roomsPerimeter: z.ZodNumber;
403
409
  wetRoomsCount: z.ZodInt;
404
410
  }, z.core.$strip>;
@@ -482,11 +488,12 @@ export declare const PricingCategorySchema: z.ZodObject<{
482
488
  id: z.ZodString;
483
489
  wastePercent: z.ZodNumber;
484
490
  surface: z.ZodEnum<{
491
+ NONE: "NONE";
485
492
  FLOOR: "FLOOR";
486
493
  WALLS: "WALLS";
487
494
  CEILING: "CEILING";
488
- NONE: "NONE";
489
495
  }>;
496
+ requiresPutty: z.ZodDefault<z.ZodBoolean>;
490
497
  }, z.core.$strip>;
491
498
  export type PricingCategory = z.infer<typeof PricingCategorySchema>;
492
499
  export declare const PricingMaterialTypeSchema: z.ZodObject<{
@@ -521,6 +528,10 @@ export declare const PricingPackageItemSchema: z.ZodObject<{
521
528
  }>>;
522
529
  }, z.core.$strip>;
523
530
  export type PricingPackageItem = z.infer<typeof PricingPackageItemSchema>;
531
+ export declare function findOptionPriceSourceViolation(priceCents: number | null, priceCoefficientKeys: readonly string[]): {
532
+ path: 'priceCents' | 'priceCoefficientKeys';
533
+ message: string;
534
+ } | null;
524
535
  export declare const PricingOptionSchema: z.ZodObject<{
525
536
  id: z.ZodString;
526
537
  code: z.ZodNullable<z.ZodEnum<{
@@ -530,7 +541,7 @@ export declare const PricingOptionSchema: z.ZodObject<{
530
541
  ENGINEERING: "ENGINEERING";
531
542
  ADDITIONAL: "ADDITIONAL";
532
543
  }>;
533
- priceCents: z.ZodInt;
544
+ priceCents: z.ZodNullable<z.ZodInt>;
534
545
  unit: z.ZodEnum<{
535
546
  PIECE: "PIECE";
536
547
  ROOM_SQM: "ROOM_SQM";
@@ -611,11 +622,12 @@ export declare const PricingDataSchema: z.ZodObject<{
611
622
  id: z.ZodString;
612
623
  wastePercent: z.ZodNumber;
613
624
  surface: z.ZodEnum<{
625
+ NONE: "NONE";
614
626
  FLOOR: "FLOOR";
615
627
  WALLS: "WALLS";
616
628
  CEILING: "CEILING";
617
- NONE: "NONE";
618
629
  }>;
630
+ requiresPutty: z.ZodDefault<z.ZodBoolean>;
619
631
  }, z.core.$strip>>;
620
632
  materialTypes: z.ZodArray<z.ZodObject<{
621
633
  id: z.ZodString;
@@ -655,7 +667,7 @@ export declare const PricingDataSchema: z.ZodObject<{
655
667
  ENGINEERING: "ENGINEERING";
656
668
  ADDITIONAL: "ADDITIONAL";
657
669
  }>;
658
- priceCents: z.ZodInt;
670
+ priceCents: z.ZodNullable<z.ZodInt>;
659
671
  unit: z.ZodEnum<{
660
672
  PIECE: "PIECE";
661
673
  ROOM_SQM: "ROOM_SQM";
@@ -812,6 +824,8 @@ export interface FormulaVariable {
812
824
  export declare const FORMULA_SCOPE_VARIABLES: Record<FormulaScope, readonly FormulaVariable[]>;
813
825
  export declare const FORMULA_QUANTITY_VARIABLE: FormulaVariable;
814
826
  export declare const FORMULA_SURFACE_AREA_VARIABLE: FormulaVariable;
827
+ export declare const FORMULA_PUTTY_AREA_VARIABLE: FormulaVariable;
828
+ export declare const FORMULA_PUTTY_REQUIRED_VARIABLE: FormulaVariable;
815
829
  export declare const FORMULA_VARIABLES: Record<RuleLevel, readonly FormulaVariable[]>;
816
830
  export interface ValidateFormulaInput {
817
831
  readonly level: RuleLevel;
@@ -843,11 +857,12 @@ export interface ObjectVariables {
843
857
  readonly bathroomsCount: number;
844
858
  readonly roomsFloorArea: number;
845
859
  readonly roomsWallArea: number;
860
+ readonly roomsPuttyWallArea: number;
846
861
  readonly roomsPerimeter: number;
847
862
  readonly wetRoomsCount: number;
848
863
  }
849
864
  export declare function totalAreaOf(scope: Scope, totalAreaSqm: number | null, roomsFloorArea: number): number;
850
- export declare function objectVariables(configuration: Configuration): ObjectVariables;
865
+ export declare function objectVariables(configuration: Configuration, categories: readonly PricingCategory[]): ObjectVariables;
851
866
  //#endregion
852
867
  //#region src/variables/product.d.ts
853
868
  export interface ProductVariables {
@@ -858,6 +873,9 @@ export interface ProductVariables {
858
873
  export declare function productVariablesOf(product: Pick<PricingProduct, 'priceCents' | 'unit'>, wastePercent: number): ProductVariables;
859
874
  export declare function productVariables(product: PricingProduct, category: PricingCategory): ProductVariables;
860
875
  //#endregion
876
+ //#region src/variables/putty.d.ts
877
+ export declare function puttyRequiredOn(room: RoomConfiguration, surface: PricingCategory['surface'], categories: readonly PricingCategory[]): boolean;
878
+ //#endregion
861
879
  //#region src/variables/room.d.ts
862
880
  export interface RoomVariables {
863
881
  readonly length: number;
@@ -869,6 +887,22 @@ export interface RoomVariables {
869
887
  readonly heatedFloor: boolean;
870
888
  readonly roomType: RoomTypeCode;
871
889
  }
890
+ export type MeasuredSurface = 'FLOOR' | 'WALLS' | 'CEILING';
891
+ export declare function surfaceAreaOf(variables: Pick<RoomVariables, 'floorArea' | 'wallArea'>, surface: MeasuredSurface): number;
872
892
  export type RoomDimensions = Pick<RoomConfiguration, 'roomTypeCode' | 'lengthM' | 'widthM' | 'heightM' | 'heatedFloor'>;
873
893
  export declare function roomVariables(room: RoomDimensions): RoomVariables;
894
+ //#endregion
895
+ //#region src/variables/share.d.ts
896
+ export type ShareSurface = 'WALLS' | 'FLOOR';
897
+ export interface SurfaceShares {
898
+ readonly selections: readonly RoomSelection[];
899
+ readonly shared: boolean;
900
+ readonly complete: boolean;
901
+ readonly totalPercent: number;
902
+ }
903
+ export declare function isShareSurface(surface: PricingCategory['surface']): surface is ShareSurface;
904
+ export declare function surfaceShares(room: RoomConfiguration, surface: ShareSurface, categories: readonly PricingCategory[]): SurfaceShares;
905
+ export declare function areaShareOf(selection: RoomSelection): number;
906
+ export declare function areaShareError(room: RoomConfiguration, selection: RoomSelection, categories: readonly PricingCategory[]): EstimateMessage | null;
907
+ export declare function puttyShareOn(room: RoomConfiguration, surface: MeasuredSurface, categories: readonly PricingCategory[]): number;
874
908
  //#endregion
package/dist/index.d.ts CHANGED
@@ -29,10 +29,10 @@ export declare const RateSourceSchema: z.ZodEnum<{
29
29
  }>;
30
30
  export type RateSource = z.infer<typeof RateSourceSchema>;
31
31
  export declare const SurfaceKindSchema: z.ZodEnum<{
32
+ NONE: "NONE";
32
33
  FLOOR: "FLOOR";
33
34
  WALLS: "WALLS";
34
35
  CEILING: "CEILING";
35
- NONE: "NONE";
36
36
  }>;
37
37
  export type SurfaceKind = z.infer<typeof SurfaceKindSchema>;
38
38
  export declare const ProductUnitSchema: z.ZodEnum<{
@@ -106,6 +106,7 @@ export type Range = z.infer<typeof RangeSchema>;
106
106
  export declare const RoomSelectionSchema: z.ZodObject<{
107
107
  categoryId: z.ZodString;
108
108
  productId: z.ZodNullable<z.ZodString>;
109
+ areaShare: z.ZodOptional<z.ZodInt>;
109
110
  }, z.core.$strip>;
110
111
  export type RoomSelection = z.infer<typeof RoomSelectionSchema>;
111
112
  export declare const RoomConfigurationSchema: z.ZodObject<{
@@ -127,6 +128,7 @@ export declare const RoomConfigurationSchema: z.ZodObject<{
127
128
  selections: z.ZodArray<z.ZodObject<{
128
129
  categoryId: z.ZodString;
129
130
  productId: z.ZodNullable<z.ZodString>;
131
+ areaShare: z.ZodOptional<z.ZodInt>;
130
132
  }, z.core.$strip>>;
131
133
  }, z.core.$strip>;
132
134
  export type RoomConfiguration = z.infer<typeof RoomConfigurationSchema>;
@@ -168,6 +170,7 @@ export declare const ConfigurationSchema: z.ZodObject<{
168
170
  selections: z.ZodArray<z.ZodObject<{
169
171
  categoryId: z.ZodString;
170
172
  productId: z.ZodNullable<z.ZodString>;
173
+ areaShare: z.ZodOptional<z.ZodInt>;
171
174
  }, z.core.$strip>>;
172
175
  }, z.core.$strip>>;
173
176
  options: z.ZodArray<z.ZodObject<{
@@ -382,6 +385,8 @@ export declare const RuleSampleSchema: z.ZodObject<{
382
385
  wastePercent: z.ZodNumber;
383
386
  }, z.core.$strip>>;
384
387
  surfaceArea: z.ZodOptional<z.ZodNumber>;
388
+ puttyRequired: z.ZodOptional<z.ZodBoolean>;
389
+ puttyArea: z.ZodOptional<z.ZodNumber>;
385
390
  object: z.ZodObject<{
386
391
  scope: z.ZodEnum<{
387
392
  FULL: "FULL";
@@ -399,6 +404,7 @@ export declare const RuleSampleSchema: z.ZodObject<{
399
404
  bathroomsCount: z.ZodInt;
400
405
  roomsFloorArea: z.ZodNumber;
401
406
  roomsWallArea: z.ZodNumber;
407
+ roomsPuttyWallArea: z.ZodOptional<z.ZodNumber>;
402
408
  roomsPerimeter: z.ZodNumber;
403
409
  wetRoomsCount: z.ZodInt;
404
410
  }, z.core.$strip>;
@@ -482,11 +488,12 @@ export declare const PricingCategorySchema: z.ZodObject<{
482
488
  id: z.ZodString;
483
489
  wastePercent: z.ZodNumber;
484
490
  surface: z.ZodEnum<{
491
+ NONE: "NONE";
485
492
  FLOOR: "FLOOR";
486
493
  WALLS: "WALLS";
487
494
  CEILING: "CEILING";
488
- NONE: "NONE";
489
495
  }>;
496
+ requiresPutty: z.ZodDefault<z.ZodBoolean>;
490
497
  }, z.core.$strip>;
491
498
  export type PricingCategory = z.infer<typeof PricingCategorySchema>;
492
499
  export declare const PricingMaterialTypeSchema: z.ZodObject<{
@@ -521,6 +528,10 @@ export declare const PricingPackageItemSchema: z.ZodObject<{
521
528
  }>>;
522
529
  }, z.core.$strip>;
523
530
  export type PricingPackageItem = z.infer<typeof PricingPackageItemSchema>;
531
+ export declare function findOptionPriceSourceViolation(priceCents: number | null, priceCoefficientKeys: readonly string[]): {
532
+ path: 'priceCents' | 'priceCoefficientKeys';
533
+ message: string;
534
+ } | null;
524
535
  export declare const PricingOptionSchema: z.ZodObject<{
525
536
  id: z.ZodString;
526
537
  code: z.ZodNullable<z.ZodEnum<{
@@ -530,7 +541,7 @@ export declare const PricingOptionSchema: z.ZodObject<{
530
541
  ENGINEERING: "ENGINEERING";
531
542
  ADDITIONAL: "ADDITIONAL";
532
543
  }>;
533
- priceCents: z.ZodInt;
544
+ priceCents: z.ZodNullable<z.ZodInt>;
534
545
  unit: z.ZodEnum<{
535
546
  PIECE: "PIECE";
536
547
  ROOM_SQM: "ROOM_SQM";
@@ -611,11 +622,12 @@ export declare const PricingDataSchema: z.ZodObject<{
611
622
  id: z.ZodString;
612
623
  wastePercent: z.ZodNumber;
613
624
  surface: z.ZodEnum<{
625
+ NONE: "NONE";
614
626
  FLOOR: "FLOOR";
615
627
  WALLS: "WALLS";
616
628
  CEILING: "CEILING";
617
- NONE: "NONE";
618
629
  }>;
630
+ requiresPutty: z.ZodDefault<z.ZodBoolean>;
619
631
  }, z.core.$strip>>;
620
632
  materialTypes: z.ZodArray<z.ZodObject<{
621
633
  id: z.ZodString;
@@ -655,7 +667,7 @@ export declare const PricingDataSchema: z.ZodObject<{
655
667
  ENGINEERING: "ENGINEERING";
656
668
  ADDITIONAL: "ADDITIONAL";
657
669
  }>;
658
- priceCents: z.ZodInt;
670
+ priceCents: z.ZodNullable<z.ZodInt>;
659
671
  unit: z.ZodEnum<{
660
672
  PIECE: "PIECE";
661
673
  ROOM_SQM: "ROOM_SQM";
@@ -812,6 +824,8 @@ export interface FormulaVariable {
812
824
  export declare const FORMULA_SCOPE_VARIABLES: Record<FormulaScope, readonly FormulaVariable[]>;
813
825
  export declare const FORMULA_QUANTITY_VARIABLE: FormulaVariable;
814
826
  export declare const FORMULA_SURFACE_AREA_VARIABLE: FormulaVariable;
827
+ export declare const FORMULA_PUTTY_AREA_VARIABLE: FormulaVariable;
828
+ export declare const FORMULA_PUTTY_REQUIRED_VARIABLE: FormulaVariable;
815
829
  export declare const FORMULA_VARIABLES: Record<RuleLevel, readonly FormulaVariable[]>;
816
830
  export interface ValidateFormulaInput {
817
831
  readonly level: RuleLevel;
@@ -843,11 +857,12 @@ export interface ObjectVariables {
843
857
  readonly bathroomsCount: number;
844
858
  readonly roomsFloorArea: number;
845
859
  readonly roomsWallArea: number;
860
+ readonly roomsPuttyWallArea: number;
846
861
  readonly roomsPerimeter: number;
847
862
  readonly wetRoomsCount: number;
848
863
  }
849
864
  export declare function totalAreaOf(scope: Scope, totalAreaSqm: number | null, roomsFloorArea: number): number;
850
- export declare function objectVariables(configuration: Configuration): ObjectVariables;
865
+ export declare function objectVariables(configuration: Configuration, categories: readonly PricingCategory[]): ObjectVariables;
851
866
  //#endregion
852
867
  //#region src/variables/product.d.ts
853
868
  export interface ProductVariables {
@@ -858,6 +873,9 @@ export interface ProductVariables {
858
873
  export declare function productVariablesOf(product: Pick<PricingProduct, 'priceCents' | 'unit'>, wastePercent: number): ProductVariables;
859
874
  export declare function productVariables(product: PricingProduct, category: PricingCategory): ProductVariables;
860
875
  //#endregion
876
+ //#region src/variables/putty.d.ts
877
+ export declare function puttyRequiredOn(room: RoomConfiguration, surface: PricingCategory['surface'], categories: readonly PricingCategory[]): boolean;
878
+ //#endregion
861
879
  //#region src/variables/room.d.ts
862
880
  export interface RoomVariables {
863
881
  readonly length: number;
@@ -869,6 +887,22 @@ export interface RoomVariables {
869
887
  readonly heatedFloor: boolean;
870
888
  readonly roomType: RoomTypeCode;
871
889
  }
890
+ export type MeasuredSurface = 'FLOOR' | 'WALLS' | 'CEILING';
891
+ export declare function surfaceAreaOf(variables: Pick<RoomVariables, 'floorArea' | 'wallArea'>, surface: MeasuredSurface): number;
872
892
  export type RoomDimensions = Pick<RoomConfiguration, 'roomTypeCode' | 'lengthM' | 'widthM' | 'heightM' | 'heatedFloor'>;
873
893
  export declare function roomVariables(room: RoomDimensions): RoomVariables;
894
+ //#endregion
895
+ //#region src/variables/share.d.ts
896
+ export type ShareSurface = 'WALLS' | 'FLOOR';
897
+ export interface SurfaceShares {
898
+ readonly selections: readonly RoomSelection[];
899
+ readonly shared: boolean;
900
+ readonly complete: boolean;
901
+ readonly totalPercent: number;
902
+ }
903
+ export declare function isShareSurface(surface: PricingCategory['surface']): surface is ShareSurface;
904
+ export declare function surfaceShares(room: RoomConfiguration, surface: ShareSurface, categories: readonly PricingCategory[]): SurfaceShares;
905
+ export declare function areaShareOf(selection: RoomSelection): number;
906
+ export declare function areaShareError(room: RoomConfiguration, selection: RoomSelection, categories: readonly PricingCategory[]): EstimateMessage | null;
907
+ export declare function puttyShareOn(room: RoomConfiguration, surface: MeasuredSurface, categories: readonly PricingCategory[]): number;
874
908
  //#endregion
package/dist/index.js CHANGED
@@ -27,6 +27,9 @@ function toCents(dollars) {
27
27
  }
28
28
  //#endregion
29
29
  //#region src/variables/room.ts
30
+ function surfaceAreaOf(variables, surface) {
31
+ return surface === "WALLS" ? variables.wallArea : variables.floorArea;
32
+ }
30
33
  function roomVariables(room) {
31
34
  const perimeter = 2 * (room.lengthM + room.widthM);
32
35
  return {
@@ -41,6 +44,63 @@ function roomVariables(room) {
41
44
  };
42
45
  }
43
46
  //#endregion
47
+ //#region src/variables/putty.ts
48
+ function puttyRequiredOn(room, surface, categories) {
49
+ const selected = categories.filter((category) => category.surface === surface && room.selections.some((selection) => selection.categoryId === category.id));
50
+ return selected.length === 0 || selected.some((category) => category.requiresPutty);
51
+ }
52
+ //#endregion
53
+ //#region src/variables/share.ts
54
+ function isShareSurface(surface) {
55
+ return surface === "WALLS" || surface === "FLOOR";
56
+ }
57
+ function surfaceShares(room, surface, categories) {
58
+ const selections = room.selections.filter((selection) => selection.productId !== null && categories.some((category) => category.id === selection.categoryId && category.surface === surface));
59
+ return {
60
+ selections,
61
+ shared: selections.some((selection) => selection.areaShare !== void 0),
62
+ complete: selections.every((selection) => selection.areaShare !== void 0),
63
+ totalPercent: selections.reduce((sum, selection) => sum + (selection.areaShare ?? 0), 0)
64
+ };
65
+ }
66
+ function areaShareOf(selection) {
67
+ return selection.areaShare === void 0 ? 1 : selection.areaShare / 100;
68
+ }
69
+ function areaShareError(room, selection, categories) {
70
+ const category = categories.find((item) => item.id === selection.categoryId);
71
+ if (category === void 0) return null;
72
+ if (!isShareSurface(category.surface)) return selection.areaShare === void 0 ? null : {
73
+ code: "AREA_SHARE_NOT_ALLOWED",
74
+ params: {
75
+ categoryId: category.id,
76
+ surface: category.surface
77
+ }
78
+ };
79
+ const shares = surfaceShares(room, category.surface, categories);
80
+ const params = {
81
+ roomClientId: room.clientId,
82
+ surface: category.surface
83
+ };
84
+ if (shares.shared && !shares.complete) return {
85
+ code: "AREA_SHARE_MISSING",
86
+ params
87
+ };
88
+ if (shares.totalPercent > 100) return {
89
+ code: "AREA_SHARE_EXCEEDS_SURFACE",
90
+ params: {
91
+ ...params,
92
+ percent: shares.totalPercent
93
+ }
94
+ };
95
+ return null;
96
+ }
97
+ function puttyShareOn(room, surface, categories) {
98
+ const shares = surface === "WALLS" ? surfaceShares(room, surface, categories) : null;
99
+ if (shares === null || !shares.shared) return puttyRequiredOn(room, surface, categories) ? 1 : 0;
100
+ const needing = shares.selections.filter((selection) => categories.find((category) => category.id === selection.categoryId)?.requiresPutty);
101
+ return Math.min(1, needing.reduce((sum, selection) => sum + areaShareOf(selection), 0));
102
+ }
103
+ //#endregion
44
104
  //#region src/variables/object.ts
45
105
  const COUNTED_AS_ROOMS = [
46
106
  "LIVING_ROOM",
@@ -55,7 +115,7 @@ const WET_ROOMS = [
55
115
  function totalAreaOf(scope, totalAreaSqm, roomsFloorArea) {
56
116
  return scope === "PARTIAL" || totalAreaSqm === null ? roomsFloorArea : totalAreaSqm;
57
117
  }
58
- function objectVariables(configuration) {
118
+ function objectVariables(configuration, categories) {
59
119
  const rooms = configuration.rooms.map(roomVariables);
60
120
  const sum = (pick) => rooms.reduce((total, room) => total + pick(room), 0);
61
121
  const roomsFloorArea = sum((room) => room.floorArea);
@@ -69,6 +129,7 @@ function objectVariables(configuration) {
69
129
  bathroomsCount: configuration.rooms.filter((room) => room.roomTypeCode === "BATHROOM").length,
70
130
  roomsFloorArea,
71
131
  roomsWallArea: sum((room) => room.wallArea),
132
+ roomsPuttyWallArea: configuration.rooms.reduce((total, room) => total + roomVariables(room).wallArea * puttyShareOn(room, "WALLS", categories), 0),
72
133
  roomsPerimeter: sum((room) => room.perimeter),
73
134
  wetRoomsCount: rooms.filter((room) => WET_ROOMS.includes(room.roomType)).length
74
135
  };
@@ -701,23 +762,25 @@ function applySurfacePrep(data, room, lines, objectScope) {
701
762
  ...roomVariables(room),
702
763
  ...objectScope
703
764
  };
704
- const areas = {
705
- FLOOR: variables.floorArea,
706
- CEILING: variables.floorArea,
707
- WALLS: variables.wallArea
708
- };
709
765
  const surfaces = /* @__PURE__ */ new Map();
710
766
  lines.forEach((line, index) => {
711
- const surface = data.categories.find((category) => category.id === line.categoryId)?.surface;
712
- if (line.status === "OK" && surface !== void 0 && surface !== "NONE") surfaces.set(surface, [...surfaces.get(surface) ?? [], index]);
767
+ const category = data.categories.find((item) => item.id === line.categoryId);
768
+ if (line.status === "OK" && category !== void 0 && category.surface !== "NONE") {
769
+ const surface = category.surface;
770
+ surfaces.set(surface, [...surfaces.get(surface) ?? [], index]);
771
+ }
713
772
  });
714
773
  for (const [surface, indexes] of surfaces) {
715
774
  const rule = surfaceRuleOf(data, surface);
716
775
  if (rule === void 0) continue;
776
+ const surfaceArea = surfaceAreaOf(variables, surface);
777
+ const puttyShare = puttyShareOn(room, surface, data.categories);
717
778
  const outcome = runNumericFormula(rule.costFormula, {
718
779
  variables: {
719
780
  ...variables,
720
- surfaceArea: areas[surface]
781
+ surfaceArea,
782
+ puttyRequired: puttyShare > 0,
783
+ puttyArea: surfaceArea * puttyShare
721
784
  },
722
785
  coefficients: data.coefficients
723
786
  });
@@ -750,7 +813,9 @@ function selectRule(data, categoryId, materialTypeId) {
750
813
  const rules = data.rules.filter(isMaterialRule);
751
814
  return rules.find((rule) => rule.level === "MATERIAL_TYPE" && rule.categoryId === categoryId && rule.materialTypeId === materialTypeId) ?? rules.find((rule) => rule.level === "CATEGORY" && rule.categoryId === categoryId);
752
815
  }
753
- function materialLine(configuration, data, room, categoryId, productId, objectScope) {
816
+ function materialLine(configuration, data, room, selection, objectScope) {
817
+ const { categoryId } = selection;
818
+ const productId = selection.productId;
754
819
  const init = {
755
820
  id: lineId("MATERIALS", room.clientId, categoryId),
756
821
  section: "MATERIALS",
@@ -758,6 +823,8 @@ function materialLine(configuration, data, room, categoryId, productId, objectSc
758
823
  categoryId,
759
824
  productId
760
825
  };
826
+ const shareError = areaShareError(room, selection, data.categories);
827
+ if (shareError !== null) return failedLine(init, "ERROR", shareError);
761
828
  const category = data.categories.find((item) => item.id === categoryId);
762
829
  const product = data.products.find((item) => item.id === productId && item.categoryId === categoryId);
763
830
  if (product === void 0 || category === void 0 || !product.available) return failedLine(init, "UNAVAILABLE", {
@@ -779,8 +846,12 @@ function materialLine(configuration, data, room, categoryId, productId, objectSc
779
846
  materialTypeId: product.materialTypeId
780
847
  }
781
848
  });
849
+ const base = roomVariables(room);
850
+ const share = areaShareOf(selection);
782
851
  const variables = {
783
- ...roomVariables(room),
852
+ ...base,
853
+ wallArea: category.surface === "WALLS" ? base.wallArea * share : base.wallArea,
854
+ floorArea: category.surface === "FLOOR" ? base.floorArea * share : base.floorArea,
784
855
  ...productVariables(product, category),
785
856
  ...objectScope
786
857
  };
@@ -850,7 +921,7 @@ function materialLine(configuration, data, room, categoryId, productId, objectSc
850
921
  }
851
922
  function materialLines(configuration, data, objectScope) {
852
923
  return configuration.rooms.flatMap((room) => {
853
- const lines = room.selections.flatMap(({ categoryId, productId }) => productId === null ? [] : [materialLine(configuration, data, room, categoryId, productId, objectScope)]);
924
+ const lines = room.selections.flatMap((selection) => selection.productId === null ? [] : [materialLine(configuration, data, room, selection, objectScope)]);
854
925
  return configuration.scope === "PARTIAL" ? applySurfacePrep(data, room, lines, objectScope) : lines;
855
926
  });
856
927
  }
@@ -948,7 +1019,8 @@ const RangeSchema = z.object({
948
1019
  //#region src/schemas/configuration.ts
949
1020
  const RoomSelectionSchema = z.object({
950
1021
  categoryId: z.string(),
951
- productId: z.string().nullable()
1022
+ productId: z.string().nullable(),
1023
+ areaShare: z.int().min(1).max(100).optional()
952
1024
  });
953
1025
  const RoomConfigurationSchema = z.object({
954
1026
  clientId: z.string(),
@@ -1052,7 +1124,8 @@ const PricingRoomTypeSchema = z.object({
1052
1124
  const PricingCategorySchema = z.object({
1053
1125
  id: z.string(),
1054
1126
  wastePercent: z.number().min(0),
1055
- surface: SurfaceKindSchema
1127
+ surface: SurfaceKindSchema,
1128
+ requiresPutty: z.boolean().default(true)
1056
1129
  });
1057
1130
  const PricingMaterialTypeSchema = z.object({
1058
1131
  id: z.string(),
@@ -1074,11 +1147,11 @@ const PricingPackageItemSchema = z.object({
1074
1147
  priceCents: CentsSchema.nullable(),
1075
1148
  unit: OptionUnitSchema.nullable()
1076
1149
  });
1077
- const PricingOptionSchema = z.object({
1150
+ const PricingOptionObjectSchema = z.object({
1078
1151
  id: z.string(),
1079
1152
  code: OptionCodeSchema.nullable(),
1080
1153
  kind: OptionKindSchema,
1081
- priceCents: CentsSchema,
1154
+ priceCents: CentsSchema.nullable(),
1082
1155
  unit: OptionUnitSchema,
1083
1156
  roomTypeCodes: z.array(z.string()),
1084
1157
  priceCoefficientKeys: z.array(z.string()),
@@ -1087,6 +1160,26 @@ const PricingOptionSchema = z.object({
1087
1160
  maxQuantity: z.int().nullable(),
1088
1161
  available: z.boolean()
1089
1162
  });
1163
+ function findOptionPriceSourceViolation(priceCents, priceCoefficientKeys) {
1164
+ const hasKeys = priceCoefficientKeys.length > 0;
1165
+ if (priceCents !== null && hasKeys) return {
1166
+ path: "priceCents",
1167
+ message: "priceCents must be null when priceCoefficientKeys is set"
1168
+ };
1169
+ if (priceCents === null && !hasKeys) return {
1170
+ path: "priceCoefficientKeys",
1171
+ message: "priceCoefficientKeys is required when priceCents is null"
1172
+ };
1173
+ return null;
1174
+ }
1175
+ const PricingOptionSchema = PricingOptionObjectSchema.superRefine((option, context) => {
1176
+ const violation = findOptionPriceSourceViolation(option.priceCents, option.priceCoefficientKeys);
1177
+ if (violation !== null) context.addIssue({
1178
+ code: "custom",
1179
+ path: [violation.path],
1180
+ message: violation.message
1181
+ });
1182
+ });
1090
1183
  const PricingDataSchema = z.object({
1091
1184
  version: z.string().regex(/^[0-9a-f]{16}$/),
1092
1185
  exchangeRate: ExchangeRateSchema.nullable(),
@@ -1166,6 +1259,8 @@ const RuleSampleSchema = z.object({
1166
1259
  wastePercent: z.number().min(0)
1167
1260
  }).optional(),
1168
1261
  surfaceArea: z.number().min(0).optional(),
1262
+ puttyRequired: z.boolean().optional(),
1263
+ puttyArea: z.number().min(0).optional(),
1169
1264
  object: z.object({
1170
1265
  scope: ScopeSchema,
1171
1266
  totalAreaSqm: z.number().positive().nullable(),
@@ -1176,6 +1271,7 @@ const RuleSampleSchema = z.object({
1176
1271
  bathroomsCount: z.int().min(0),
1177
1272
  roomsFloorArea: z.number().min(0),
1178
1273
  roomsWallArea: z.number().min(0),
1274
+ roomsPuttyWallArea: z.number().min(0).optional(),
1179
1275
  roomsPerimeter: z.number().min(0),
1180
1276
  wetRoomsCount: z.int().min(0)
1181
1277
  })
@@ -1243,10 +1339,32 @@ function roomsAreaWarnings(configuration, limits) {
1243
1339
  params: { missingSqm }
1244
1340
  }] : [];
1245
1341
  }
1246
- function estimateWarnings(lines, configuration, rate, limits) {
1342
+ const SHARE_SURFACES = ["WALLS", "FLOOR"];
1343
+ function areaShareWarnings(configuration, categories) {
1344
+ return configuration.rooms.flatMap((room) => {
1345
+ const variables = roomVariables(room);
1346
+ return SHARE_SURFACES.flatMap((surface) => {
1347
+ const shares = surfaceShares(room, surface, categories);
1348
+ if (!shares.shared || !shares.complete || shares.totalPercent >= 100) return [];
1349
+ const percent = 100 - shares.totalPercent;
1350
+ const area = surfaceAreaOf(variables, surface);
1351
+ return [{
1352
+ code: "AREA_SHARE_UNALLOCATED",
1353
+ params: {
1354
+ roomClientId: room.clientId,
1355
+ surface,
1356
+ percent,
1357
+ sqm: roundArea(area * percent / 100)
1358
+ }
1359
+ }];
1360
+ });
1361
+ });
1362
+ }
1363
+ function estimateWarnings(lines, configuration, rate, data) {
1247
1364
  return [
1248
1365
  ...collectWarnings(lines),
1249
- ...roomsAreaWarnings(configuration, limits),
1366
+ ...roomsAreaWarnings(configuration, data.limits),
1367
+ ...areaShareWarnings(configuration, data.categories),
1250
1368
  ...configuration.currency === "UAH" && rate === null ? [{ code: "EXCHANGE_RATE_UNAVAILABLE" }] : []
1251
1369
  ];
1252
1370
  }
@@ -1260,7 +1378,7 @@ function withKopecks(line, rate) {
1260
1378
  }
1261
1379
  function estimate(configuration, data) {
1262
1380
  const rate = data.exchangeRate?.rate ?? null;
1263
- const variables = objectVariables(configuration);
1381
+ const variables = objectVariables(configuration, data.categories);
1264
1382
  const lines = [
1265
1383
  ...objectRuleLines(configuration, data, "BASE", variables),
1266
1384
  ...objectRuleLines(configuration, data, "OBJECT", variables),
@@ -1274,7 +1392,7 @@ function estimate(configuration, data) {
1274
1392
  exchangeRate: data.exchangeRate,
1275
1393
  lines,
1276
1394
  totals: computeTotals(lines, rate !== null),
1277
- warnings: estimateWarnings(lines, configuration, rate, data.limits),
1395
+ warnings: estimateWarnings(lines, configuration, rate, data),
1278
1396
  incomplete: isIncomplete(lines)
1279
1397
  };
1280
1398
  }
@@ -1356,6 +1474,7 @@ function objectScope({ object }) {
1356
1474
  bathroomsCount: object.bathroomsCount,
1357
1475
  roomsFloorArea: object.roomsFloorArea,
1358
1476
  roomsWallArea: object.roomsWallArea,
1477
+ roomsPuttyWallArea: object.roomsPuttyWallArea ?? object.roomsWallArea,
1359
1478
  roomsPerimeter: object.roomsPerimeter,
1360
1479
  wetRoomsCount: object.wetRoomsCount
1361
1480
  };
@@ -1368,10 +1487,13 @@ function scopeOf(level, sample) {
1368
1487
  code: "SAMPLE_INCOMPLETE",
1369
1488
  params: { missing: room === void 0 ? "room" : "surfaceArea" }
1370
1489
  } };
1490
+ const puttyRequired = sample.puttyRequired ?? true;
1371
1491
  return { scope: {
1372
1492
  ...roomVariables(room),
1373
1493
  ...objectScope(sample),
1374
- surfaceArea
1494
+ surfaceArea,
1495
+ puttyRequired,
1496
+ puttyArea: sample.puttyArea ?? (puttyRequired ? surfaceArea : 0)
1375
1497
  } };
1376
1498
  }
1377
1499
  if (room === void 0 || product === void 0) return { error: {
@@ -1513,12 +1635,15 @@ const FORMULA_SCOPE_VARIABLES = {
1513
1635
  enumeration("scope", ScopeSchema.options),
1514
1636
  number("roomsFloorArea"),
1515
1637
  number("roomsWallArea"),
1638
+ number("roomsPuttyWallArea"),
1516
1639
  number("roomsPerimeter"),
1517
1640
  number("wetRoomsCount")
1518
1641
  ]
1519
1642
  };
1520
1643
  const FORMULA_QUANTITY_VARIABLE = number("quantity");
1521
1644
  const FORMULA_SURFACE_AREA_VARIABLE = number("surfaceArea");
1645
+ const FORMULA_PUTTY_AREA_VARIABLE = number("puttyArea");
1646
+ const FORMULA_PUTTY_REQUIRED_VARIABLE = boolean("puttyRequired");
1522
1647
  const FORMULA_VARIABLES = {
1523
1648
  OBJECT: FORMULA_SCOPE_VARIABLES.object,
1524
1649
  CATEGORY: [
@@ -1534,7 +1659,9 @@ const FORMULA_VARIABLES = {
1534
1659
  SURFACE: [
1535
1660
  ...FORMULA_SCOPE_VARIABLES.room,
1536
1661
  ...FORMULA_SCOPE_VARIABLES.object,
1537
- FORMULA_SURFACE_AREA_VARIABLE
1662
+ FORMULA_SURFACE_AREA_VARIABLE,
1663
+ FORMULA_PUTTY_REQUIRED_VARIABLE,
1664
+ FORMULA_PUTTY_AREA_VARIABLE
1538
1665
  ]
1539
1666
  };
1540
1667
  const ALL_VARIABLE_NAMES = new Set([...FORMULA_VARIABLES.CATEGORY, ...FORMULA_VARIABLES.SURFACE].map((variable) => variable.name));
@@ -1749,4 +1876,4 @@ function validateFormula(input) {
1749
1876
  };
1750
1877
  }
1751
1878
  //#endregion
1752
- export { COEFFICIENT_NAMESPACE, CentsSchema, ConfigurationSchema, CurrencySchema, EstimateLineSchema, EstimateMessageSchema, EstimateSchema, EstimateTotalsSchema, EstimateWarningSchema, ExchangeRateSchema, FORMULA_FUNCTIONS, FORMULA_FUNCTION_TABLE, FORMULA_LIMITS, FORMULA_QUANTITY_VARIABLE, FORMULA_SCOPE_VARIABLES, FORMULA_SURFACE_AREA_VARIABLE, FORMULA_VARIABLES, FinishLevelSchema, LineComponentKindSchema, LineComponentSchema, LineStatusSchema, LocalizedTextSchema, OptionCodeSchema, OptionKindSchema, OptionSelectionSchema, OptionUnitSchema, PricingCategorySchema, PricingDataSchema, PricingLimitsSchema, PricingMaterialTypeSchema, PricingOptionSchema, PricingPackageItemSchema, PricingProductSchema, PricingRoomTypeSchema, PricingRuleSchema, ProductUnitSchema, RangeSchema, RateSourceSchema, RoomConfigurationSchema, RoomSelectionSchema, RoomTypeCodeSchema, RuleLevelSchema, RulePreviewSchema, RuleSampleSchema, ScopeSchema, SectionSchema, SectionTotalSchema, SurfaceKindSchema, checkLimits, convertCentsToKopecks, estimate, evaluate, levenshtein, objectVariables, parse, previewRule, productVariables, productVariablesOf, roomVariables, suggestNearest, toCents, totalAreaOf, validateFormula };
1879
+ export { COEFFICIENT_NAMESPACE, CentsSchema, ConfigurationSchema, CurrencySchema, EstimateLineSchema, EstimateMessageSchema, EstimateSchema, EstimateTotalsSchema, EstimateWarningSchema, ExchangeRateSchema, FORMULA_FUNCTIONS, FORMULA_FUNCTION_TABLE, FORMULA_LIMITS, FORMULA_PUTTY_AREA_VARIABLE, FORMULA_PUTTY_REQUIRED_VARIABLE, FORMULA_QUANTITY_VARIABLE, FORMULA_SCOPE_VARIABLES, FORMULA_SURFACE_AREA_VARIABLE, FORMULA_VARIABLES, FinishLevelSchema, LineComponentKindSchema, LineComponentSchema, LineStatusSchema, LocalizedTextSchema, OptionCodeSchema, OptionKindSchema, OptionSelectionSchema, OptionUnitSchema, PricingCategorySchema, PricingDataSchema, PricingLimitsSchema, PricingMaterialTypeSchema, PricingOptionSchema, PricingPackageItemSchema, PricingProductSchema, PricingRoomTypeSchema, PricingRuleSchema, ProductUnitSchema, RangeSchema, RateSourceSchema, RoomConfigurationSchema, RoomSelectionSchema, RoomTypeCodeSchema, RuleLevelSchema, RulePreviewSchema, RuleSampleSchema, ScopeSchema, SectionSchema, SectionTotalSchema, SurfaceKindSchema, areaShareError, areaShareOf, checkLimits, convertCentsToKopecks, estimate, evaluate, findOptionPriceSourceViolation, isShareSurface, levenshtein, objectVariables, parse, previewRule, productVariables, productVariablesOf, puttyRequiredOn, puttyShareOn, roomVariables, suggestNearest, surfaceAreaOf, surfaceShares, toCents, totalAreaOf, validateFormula };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roomwise/calc-engine",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",