@roomwise/calc-engine 0.5.0 → 0.6.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
@@ -51,6 +51,12 @@ function toCents(dollars) {
51
51
  return cents;
52
52
  }
53
53
  //#endregion
54
+ //#region src/variables/putty.ts
55
+ function puttyRequiredOn(room, surface, categories) {
56
+ const selected = categories.filter((category) => category.surface === surface && room.selections.some((selection) => selection.categoryId === category.id));
57
+ return selected.length === 0 || selected.some((category) => category.requiresPutty);
58
+ }
59
+ //#endregion
54
60
  //#region src/variables/room.ts
55
61
  function roomVariables(room) {
56
62
  const perimeter = 2 * (room.lengthM + room.widthM);
@@ -80,7 +86,7 @@ const WET_ROOMS = [
80
86
  function totalAreaOf(scope, totalAreaSqm, roomsFloorArea) {
81
87
  return scope === "PARTIAL" || totalAreaSqm === null ? roomsFloorArea : totalAreaSqm;
82
88
  }
83
- function objectVariables(configuration) {
89
+ function objectVariables(configuration, categories) {
84
90
  const rooms = configuration.rooms.map(roomVariables);
85
91
  const sum = (pick) => rooms.reduce((total, room) => total + pick(room), 0);
86
92
  const roomsFloorArea = sum((room) => room.floorArea);
@@ -94,6 +100,7 @@ function objectVariables(configuration) {
94
100
  bathroomsCount: configuration.rooms.filter((room) => room.roomTypeCode === "BATHROOM").length,
95
101
  roomsFloorArea,
96
102
  roomsWallArea: sum((room) => room.wallArea),
103
+ roomsPuttyWallArea: configuration.rooms.reduce((total, room) => puttyRequiredOn(room, "WALLS", categories) ? total + roomVariables(room).wallArea : total, 0),
97
104
  roomsPerimeter: sum((room) => room.perimeter),
98
105
  wetRoomsCount: rooms.filter((room) => WET_ROOMS.includes(room.roomType)).length
99
106
  };
@@ -733,8 +740,11 @@ function applySurfacePrep(data, room, lines, objectScope) {
733
740
  };
734
741
  const surfaces = /* @__PURE__ */ new Map();
735
742
  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]);
743
+ const category = data.categories.find((item) => item.id === line.categoryId);
744
+ if (line.status === "OK" && category !== void 0 && category.surface !== "NONE") {
745
+ const surface = category.surface;
746
+ surfaces.set(surface, [...surfaces.get(surface) ?? [], index]);
747
+ }
738
748
  });
739
749
  for (const [surface, indexes] of surfaces) {
740
750
  const rule = surfaceRuleOf(data, surface);
@@ -742,7 +752,8 @@ function applySurfacePrep(data, room, lines, objectScope) {
742
752
  const outcome = runNumericFormula(rule.costFormula, {
743
753
  variables: {
744
754
  ...variables,
745
- surfaceArea: areas[surface]
755
+ surfaceArea: areas[surface],
756
+ puttyRequired: puttyRequiredOn(room, surface, data.categories)
746
757
  },
747
758
  coefficients: data.coefficients
748
759
  });
@@ -1077,7 +1088,8 @@ const PricingRoomTypeSchema = zod.z.object({
1077
1088
  const PricingCategorySchema = zod.z.object({
1078
1089
  id: zod.z.string(),
1079
1090
  wastePercent: zod.z.number().min(0),
1080
- surface: SurfaceKindSchema
1091
+ surface: SurfaceKindSchema,
1092
+ requiresPutty: zod.z.boolean().default(true)
1081
1093
  });
1082
1094
  const PricingMaterialTypeSchema = zod.z.object({
1083
1095
  id: zod.z.string(),
@@ -1099,11 +1111,11 @@ const PricingPackageItemSchema = zod.z.object({
1099
1111
  priceCents: CentsSchema.nullable(),
1100
1112
  unit: OptionUnitSchema.nullable()
1101
1113
  });
1102
- const PricingOptionSchema = zod.z.object({
1114
+ const PricingOptionObjectSchema = zod.z.object({
1103
1115
  id: zod.z.string(),
1104
1116
  code: OptionCodeSchema.nullable(),
1105
1117
  kind: OptionKindSchema,
1106
- priceCents: CentsSchema,
1118
+ priceCents: CentsSchema.nullable(),
1107
1119
  unit: OptionUnitSchema,
1108
1120
  roomTypeCodes: zod.z.array(zod.z.string()),
1109
1121
  priceCoefficientKeys: zod.z.array(zod.z.string()),
@@ -1112,6 +1124,26 @@ const PricingOptionSchema = zod.z.object({
1112
1124
  maxQuantity: zod.z.int().nullable(),
1113
1125
  available: zod.z.boolean()
1114
1126
  });
1127
+ function findOptionPriceSourceViolation(priceCents, priceCoefficientKeys) {
1128
+ const hasKeys = priceCoefficientKeys.length > 0;
1129
+ if (priceCents !== null && hasKeys) return {
1130
+ path: "priceCents",
1131
+ message: "priceCents must be null when priceCoefficientKeys is set"
1132
+ };
1133
+ if (priceCents === null && !hasKeys) return {
1134
+ path: "priceCoefficientKeys",
1135
+ message: "priceCoefficientKeys is required when priceCents is null"
1136
+ };
1137
+ return null;
1138
+ }
1139
+ const PricingOptionSchema = PricingOptionObjectSchema.superRefine((option, context) => {
1140
+ const violation = findOptionPriceSourceViolation(option.priceCents, option.priceCoefficientKeys);
1141
+ if (violation !== null) context.addIssue({
1142
+ code: "custom",
1143
+ path: [violation.path],
1144
+ message: violation.message
1145
+ });
1146
+ });
1115
1147
  const PricingDataSchema = zod.z.object({
1116
1148
  version: zod.z.string().regex(/^[0-9a-f]{16}$/),
1117
1149
  exchangeRate: ExchangeRateSchema.nullable(),
@@ -1191,6 +1223,7 @@ const RuleSampleSchema = zod.z.object({
1191
1223
  wastePercent: zod.z.number().min(0)
1192
1224
  }).optional(),
1193
1225
  surfaceArea: zod.z.number().min(0).optional(),
1226
+ puttyRequired: zod.z.boolean().optional(),
1194
1227
  object: zod.z.object({
1195
1228
  scope: ScopeSchema,
1196
1229
  totalAreaSqm: zod.z.number().positive().nullable(),
@@ -1201,6 +1234,7 @@ const RuleSampleSchema = zod.z.object({
1201
1234
  bathroomsCount: zod.z.int().min(0),
1202
1235
  roomsFloorArea: zod.z.number().min(0),
1203
1236
  roomsWallArea: zod.z.number().min(0),
1237
+ roomsPuttyWallArea: zod.z.number().min(0).optional(),
1204
1238
  roomsPerimeter: zod.z.number().min(0),
1205
1239
  wetRoomsCount: zod.z.int().min(0)
1206
1240
  })
@@ -1285,7 +1319,7 @@ function withKopecks(line, rate) {
1285
1319
  }
1286
1320
  function estimate(configuration, data) {
1287
1321
  const rate = data.exchangeRate?.rate ?? null;
1288
- const variables = objectVariables(configuration);
1322
+ const variables = objectVariables(configuration, data.categories);
1289
1323
  const lines = [
1290
1324
  ...objectRuleLines(configuration, data, "BASE", variables),
1291
1325
  ...objectRuleLines(configuration, data, "OBJECT", variables),
@@ -1381,6 +1415,7 @@ function objectScope({ object }) {
1381
1415
  bathroomsCount: object.bathroomsCount,
1382
1416
  roomsFloorArea: object.roomsFloorArea,
1383
1417
  roomsWallArea: object.roomsWallArea,
1418
+ roomsPuttyWallArea: object.roomsPuttyWallArea ?? object.roomsWallArea,
1384
1419
  roomsPerimeter: object.roomsPerimeter,
1385
1420
  wetRoomsCount: object.wetRoomsCount
1386
1421
  };
@@ -1396,7 +1431,8 @@ function scopeOf(level, sample) {
1396
1431
  return { scope: {
1397
1432
  ...roomVariables(room),
1398
1433
  ...objectScope(sample),
1399
- surfaceArea
1434
+ surfaceArea,
1435
+ puttyRequired: sample.puttyRequired ?? true
1400
1436
  } };
1401
1437
  }
1402
1438
  if (room === void 0 || product === void 0) return { error: {
@@ -1538,12 +1574,14 @@ const FORMULA_SCOPE_VARIABLES = {
1538
1574
  enumeration("scope", ScopeSchema.options),
1539
1575
  number("roomsFloorArea"),
1540
1576
  number("roomsWallArea"),
1577
+ number("roomsPuttyWallArea"),
1541
1578
  number("roomsPerimeter"),
1542
1579
  number("wetRoomsCount")
1543
1580
  ]
1544
1581
  };
1545
1582
  const FORMULA_QUANTITY_VARIABLE = number("quantity");
1546
1583
  const FORMULA_SURFACE_AREA_VARIABLE = number("surfaceArea");
1584
+ const FORMULA_PUTTY_REQUIRED_VARIABLE = boolean("puttyRequired");
1547
1585
  const FORMULA_VARIABLES = {
1548
1586
  OBJECT: FORMULA_SCOPE_VARIABLES.object,
1549
1587
  CATEGORY: [
@@ -1559,7 +1597,8 @@ const FORMULA_VARIABLES = {
1559
1597
  SURFACE: [
1560
1598
  ...FORMULA_SCOPE_VARIABLES.room,
1561
1599
  ...FORMULA_SCOPE_VARIABLES.object,
1562
- FORMULA_SURFACE_AREA_VARIABLE
1600
+ FORMULA_SURFACE_AREA_VARIABLE,
1601
+ FORMULA_PUTTY_REQUIRED_VARIABLE
1563
1602
  ]
1564
1603
  };
1565
1604
  const ALL_VARIABLE_NAMES = new Set([...FORMULA_VARIABLES.CATEGORY, ...FORMULA_VARIABLES.SURFACE].map((variable) => variable.name));
@@ -1787,6 +1826,7 @@ exports.ExchangeRateSchema = ExchangeRateSchema;
1787
1826
  exports.FORMULA_FUNCTIONS = FORMULA_FUNCTIONS;
1788
1827
  exports.FORMULA_FUNCTION_TABLE = FORMULA_FUNCTION_TABLE;
1789
1828
  exports.FORMULA_LIMITS = FORMULA_LIMITS;
1829
+ exports.FORMULA_PUTTY_REQUIRED_VARIABLE = FORMULA_PUTTY_REQUIRED_VARIABLE;
1790
1830
  exports.FORMULA_QUANTITY_VARIABLE = FORMULA_QUANTITY_VARIABLE;
1791
1831
  exports.FORMULA_SCOPE_VARIABLES = FORMULA_SCOPE_VARIABLES;
1792
1832
  exports.FORMULA_SURFACE_AREA_VARIABLE = FORMULA_SURFACE_AREA_VARIABLE;
@@ -1826,12 +1866,14 @@ exports.checkLimits = checkLimits;
1826
1866
  exports.convertCentsToKopecks = convertCentsToKopecks;
1827
1867
  exports.estimate = estimate;
1828
1868
  exports.evaluate = evaluate;
1869
+ exports.findOptionPriceSourceViolation = findOptionPriceSourceViolation;
1829
1870
  exports.levenshtein = levenshtein;
1830
1871
  exports.objectVariables = objectVariables;
1831
1872
  exports.parse = parse;
1832
1873
  exports.previewRule = previewRule;
1833
1874
  exports.productVariables = productVariables;
1834
1875
  exports.productVariablesOf = productVariablesOf;
1876
+ exports.puttyRequiredOn = puttyRequiredOn;
1835
1877
  exports.roomVariables = roomVariables;
1836
1878
  exports.suggestNearest = suggestNearest;
1837
1879
  exports.toCents = toCents;
package/dist/index.d.cts CHANGED
@@ -382,6 +382,7 @@ export declare const RuleSampleSchema: z.ZodObject<{
382
382
  wastePercent: z.ZodNumber;
383
383
  }, z.core.$strip>>;
384
384
  surfaceArea: z.ZodOptional<z.ZodNumber>;
385
+ puttyRequired: z.ZodOptional<z.ZodBoolean>;
385
386
  object: z.ZodObject<{
386
387
  scope: z.ZodEnum<{
387
388
  FULL: "FULL";
@@ -399,6 +400,7 @@ export declare const RuleSampleSchema: z.ZodObject<{
399
400
  bathroomsCount: z.ZodInt;
400
401
  roomsFloorArea: z.ZodNumber;
401
402
  roomsWallArea: z.ZodNumber;
403
+ roomsPuttyWallArea: z.ZodOptional<z.ZodNumber>;
402
404
  roomsPerimeter: z.ZodNumber;
403
405
  wetRoomsCount: z.ZodInt;
404
406
  }, z.core.$strip>;
@@ -487,6 +489,7 @@ export declare const PricingCategorySchema: z.ZodObject<{
487
489
  CEILING: "CEILING";
488
490
  NONE: "NONE";
489
491
  }>;
492
+ requiresPutty: z.ZodDefault<z.ZodBoolean>;
490
493
  }, z.core.$strip>;
491
494
  export type PricingCategory = z.infer<typeof PricingCategorySchema>;
492
495
  export declare const PricingMaterialTypeSchema: z.ZodObject<{
@@ -521,6 +524,10 @@ export declare const PricingPackageItemSchema: z.ZodObject<{
521
524
  }>>;
522
525
  }, z.core.$strip>;
523
526
  export type PricingPackageItem = z.infer<typeof PricingPackageItemSchema>;
527
+ export declare function findOptionPriceSourceViolation(priceCents: number | null, priceCoefficientKeys: readonly string[]): {
528
+ path: 'priceCents' | 'priceCoefficientKeys';
529
+ message: string;
530
+ } | null;
524
531
  export declare const PricingOptionSchema: z.ZodObject<{
525
532
  id: z.ZodString;
526
533
  code: z.ZodNullable<z.ZodEnum<{
@@ -530,7 +537,7 @@ export declare const PricingOptionSchema: z.ZodObject<{
530
537
  ENGINEERING: "ENGINEERING";
531
538
  ADDITIONAL: "ADDITIONAL";
532
539
  }>;
533
- priceCents: z.ZodInt;
540
+ priceCents: z.ZodNullable<z.ZodInt>;
534
541
  unit: z.ZodEnum<{
535
542
  PIECE: "PIECE";
536
543
  ROOM_SQM: "ROOM_SQM";
@@ -616,6 +623,7 @@ export declare const PricingDataSchema: z.ZodObject<{
616
623
  CEILING: "CEILING";
617
624
  NONE: "NONE";
618
625
  }>;
626
+ requiresPutty: z.ZodDefault<z.ZodBoolean>;
619
627
  }, z.core.$strip>>;
620
628
  materialTypes: z.ZodArray<z.ZodObject<{
621
629
  id: z.ZodString;
@@ -655,7 +663,7 @@ export declare const PricingDataSchema: z.ZodObject<{
655
663
  ENGINEERING: "ENGINEERING";
656
664
  ADDITIONAL: "ADDITIONAL";
657
665
  }>;
658
- priceCents: z.ZodInt;
666
+ priceCents: z.ZodNullable<z.ZodInt>;
659
667
  unit: z.ZodEnum<{
660
668
  PIECE: "PIECE";
661
669
  ROOM_SQM: "ROOM_SQM";
@@ -812,6 +820,7 @@ export interface FormulaVariable {
812
820
  export declare const FORMULA_SCOPE_VARIABLES: Record<FormulaScope, readonly FormulaVariable[]>;
813
821
  export declare const FORMULA_QUANTITY_VARIABLE: FormulaVariable;
814
822
  export declare const FORMULA_SURFACE_AREA_VARIABLE: FormulaVariable;
823
+ export declare const FORMULA_PUTTY_REQUIRED_VARIABLE: FormulaVariable;
815
824
  export declare const FORMULA_VARIABLES: Record<RuleLevel, readonly FormulaVariable[]>;
816
825
  export interface ValidateFormulaInput {
817
826
  readonly level: RuleLevel;
@@ -843,11 +852,12 @@ export interface ObjectVariables {
843
852
  readonly bathroomsCount: number;
844
853
  readonly roomsFloorArea: number;
845
854
  readonly roomsWallArea: number;
855
+ readonly roomsPuttyWallArea: number;
846
856
  readonly roomsPerimeter: number;
847
857
  readonly wetRoomsCount: number;
848
858
  }
849
859
  export declare function totalAreaOf(scope: Scope, totalAreaSqm: number | null, roomsFloorArea: number): number;
850
- export declare function objectVariables(configuration: Configuration): ObjectVariables;
860
+ export declare function objectVariables(configuration: Configuration, categories: readonly PricingCategory[]): ObjectVariables;
851
861
  //#endregion
852
862
  //#region src/variables/product.d.ts
853
863
  export interface ProductVariables {
@@ -858,6 +868,9 @@ export interface ProductVariables {
858
868
  export declare function productVariablesOf(product: Pick<PricingProduct, 'priceCents' | 'unit'>, wastePercent: number): ProductVariables;
859
869
  export declare function productVariables(product: PricingProduct, category: PricingCategory): ProductVariables;
860
870
  //#endregion
871
+ //#region src/variables/putty.d.ts
872
+ export declare function puttyRequiredOn(room: RoomConfiguration, surface: PricingCategory['surface'], categories: readonly PricingCategory[]): boolean;
873
+ //#endregion
861
874
  //#region src/variables/room.d.ts
862
875
  export interface RoomVariables {
863
876
  readonly length: number;
package/dist/index.d.ts CHANGED
@@ -382,6 +382,7 @@ export declare const RuleSampleSchema: z.ZodObject<{
382
382
  wastePercent: z.ZodNumber;
383
383
  }, z.core.$strip>>;
384
384
  surfaceArea: z.ZodOptional<z.ZodNumber>;
385
+ puttyRequired: z.ZodOptional<z.ZodBoolean>;
385
386
  object: z.ZodObject<{
386
387
  scope: z.ZodEnum<{
387
388
  FULL: "FULL";
@@ -399,6 +400,7 @@ export declare const RuleSampleSchema: z.ZodObject<{
399
400
  bathroomsCount: z.ZodInt;
400
401
  roomsFloorArea: z.ZodNumber;
401
402
  roomsWallArea: z.ZodNumber;
403
+ roomsPuttyWallArea: z.ZodOptional<z.ZodNumber>;
402
404
  roomsPerimeter: z.ZodNumber;
403
405
  wetRoomsCount: z.ZodInt;
404
406
  }, z.core.$strip>;
@@ -487,6 +489,7 @@ export declare const PricingCategorySchema: z.ZodObject<{
487
489
  CEILING: "CEILING";
488
490
  NONE: "NONE";
489
491
  }>;
492
+ requiresPutty: z.ZodDefault<z.ZodBoolean>;
490
493
  }, z.core.$strip>;
491
494
  export type PricingCategory = z.infer<typeof PricingCategorySchema>;
492
495
  export declare const PricingMaterialTypeSchema: z.ZodObject<{
@@ -521,6 +524,10 @@ export declare const PricingPackageItemSchema: z.ZodObject<{
521
524
  }>>;
522
525
  }, z.core.$strip>;
523
526
  export type PricingPackageItem = z.infer<typeof PricingPackageItemSchema>;
527
+ export declare function findOptionPriceSourceViolation(priceCents: number | null, priceCoefficientKeys: readonly string[]): {
528
+ path: 'priceCents' | 'priceCoefficientKeys';
529
+ message: string;
530
+ } | null;
524
531
  export declare const PricingOptionSchema: z.ZodObject<{
525
532
  id: z.ZodString;
526
533
  code: z.ZodNullable<z.ZodEnum<{
@@ -530,7 +537,7 @@ export declare const PricingOptionSchema: z.ZodObject<{
530
537
  ENGINEERING: "ENGINEERING";
531
538
  ADDITIONAL: "ADDITIONAL";
532
539
  }>;
533
- priceCents: z.ZodInt;
540
+ priceCents: z.ZodNullable<z.ZodInt>;
534
541
  unit: z.ZodEnum<{
535
542
  PIECE: "PIECE";
536
543
  ROOM_SQM: "ROOM_SQM";
@@ -616,6 +623,7 @@ export declare const PricingDataSchema: z.ZodObject<{
616
623
  CEILING: "CEILING";
617
624
  NONE: "NONE";
618
625
  }>;
626
+ requiresPutty: z.ZodDefault<z.ZodBoolean>;
619
627
  }, z.core.$strip>>;
620
628
  materialTypes: z.ZodArray<z.ZodObject<{
621
629
  id: z.ZodString;
@@ -655,7 +663,7 @@ export declare const PricingDataSchema: z.ZodObject<{
655
663
  ENGINEERING: "ENGINEERING";
656
664
  ADDITIONAL: "ADDITIONAL";
657
665
  }>;
658
- priceCents: z.ZodInt;
666
+ priceCents: z.ZodNullable<z.ZodInt>;
659
667
  unit: z.ZodEnum<{
660
668
  PIECE: "PIECE";
661
669
  ROOM_SQM: "ROOM_SQM";
@@ -812,6 +820,7 @@ export interface FormulaVariable {
812
820
  export declare const FORMULA_SCOPE_VARIABLES: Record<FormulaScope, readonly FormulaVariable[]>;
813
821
  export declare const FORMULA_QUANTITY_VARIABLE: FormulaVariable;
814
822
  export declare const FORMULA_SURFACE_AREA_VARIABLE: FormulaVariable;
823
+ export declare const FORMULA_PUTTY_REQUIRED_VARIABLE: FormulaVariable;
815
824
  export declare const FORMULA_VARIABLES: Record<RuleLevel, readonly FormulaVariable[]>;
816
825
  export interface ValidateFormulaInput {
817
826
  readonly level: RuleLevel;
@@ -843,11 +852,12 @@ export interface ObjectVariables {
843
852
  readonly bathroomsCount: number;
844
853
  readonly roomsFloorArea: number;
845
854
  readonly roomsWallArea: number;
855
+ readonly roomsPuttyWallArea: number;
846
856
  readonly roomsPerimeter: number;
847
857
  readonly wetRoomsCount: number;
848
858
  }
849
859
  export declare function totalAreaOf(scope: Scope, totalAreaSqm: number | null, roomsFloorArea: number): number;
850
- export declare function objectVariables(configuration: Configuration): ObjectVariables;
860
+ export declare function objectVariables(configuration: Configuration, categories: readonly PricingCategory[]): ObjectVariables;
851
861
  //#endregion
852
862
  //#region src/variables/product.d.ts
853
863
  export interface ProductVariables {
@@ -858,6 +868,9 @@ export interface ProductVariables {
858
868
  export declare function productVariablesOf(product: Pick<PricingProduct, 'priceCents' | 'unit'>, wastePercent: number): ProductVariables;
859
869
  export declare function productVariables(product: PricingProduct, category: PricingCategory): ProductVariables;
860
870
  //#endregion
871
+ //#region src/variables/putty.d.ts
872
+ export declare function puttyRequiredOn(room: RoomConfiguration, surface: PricingCategory['surface'], categories: readonly PricingCategory[]): boolean;
873
+ //#endregion
861
874
  //#region src/variables/room.d.ts
862
875
  export interface RoomVariables {
863
876
  readonly length: number;
package/dist/index.js CHANGED
@@ -26,6 +26,12 @@ function toCents(dollars) {
26
26
  return cents;
27
27
  }
28
28
  //#endregion
29
+ //#region src/variables/putty.ts
30
+ function puttyRequiredOn(room, surface, categories) {
31
+ const selected = categories.filter((category) => category.surface === surface && room.selections.some((selection) => selection.categoryId === category.id));
32
+ return selected.length === 0 || selected.some((category) => category.requiresPutty);
33
+ }
34
+ //#endregion
29
35
  //#region src/variables/room.ts
30
36
  function roomVariables(room) {
31
37
  const perimeter = 2 * (room.lengthM + room.widthM);
@@ -55,7 +61,7 @@ const WET_ROOMS = [
55
61
  function totalAreaOf(scope, totalAreaSqm, roomsFloorArea) {
56
62
  return scope === "PARTIAL" || totalAreaSqm === null ? roomsFloorArea : totalAreaSqm;
57
63
  }
58
- function objectVariables(configuration) {
64
+ function objectVariables(configuration, categories) {
59
65
  const rooms = configuration.rooms.map(roomVariables);
60
66
  const sum = (pick) => rooms.reduce((total, room) => total + pick(room), 0);
61
67
  const roomsFloorArea = sum((room) => room.floorArea);
@@ -69,6 +75,7 @@ function objectVariables(configuration) {
69
75
  bathroomsCount: configuration.rooms.filter((room) => room.roomTypeCode === "BATHROOM").length,
70
76
  roomsFloorArea,
71
77
  roomsWallArea: sum((room) => room.wallArea),
78
+ roomsPuttyWallArea: configuration.rooms.reduce((total, room) => puttyRequiredOn(room, "WALLS", categories) ? total + roomVariables(room).wallArea : total, 0),
72
79
  roomsPerimeter: sum((room) => room.perimeter),
73
80
  wetRoomsCount: rooms.filter((room) => WET_ROOMS.includes(room.roomType)).length
74
81
  };
@@ -708,8 +715,11 @@ function applySurfacePrep(data, room, lines, objectScope) {
708
715
  };
709
716
  const surfaces = /* @__PURE__ */ new Map();
710
717
  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]);
718
+ const category = data.categories.find((item) => item.id === line.categoryId);
719
+ if (line.status === "OK" && category !== void 0 && category.surface !== "NONE") {
720
+ const surface = category.surface;
721
+ surfaces.set(surface, [...surfaces.get(surface) ?? [], index]);
722
+ }
713
723
  });
714
724
  for (const [surface, indexes] of surfaces) {
715
725
  const rule = surfaceRuleOf(data, surface);
@@ -717,7 +727,8 @@ function applySurfacePrep(data, room, lines, objectScope) {
717
727
  const outcome = runNumericFormula(rule.costFormula, {
718
728
  variables: {
719
729
  ...variables,
720
- surfaceArea: areas[surface]
730
+ surfaceArea: areas[surface],
731
+ puttyRequired: puttyRequiredOn(room, surface, data.categories)
721
732
  },
722
733
  coefficients: data.coefficients
723
734
  });
@@ -1052,7 +1063,8 @@ const PricingRoomTypeSchema = z.object({
1052
1063
  const PricingCategorySchema = z.object({
1053
1064
  id: z.string(),
1054
1065
  wastePercent: z.number().min(0),
1055
- surface: SurfaceKindSchema
1066
+ surface: SurfaceKindSchema,
1067
+ requiresPutty: z.boolean().default(true)
1056
1068
  });
1057
1069
  const PricingMaterialTypeSchema = z.object({
1058
1070
  id: z.string(),
@@ -1074,11 +1086,11 @@ const PricingPackageItemSchema = z.object({
1074
1086
  priceCents: CentsSchema.nullable(),
1075
1087
  unit: OptionUnitSchema.nullable()
1076
1088
  });
1077
- const PricingOptionSchema = z.object({
1089
+ const PricingOptionObjectSchema = z.object({
1078
1090
  id: z.string(),
1079
1091
  code: OptionCodeSchema.nullable(),
1080
1092
  kind: OptionKindSchema,
1081
- priceCents: CentsSchema,
1093
+ priceCents: CentsSchema.nullable(),
1082
1094
  unit: OptionUnitSchema,
1083
1095
  roomTypeCodes: z.array(z.string()),
1084
1096
  priceCoefficientKeys: z.array(z.string()),
@@ -1087,6 +1099,26 @@ const PricingOptionSchema = z.object({
1087
1099
  maxQuantity: z.int().nullable(),
1088
1100
  available: z.boolean()
1089
1101
  });
1102
+ function findOptionPriceSourceViolation(priceCents, priceCoefficientKeys) {
1103
+ const hasKeys = priceCoefficientKeys.length > 0;
1104
+ if (priceCents !== null && hasKeys) return {
1105
+ path: "priceCents",
1106
+ message: "priceCents must be null when priceCoefficientKeys is set"
1107
+ };
1108
+ if (priceCents === null && !hasKeys) return {
1109
+ path: "priceCoefficientKeys",
1110
+ message: "priceCoefficientKeys is required when priceCents is null"
1111
+ };
1112
+ return null;
1113
+ }
1114
+ const PricingOptionSchema = PricingOptionObjectSchema.superRefine((option, context) => {
1115
+ const violation = findOptionPriceSourceViolation(option.priceCents, option.priceCoefficientKeys);
1116
+ if (violation !== null) context.addIssue({
1117
+ code: "custom",
1118
+ path: [violation.path],
1119
+ message: violation.message
1120
+ });
1121
+ });
1090
1122
  const PricingDataSchema = z.object({
1091
1123
  version: z.string().regex(/^[0-9a-f]{16}$/),
1092
1124
  exchangeRate: ExchangeRateSchema.nullable(),
@@ -1166,6 +1198,7 @@ const RuleSampleSchema = z.object({
1166
1198
  wastePercent: z.number().min(0)
1167
1199
  }).optional(),
1168
1200
  surfaceArea: z.number().min(0).optional(),
1201
+ puttyRequired: z.boolean().optional(),
1169
1202
  object: z.object({
1170
1203
  scope: ScopeSchema,
1171
1204
  totalAreaSqm: z.number().positive().nullable(),
@@ -1176,6 +1209,7 @@ const RuleSampleSchema = z.object({
1176
1209
  bathroomsCount: z.int().min(0),
1177
1210
  roomsFloorArea: z.number().min(0),
1178
1211
  roomsWallArea: z.number().min(0),
1212
+ roomsPuttyWallArea: z.number().min(0).optional(),
1179
1213
  roomsPerimeter: z.number().min(0),
1180
1214
  wetRoomsCount: z.int().min(0)
1181
1215
  })
@@ -1260,7 +1294,7 @@ function withKopecks(line, rate) {
1260
1294
  }
1261
1295
  function estimate(configuration, data) {
1262
1296
  const rate = data.exchangeRate?.rate ?? null;
1263
- const variables = objectVariables(configuration);
1297
+ const variables = objectVariables(configuration, data.categories);
1264
1298
  const lines = [
1265
1299
  ...objectRuleLines(configuration, data, "BASE", variables),
1266
1300
  ...objectRuleLines(configuration, data, "OBJECT", variables),
@@ -1356,6 +1390,7 @@ function objectScope({ object }) {
1356
1390
  bathroomsCount: object.bathroomsCount,
1357
1391
  roomsFloorArea: object.roomsFloorArea,
1358
1392
  roomsWallArea: object.roomsWallArea,
1393
+ roomsPuttyWallArea: object.roomsPuttyWallArea ?? object.roomsWallArea,
1359
1394
  roomsPerimeter: object.roomsPerimeter,
1360
1395
  wetRoomsCount: object.wetRoomsCount
1361
1396
  };
@@ -1371,7 +1406,8 @@ function scopeOf(level, sample) {
1371
1406
  return { scope: {
1372
1407
  ...roomVariables(room),
1373
1408
  ...objectScope(sample),
1374
- surfaceArea
1409
+ surfaceArea,
1410
+ puttyRequired: sample.puttyRequired ?? true
1375
1411
  } };
1376
1412
  }
1377
1413
  if (room === void 0 || product === void 0) return { error: {
@@ -1513,12 +1549,14 @@ const FORMULA_SCOPE_VARIABLES = {
1513
1549
  enumeration("scope", ScopeSchema.options),
1514
1550
  number("roomsFloorArea"),
1515
1551
  number("roomsWallArea"),
1552
+ number("roomsPuttyWallArea"),
1516
1553
  number("roomsPerimeter"),
1517
1554
  number("wetRoomsCount")
1518
1555
  ]
1519
1556
  };
1520
1557
  const FORMULA_QUANTITY_VARIABLE = number("quantity");
1521
1558
  const FORMULA_SURFACE_AREA_VARIABLE = number("surfaceArea");
1559
+ const FORMULA_PUTTY_REQUIRED_VARIABLE = boolean("puttyRequired");
1522
1560
  const FORMULA_VARIABLES = {
1523
1561
  OBJECT: FORMULA_SCOPE_VARIABLES.object,
1524
1562
  CATEGORY: [
@@ -1534,7 +1572,8 @@ const FORMULA_VARIABLES = {
1534
1572
  SURFACE: [
1535
1573
  ...FORMULA_SCOPE_VARIABLES.room,
1536
1574
  ...FORMULA_SCOPE_VARIABLES.object,
1537
- FORMULA_SURFACE_AREA_VARIABLE
1575
+ FORMULA_SURFACE_AREA_VARIABLE,
1576
+ FORMULA_PUTTY_REQUIRED_VARIABLE
1538
1577
  ]
1539
1578
  };
1540
1579
  const ALL_VARIABLE_NAMES = new Set([...FORMULA_VARIABLES.CATEGORY, ...FORMULA_VARIABLES.SURFACE].map((variable) => variable.name));
@@ -1749,4 +1788,4 @@ function validateFormula(input) {
1749
1788
  };
1750
1789
  }
1751
1790
  //#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 };
1791
+ export { COEFFICIENT_NAMESPACE, CentsSchema, ConfigurationSchema, CurrencySchema, EstimateLineSchema, EstimateMessageSchema, EstimateSchema, EstimateTotalsSchema, EstimateWarningSchema, ExchangeRateSchema, FORMULA_FUNCTIONS, FORMULA_FUNCTION_TABLE, FORMULA_LIMITS, 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, checkLimits, convertCentsToKopecks, estimate, evaluate, findOptionPriceSourceViolation, levenshtein, objectVariables, parse, previewRule, productVariables, productVariablesOf, puttyRequiredOn, roomVariables, suggestNearest, 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.6.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",