@roomwise/calc-engine 0.6.0 → 0.8.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 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`.
50
+ - Engineering and additional: package items marked `includedInBase` become `INCLUDED` lines with a zero amount. In `PARTIAL` no package items are priced at all; the section holds only options. 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
 
@@ -193,7 +193,7 @@ Every schema is a zod schema with an inferred type of the same name without the
193
193
  | `totalAreaSqm` | required | `null` |
194
194
  | `BASE` section | `OBJECT` rules with `baseComponent: true`, one line each | none |
195
195
  | Material lines | `MATERIAL` component only | `MATERIAL`, `LABOR` and `PREP` components |
196
- | Package items with `includedInBase` | `INCLUDED` lines | skipped |
196
+ | Engineering package items | priced, `INCLUDED` with `includedInBase` | none |
197
197
  | `totalArea` variable | `totalAreaSqm` | `roomsFloorArea` |
198
198
  | Area warnings | `ROOMS_AREA_EXCEEDS_TOTAL`, `ROOMS_AREA_BELOW_TOTAL` | none |
199
199
 
package/dist/index.cjs CHANGED
@@ -51,13 +51,10 @@ 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
60
54
  //#region src/variables/room.ts
55
+ function surfaceAreaOf(variables, surface) {
56
+ return surface === "WALLS" ? variables.wallArea : variables.floorArea;
57
+ }
61
58
  function roomVariables(room) {
62
59
  const perimeter = 2 * (room.lengthM + room.widthM);
63
60
  return {
@@ -72,6 +69,63 @@ function roomVariables(room) {
72
69
  };
73
70
  }
74
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
75
129
  //#region src/variables/object.ts
76
130
  const COUNTED_AS_ROOMS = [
77
131
  "LIVING_ROOM",
@@ -100,7 +154,7 @@ function objectVariables(configuration, categories) {
100
154
  bathroomsCount: configuration.rooms.filter((room) => room.roomTypeCode === "BATHROOM").length,
101
155
  roomsFloorArea,
102
156
  roomsWallArea: sum((room) => room.wallArea),
103
- roomsPuttyWallArea: configuration.rooms.reduce((total, room) => puttyRequiredOn(room, "WALLS", categories) ? total + roomVariables(room).wallArea : total, 0),
157
+ roomsPuttyWallArea: configuration.rooms.reduce((total, room) => total + roomVariables(room).wallArea * puttyShareOn(room, "WALLS", categories), 0),
104
158
  roomsPerimeter: sum((room) => room.perimeter),
105
159
  wetRoomsCount: rooms.filter((room) => WET_ROOMS.includes(room.roomType)).length
106
160
  };
@@ -268,7 +322,9 @@ function packageLine(configuration, item) {
268
322
  }, item.priceCents / 100 * quantity);
269
323
  }
270
324
  function engineeringLines(configuration, data) {
271
- return [...(configuration.scope === "PARTIAL" ? data.packageItems.filter((item) => !item.includedInBase) : data.packageItems).map((item) => packageLine(configuration, item)), ...optionLines(configuration, data, "ENGINEERING")];
325
+ const options = optionLines(configuration, data, "ENGINEERING");
326
+ if (configuration.scope === "PARTIAL") return options;
327
+ return [...data.packageItems.map((item) => packageLine(configuration, item)), ...options];
272
328
  }
273
329
  //#endregion
274
330
  //#region src/formula/language.ts
@@ -733,11 +789,6 @@ function applySurfacePrep(data, room, lines, objectScope) {
733
789
  ...roomVariables(room),
734
790
  ...objectScope
735
791
  };
736
- const areas = {
737
- FLOOR: variables.floorArea,
738
- CEILING: variables.floorArea,
739
- WALLS: variables.wallArea
740
- };
741
792
  const surfaces = /* @__PURE__ */ new Map();
742
793
  lines.forEach((line, index) => {
743
794
  const category = data.categories.find((item) => item.id === line.categoryId);
@@ -749,11 +800,14 @@ function applySurfacePrep(data, room, lines, objectScope) {
749
800
  for (const [surface, indexes] of surfaces) {
750
801
  const rule = surfaceRuleOf(data, surface);
751
802
  if (rule === void 0) continue;
803
+ const surfaceArea = surfaceAreaOf(variables, surface);
804
+ const puttyShare = puttyShareOn(room, surface, data.categories);
752
805
  const outcome = runNumericFormula(rule.costFormula, {
753
806
  variables: {
754
807
  ...variables,
755
- surfaceArea: areas[surface],
756
- puttyRequired: puttyRequiredOn(room, surface, data.categories)
808
+ surfaceArea,
809
+ puttyRequired: puttyShare > 0,
810
+ puttyArea: surfaceArea * puttyShare
757
811
  },
758
812
  coefficients: data.coefficients
759
813
  });
@@ -786,7 +840,9 @@ function selectRule(data, categoryId, materialTypeId) {
786
840
  const rules = data.rules.filter(isMaterialRule);
787
841
  return rules.find((rule) => rule.level === "MATERIAL_TYPE" && rule.categoryId === categoryId && rule.materialTypeId === materialTypeId) ?? rules.find((rule) => rule.level === "CATEGORY" && rule.categoryId === categoryId);
788
842
  }
789
- function materialLine(configuration, data, room, categoryId, productId, objectScope) {
843
+ function materialLine(configuration, data, room, selection, objectScope) {
844
+ const { categoryId } = selection;
845
+ const productId = selection.productId;
790
846
  const init = {
791
847
  id: lineId("MATERIALS", room.clientId, categoryId),
792
848
  section: "MATERIALS",
@@ -794,6 +850,8 @@ function materialLine(configuration, data, room, categoryId, productId, objectSc
794
850
  categoryId,
795
851
  productId
796
852
  };
853
+ const shareError = areaShareError(room, selection, data.categories);
854
+ if (shareError !== null) return failedLine(init, "ERROR", shareError);
797
855
  const category = data.categories.find((item) => item.id === categoryId);
798
856
  const product = data.products.find((item) => item.id === productId && item.categoryId === categoryId);
799
857
  if (product === void 0 || category === void 0 || !product.available) return failedLine(init, "UNAVAILABLE", {
@@ -815,8 +873,12 @@ function materialLine(configuration, data, room, categoryId, productId, objectSc
815
873
  materialTypeId: product.materialTypeId
816
874
  }
817
875
  });
876
+ const base = roomVariables(room);
877
+ const share = areaShareOf(selection);
818
878
  const variables = {
819
- ...roomVariables(room),
879
+ ...base,
880
+ wallArea: category.surface === "WALLS" ? base.wallArea * share : base.wallArea,
881
+ floorArea: category.surface === "FLOOR" ? base.floorArea * share : base.floorArea,
820
882
  ...productVariables(product, category),
821
883
  ...objectScope
822
884
  };
@@ -886,7 +948,7 @@ function materialLine(configuration, data, room, categoryId, productId, objectSc
886
948
  }
887
949
  function materialLines(configuration, data, objectScope) {
888
950
  return configuration.rooms.flatMap((room) => {
889
- const lines = room.selections.flatMap(({ categoryId, productId }) => productId === null ? [] : [materialLine(configuration, data, room, categoryId, productId, objectScope)]);
951
+ const lines = room.selections.flatMap((selection) => selection.productId === null ? [] : [materialLine(configuration, data, room, selection, objectScope)]);
890
952
  return configuration.scope === "PARTIAL" ? applySurfacePrep(data, room, lines, objectScope) : lines;
891
953
  });
892
954
  }
@@ -984,7 +1046,8 @@ const RangeSchema = zod.z.object({
984
1046
  //#region src/schemas/configuration.ts
985
1047
  const RoomSelectionSchema = zod.z.object({
986
1048
  categoryId: zod.z.string(),
987
- productId: zod.z.string().nullable()
1049
+ productId: zod.z.string().nullable(),
1050
+ areaShare: zod.z.int().min(1).max(100).optional()
988
1051
  });
989
1052
  const RoomConfigurationSchema = zod.z.object({
990
1053
  clientId: zod.z.string(),
@@ -1224,6 +1287,7 @@ const RuleSampleSchema = zod.z.object({
1224
1287
  }).optional(),
1225
1288
  surfaceArea: zod.z.number().min(0).optional(),
1226
1289
  puttyRequired: zod.z.boolean().optional(),
1290
+ puttyArea: zod.z.number().min(0).optional(),
1227
1291
  object: zod.z.object({
1228
1292
  scope: ScopeSchema,
1229
1293
  totalAreaSqm: zod.z.number().positive().nullable(),
@@ -1302,10 +1366,32 @@ function roomsAreaWarnings(configuration, limits) {
1302
1366
  params: { missingSqm }
1303
1367
  }] : [];
1304
1368
  }
1305
- function estimateWarnings(lines, configuration, rate, limits) {
1369
+ const SHARE_SURFACES = ["WALLS", "FLOOR"];
1370
+ function areaShareWarnings(configuration, categories) {
1371
+ return configuration.rooms.flatMap((room) => {
1372
+ const variables = roomVariables(room);
1373
+ return SHARE_SURFACES.flatMap((surface) => {
1374
+ const shares = surfaceShares(room, surface, categories);
1375
+ if (!shares.shared || !shares.complete || shares.totalPercent >= 100) return [];
1376
+ const percent = 100 - shares.totalPercent;
1377
+ const area = surfaceAreaOf(variables, surface);
1378
+ return [{
1379
+ code: "AREA_SHARE_UNALLOCATED",
1380
+ params: {
1381
+ roomClientId: room.clientId,
1382
+ surface,
1383
+ percent,
1384
+ sqm: roundArea(area * percent / 100)
1385
+ }
1386
+ }];
1387
+ });
1388
+ });
1389
+ }
1390
+ function estimateWarnings(lines, configuration, rate, data) {
1306
1391
  return [
1307
1392
  ...collectWarnings(lines),
1308
- ...roomsAreaWarnings(configuration, limits),
1393
+ ...roomsAreaWarnings(configuration, data.limits),
1394
+ ...areaShareWarnings(configuration, data.categories),
1309
1395
  ...configuration.currency === "UAH" && rate === null ? [{ code: "EXCHANGE_RATE_UNAVAILABLE" }] : []
1310
1396
  ];
1311
1397
  }
@@ -1333,7 +1419,7 @@ function estimate(configuration, data) {
1333
1419
  exchangeRate: data.exchangeRate,
1334
1420
  lines,
1335
1421
  totals: computeTotals(lines, rate !== null),
1336
- warnings: estimateWarnings(lines, configuration, rate, data.limits),
1422
+ warnings: estimateWarnings(lines, configuration, rate, data),
1337
1423
  incomplete: isIncomplete(lines)
1338
1424
  };
1339
1425
  }
@@ -1428,11 +1514,13 @@ function scopeOf(level, sample) {
1428
1514
  code: "SAMPLE_INCOMPLETE",
1429
1515
  params: { missing: room === void 0 ? "room" : "surfaceArea" }
1430
1516
  } };
1517
+ const puttyRequired = sample.puttyRequired ?? true;
1431
1518
  return { scope: {
1432
1519
  ...roomVariables(room),
1433
1520
  ...objectScope(sample),
1434
1521
  surfaceArea,
1435
- puttyRequired: sample.puttyRequired ?? true
1522
+ puttyRequired,
1523
+ puttyArea: sample.puttyArea ?? (puttyRequired ? surfaceArea : 0)
1436
1524
  } };
1437
1525
  }
1438
1526
  if (room === void 0 || product === void 0) return { error: {
@@ -1581,6 +1669,7 @@ const FORMULA_SCOPE_VARIABLES = {
1581
1669
  };
1582
1670
  const FORMULA_QUANTITY_VARIABLE = number("quantity");
1583
1671
  const FORMULA_SURFACE_AREA_VARIABLE = number("surfaceArea");
1672
+ const FORMULA_PUTTY_AREA_VARIABLE = number("puttyArea");
1584
1673
  const FORMULA_PUTTY_REQUIRED_VARIABLE = boolean("puttyRequired");
1585
1674
  const FORMULA_VARIABLES = {
1586
1675
  OBJECT: FORMULA_SCOPE_VARIABLES.object,
@@ -1598,7 +1687,8 @@ const FORMULA_VARIABLES = {
1598
1687
  ...FORMULA_SCOPE_VARIABLES.room,
1599
1688
  ...FORMULA_SCOPE_VARIABLES.object,
1600
1689
  FORMULA_SURFACE_AREA_VARIABLE,
1601
- FORMULA_PUTTY_REQUIRED_VARIABLE
1690
+ FORMULA_PUTTY_REQUIRED_VARIABLE,
1691
+ FORMULA_PUTTY_AREA_VARIABLE
1602
1692
  ]
1603
1693
  };
1604
1694
  const ALL_VARIABLE_NAMES = new Set([...FORMULA_VARIABLES.CATEGORY, ...FORMULA_VARIABLES.SURFACE].map((variable) => variable.name));
@@ -1826,6 +1916,7 @@ exports.ExchangeRateSchema = ExchangeRateSchema;
1826
1916
  exports.FORMULA_FUNCTIONS = FORMULA_FUNCTIONS;
1827
1917
  exports.FORMULA_FUNCTION_TABLE = FORMULA_FUNCTION_TABLE;
1828
1918
  exports.FORMULA_LIMITS = FORMULA_LIMITS;
1919
+ exports.FORMULA_PUTTY_AREA_VARIABLE = FORMULA_PUTTY_AREA_VARIABLE;
1829
1920
  exports.FORMULA_PUTTY_REQUIRED_VARIABLE = FORMULA_PUTTY_REQUIRED_VARIABLE;
1830
1921
  exports.FORMULA_QUANTITY_VARIABLE = FORMULA_QUANTITY_VARIABLE;
1831
1922
  exports.FORMULA_SCOPE_VARIABLES = FORMULA_SCOPE_VARIABLES;
@@ -1862,11 +1953,14 @@ exports.ScopeSchema = ScopeSchema;
1862
1953
  exports.SectionSchema = SectionSchema;
1863
1954
  exports.SectionTotalSchema = SectionTotalSchema;
1864
1955
  exports.SurfaceKindSchema = SurfaceKindSchema;
1956
+ exports.areaShareError = areaShareError;
1957
+ exports.areaShareOf = areaShareOf;
1865
1958
  exports.checkLimits = checkLimits;
1866
1959
  exports.convertCentsToKopecks = convertCentsToKopecks;
1867
1960
  exports.estimate = estimate;
1868
1961
  exports.evaluate = evaluate;
1869
1962
  exports.findOptionPriceSourceViolation = findOptionPriceSourceViolation;
1963
+ exports.isShareSurface = isShareSurface;
1870
1964
  exports.levenshtein = levenshtein;
1871
1965
  exports.objectVariables = objectVariables;
1872
1966
  exports.parse = parse;
@@ -1874,8 +1968,11 @@ exports.previewRule = previewRule;
1874
1968
  exports.productVariables = productVariables;
1875
1969
  exports.productVariablesOf = productVariablesOf;
1876
1970
  exports.puttyRequiredOn = puttyRequiredOn;
1971
+ exports.puttyShareOn = puttyShareOn;
1877
1972
  exports.roomVariables = roomVariables;
1878
1973
  exports.suggestNearest = suggestNearest;
1974
+ exports.surfaceAreaOf = surfaceAreaOf;
1975
+ exports.surfaceShares = surfaceShares;
1879
1976
  exports.toCents = toCents;
1880
1977
  exports.totalAreaOf = totalAreaOf;
1881
1978
  exports.validateFormula = validateFormula;
package/dist/index.d.cts CHANGED
@@ -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<{
@@ -383,6 +386,7 @@ export declare const RuleSampleSchema: z.ZodObject<{
383
386
  }, z.core.$strip>>;
384
387
  surfaceArea: z.ZodOptional<z.ZodNumber>;
385
388
  puttyRequired: z.ZodOptional<z.ZodBoolean>;
389
+ puttyArea: z.ZodOptional<z.ZodNumber>;
386
390
  object: z.ZodObject<{
387
391
  scope: z.ZodEnum<{
388
392
  FULL: "FULL";
@@ -820,6 +824,7 @@ export interface FormulaVariable {
820
824
  export declare const FORMULA_SCOPE_VARIABLES: Record<FormulaScope, readonly FormulaVariable[]>;
821
825
  export declare const FORMULA_QUANTITY_VARIABLE: FormulaVariable;
822
826
  export declare const FORMULA_SURFACE_AREA_VARIABLE: FormulaVariable;
827
+ export declare const FORMULA_PUTTY_AREA_VARIABLE: FormulaVariable;
823
828
  export declare const FORMULA_PUTTY_REQUIRED_VARIABLE: FormulaVariable;
824
829
  export declare const FORMULA_VARIABLES: Record<RuleLevel, readonly FormulaVariable[]>;
825
830
  export interface ValidateFormulaInput {
@@ -882,6 +887,22 @@ export interface RoomVariables {
882
887
  readonly heatedFloor: boolean;
883
888
  readonly roomType: RoomTypeCode;
884
889
  }
890
+ export type MeasuredSurface = 'FLOOR' | 'WALLS' | 'CEILING';
891
+ export declare function surfaceAreaOf(variables: Pick<RoomVariables, 'floorArea' | 'wallArea'>, surface: MeasuredSurface): number;
885
892
  export type RoomDimensions = Pick<RoomConfiguration, 'roomTypeCode' | 'lengthM' | 'widthM' | 'heightM' | 'heatedFloor'>;
886
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;
887
908
  //#endregion
package/dist/index.d.ts CHANGED
@@ -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<{
@@ -383,6 +386,7 @@ export declare const RuleSampleSchema: z.ZodObject<{
383
386
  }, z.core.$strip>>;
384
387
  surfaceArea: z.ZodOptional<z.ZodNumber>;
385
388
  puttyRequired: z.ZodOptional<z.ZodBoolean>;
389
+ puttyArea: z.ZodOptional<z.ZodNumber>;
386
390
  object: z.ZodObject<{
387
391
  scope: z.ZodEnum<{
388
392
  FULL: "FULL";
@@ -820,6 +824,7 @@ export interface FormulaVariable {
820
824
  export declare const FORMULA_SCOPE_VARIABLES: Record<FormulaScope, readonly FormulaVariable[]>;
821
825
  export declare const FORMULA_QUANTITY_VARIABLE: FormulaVariable;
822
826
  export declare const FORMULA_SURFACE_AREA_VARIABLE: FormulaVariable;
827
+ export declare const FORMULA_PUTTY_AREA_VARIABLE: FormulaVariable;
823
828
  export declare const FORMULA_PUTTY_REQUIRED_VARIABLE: FormulaVariable;
824
829
  export declare const FORMULA_VARIABLES: Record<RuleLevel, readonly FormulaVariable[]>;
825
830
  export interface ValidateFormulaInput {
@@ -882,6 +887,22 @@ export interface RoomVariables {
882
887
  readonly heatedFloor: boolean;
883
888
  readonly roomType: RoomTypeCode;
884
889
  }
890
+ export type MeasuredSurface = 'FLOOR' | 'WALLS' | 'CEILING';
891
+ export declare function surfaceAreaOf(variables: Pick<RoomVariables, 'floorArea' | 'wallArea'>, surface: MeasuredSurface): number;
885
892
  export type RoomDimensions = Pick<RoomConfiguration, 'roomTypeCode' | 'lengthM' | 'widthM' | 'heightM' | 'heatedFloor'>;
886
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;
887
908
  //#endregion
package/dist/index.js CHANGED
@@ -26,13 +26,10 @@ 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
35
29
  //#region src/variables/room.ts
30
+ function surfaceAreaOf(variables, surface) {
31
+ return surface === "WALLS" ? variables.wallArea : variables.floorArea;
32
+ }
36
33
  function roomVariables(room) {
37
34
  const perimeter = 2 * (room.lengthM + room.widthM);
38
35
  return {
@@ -47,6 +44,63 @@ function roomVariables(room) {
47
44
  };
48
45
  }
49
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
50
104
  //#region src/variables/object.ts
51
105
  const COUNTED_AS_ROOMS = [
52
106
  "LIVING_ROOM",
@@ -75,7 +129,7 @@ function objectVariables(configuration, categories) {
75
129
  bathroomsCount: configuration.rooms.filter((room) => room.roomTypeCode === "BATHROOM").length,
76
130
  roomsFloorArea,
77
131
  roomsWallArea: sum((room) => room.wallArea),
78
- roomsPuttyWallArea: configuration.rooms.reduce((total, room) => puttyRequiredOn(room, "WALLS", categories) ? total + roomVariables(room).wallArea : total, 0),
132
+ roomsPuttyWallArea: configuration.rooms.reduce((total, room) => total + roomVariables(room).wallArea * puttyShareOn(room, "WALLS", categories), 0),
79
133
  roomsPerimeter: sum((room) => room.perimeter),
80
134
  wetRoomsCount: rooms.filter((room) => WET_ROOMS.includes(room.roomType)).length
81
135
  };
@@ -243,7 +297,9 @@ function packageLine(configuration, item) {
243
297
  }, item.priceCents / 100 * quantity);
244
298
  }
245
299
  function engineeringLines(configuration, data) {
246
- return [...(configuration.scope === "PARTIAL" ? data.packageItems.filter((item) => !item.includedInBase) : data.packageItems).map((item) => packageLine(configuration, item)), ...optionLines(configuration, data, "ENGINEERING")];
300
+ const options = optionLines(configuration, data, "ENGINEERING");
301
+ if (configuration.scope === "PARTIAL") return options;
302
+ return [...data.packageItems.map((item) => packageLine(configuration, item)), ...options];
247
303
  }
248
304
  //#endregion
249
305
  //#region src/formula/language.ts
@@ -708,11 +764,6 @@ function applySurfacePrep(data, room, lines, objectScope) {
708
764
  ...roomVariables(room),
709
765
  ...objectScope
710
766
  };
711
- const areas = {
712
- FLOOR: variables.floorArea,
713
- CEILING: variables.floorArea,
714
- WALLS: variables.wallArea
715
- };
716
767
  const surfaces = /* @__PURE__ */ new Map();
717
768
  lines.forEach((line, index) => {
718
769
  const category = data.categories.find((item) => item.id === line.categoryId);
@@ -724,11 +775,14 @@ function applySurfacePrep(data, room, lines, objectScope) {
724
775
  for (const [surface, indexes] of surfaces) {
725
776
  const rule = surfaceRuleOf(data, surface);
726
777
  if (rule === void 0) continue;
778
+ const surfaceArea = surfaceAreaOf(variables, surface);
779
+ const puttyShare = puttyShareOn(room, surface, data.categories);
727
780
  const outcome = runNumericFormula(rule.costFormula, {
728
781
  variables: {
729
782
  ...variables,
730
- surfaceArea: areas[surface],
731
- puttyRequired: puttyRequiredOn(room, surface, data.categories)
783
+ surfaceArea,
784
+ puttyRequired: puttyShare > 0,
785
+ puttyArea: surfaceArea * puttyShare
732
786
  },
733
787
  coefficients: data.coefficients
734
788
  });
@@ -761,7 +815,9 @@ function selectRule(data, categoryId, materialTypeId) {
761
815
  const rules = data.rules.filter(isMaterialRule);
762
816
  return rules.find((rule) => rule.level === "MATERIAL_TYPE" && rule.categoryId === categoryId && rule.materialTypeId === materialTypeId) ?? rules.find((rule) => rule.level === "CATEGORY" && rule.categoryId === categoryId);
763
817
  }
764
- function materialLine(configuration, data, room, categoryId, productId, objectScope) {
818
+ function materialLine(configuration, data, room, selection, objectScope) {
819
+ const { categoryId } = selection;
820
+ const productId = selection.productId;
765
821
  const init = {
766
822
  id: lineId("MATERIALS", room.clientId, categoryId),
767
823
  section: "MATERIALS",
@@ -769,6 +825,8 @@ function materialLine(configuration, data, room, categoryId, productId, objectSc
769
825
  categoryId,
770
826
  productId
771
827
  };
828
+ const shareError = areaShareError(room, selection, data.categories);
829
+ if (shareError !== null) return failedLine(init, "ERROR", shareError);
772
830
  const category = data.categories.find((item) => item.id === categoryId);
773
831
  const product = data.products.find((item) => item.id === productId && item.categoryId === categoryId);
774
832
  if (product === void 0 || category === void 0 || !product.available) return failedLine(init, "UNAVAILABLE", {
@@ -790,8 +848,12 @@ function materialLine(configuration, data, room, categoryId, productId, objectSc
790
848
  materialTypeId: product.materialTypeId
791
849
  }
792
850
  });
851
+ const base = roomVariables(room);
852
+ const share = areaShareOf(selection);
793
853
  const variables = {
794
- ...roomVariables(room),
854
+ ...base,
855
+ wallArea: category.surface === "WALLS" ? base.wallArea * share : base.wallArea,
856
+ floorArea: category.surface === "FLOOR" ? base.floorArea * share : base.floorArea,
795
857
  ...productVariables(product, category),
796
858
  ...objectScope
797
859
  };
@@ -861,7 +923,7 @@ function materialLine(configuration, data, room, categoryId, productId, objectSc
861
923
  }
862
924
  function materialLines(configuration, data, objectScope) {
863
925
  return configuration.rooms.flatMap((room) => {
864
- const lines = room.selections.flatMap(({ categoryId, productId }) => productId === null ? [] : [materialLine(configuration, data, room, categoryId, productId, objectScope)]);
926
+ const lines = room.selections.flatMap((selection) => selection.productId === null ? [] : [materialLine(configuration, data, room, selection, objectScope)]);
865
927
  return configuration.scope === "PARTIAL" ? applySurfacePrep(data, room, lines, objectScope) : lines;
866
928
  });
867
929
  }
@@ -959,7 +1021,8 @@ const RangeSchema = z.object({
959
1021
  //#region src/schemas/configuration.ts
960
1022
  const RoomSelectionSchema = z.object({
961
1023
  categoryId: z.string(),
962
- productId: z.string().nullable()
1024
+ productId: z.string().nullable(),
1025
+ areaShare: z.int().min(1).max(100).optional()
963
1026
  });
964
1027
  const RoomConfigurationSchema = z.object({
965
1028
  clientId: z.string(),
@@ -1199,6 +1262,7 @@ const RuleSampleSchema = z.object({
1199
1262
  }).optional(),
1200
1263
  surfaceArea: z.number().min(0).optional(),
1201
1264
  puttyRequired: z.boolean().optional(),
1265
+ puttyArea: z.number().min(0).optional(),
1202
1266
  object: z.object({
1203
1267
  scope: ScopeSchema,
1204
1268
  totalAreaSqm: z.number().positive().nullable(),
@@ -1277,10 +1341,32 @@ function roomsAreaWarnings(configuration, limits) {
1277
1341
  params: { missingSqm }
1278
1342
  }] : [];
1279
1343
  }
1280
- function estimateWarnings(lines, configuration, rate, limits) {
1344
+ const SHARE_SURFACES = ["WALLS", "FLOOR"];
1345
+ function areaShareWarnings(configuration, categories) {
1346
+ return configuration.rooms.flatMap((room) => {
1347
+ const variables = roomVariables(room);
1348
+ return SHARE_SURFACES.flatMap((surface) => {
1349
+ const shares = surfaceShares(room, surface, categories);
1350
+ if (!shares.shared || !shares.complete || shares.totalPercent >= 100) return [];
1351
+ const percent = 100 - shares.totalPercent;
1352
+ const area = surfaceAreaOf(variables, surface);
1353
+ return [{
1354
+ code: "AREA_SHARE_UNALLOCATED",
1355
+ params: {
1356
+ roomClientId: room.clientId,
1357
+ surface,
1358
+ percent,
1359
+ sqm: roundArea(area * percent / 100)
1360
+ }
1361
+ }];
1362
+ });
1363
+ });
1364
+ }
1365
+ function estimateWarnings(lines, configuration, rate, data) {
1281
1366
  return [
1282
1367
  ...collectWarnings(lines),
1283
- ...roomsAreaWarnings(configuration, limits),
1368
+ ...roomsAreaWarnings(configuration, data.limits),
1369
+ ...areaShareWarnings(configuration, data.categories),
1284
1370
  ...configuration.currency === "UAH" && rate === null ? [{ code: "EXCHANGE_RATE_UNAVAILABLE" }] : []
1285
1371
  ];
1286
1372
  }
@@ -1308,7 +1394,7 @@ function estimate(configuration, data) {
1308
1394
  exchangeRate: data.exchangeRate,
1309
1395
  lines,
1310
1396
  totals: computeTotals(lines, rate !== null),
1311
- warnings: estimateWarnings(lines, configuration, rate, data.limits),
1397
+ warnings: estimateWarnings(lines, configuration, rate, data),
1312
1398
  incomplete: isIncomplete(lines)
1313
1399
  };
1314
1400
  }
@@ -1403,11 +1489,13 @@ function scopeOf(level, sample) {
1403
1489
  code: "SAMPLE_INCOMPLETE",
1404
1490
  params: { missing: room === void 0 ? "room" : "surfaceArea" }
1405
1491
  } };
1492
+ const puttyRequired = sample.puttyRequired ?? true;
1406
1493
  return { scope: {
1407
1494
  ...roomVariables(room),
1408
1495
  ...objectScope(sample),
1409
1496
  surfaceArea,
1410
- puttyRequired: sample.puttyRequired ?? true
1497
+ puttyRequired,
1498
+ puttyArea: sample.puttyArea ?? (puttyRequired ? surfaceArea : 0)
1411
1499
  } };
1412
1500
  }
1413
1501
  if (room === void 0 || product === void 0) return { error: {
@@ -1556,6 +1644,7 @@ const FORMULA_SCOPE_VARIABLES = {
1556
1644
  };
1557
1645
  const FORMULA_QUANTITY_VARIABLE = number("quantity");
1558
1646
  const FORMULA_SURFACE_AREA_VARIABLE = number("surfaceArea");
1647
+ const FORMULA_PUTTY_AREA_VARIABLE = number("puttyArea");
1559
1648
  const FORMULA_PUTTY_REQUIRED_VARIABLE = boolean("puttyRequired");
1560
1649
  const FORMULA_VARIABLES = {
1561
1650
  OBJECT: FORMULA_SCOPE_VARIABLES.object,
@@ -1573,7 +1662,8 @@ const FORMULA_VARIABLES = {
1573
1662
  ...FORMULA_SCOPE_VARIABLES.room,
1574
1663
  ...FORMULA_SCOPE_VARIABLES.object,
1575
1664
  FORMULA_SURFACE_AREA_VARIABLE,
1576
- FORMULA_PUTTY_REQUIRED_VARIABLE
1665
+ FORMULA_PUTTY_REQUIRED_VARIABLE,
1666
+ FORMULA_PUTTY_AREA_VARIABLE
1577
1667
  ]
1578
1668
  };
1579
1669
  const ALL_VARIABLE_NAMES = new Set([...FORMULA_VARIABLES.CATEGORY, ...FORMULA_VARIABLES.SURFACE].map((variable) => variable.name));
@@ -1788,4 +1878,4 @@ function validateFormula(input) {
1788
1878
  };
1789
1879
  }
1790
1880
  //#endregion
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 };
1881
+ 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.6.0",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",