@roomwise/calc-engine 0.6.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/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
  };
@@ -733,11 +787,6 @@ function applySurfacePrep(data, room, lines, objectScope) {
733
787
  ...roomVariables(room),
734
788
  ...objectScope
735
789
  };
736
- const areas = {
737
- FLOOR: variables.floorArea,
738
- CEILING: variables.floorArea,
739
- WALLS: variables.wallArea
740
- };
741
790
  const surfaces = /* @__PURE__ */ new Map();
742
791
  lines.forEach((line, index) => {
743
792
  const category = data.categories.find((item) => item.id === line.categoryId);
@@ -749,11 +798,14 @@ function applySurfacePrep(data, room, lines, objectScope) {
749
798
  for (const [surface, indexes] of surfaces) {
750
799
  const rule = surfaceRuleOf(data, surface);
751
800
  if (rule === void 0) continue;
801
+ const surfaceArea = surfaceAreaOf(variables, surface);
802
+ const puttyShare = puttyShareOn(room, surface, data.categories);
752
803
  const outcome = runNumericFormula(rule.costFormula, {
753
804
  variables: {
754
805
  ...variables,
755
- surfaceArea: areas[surface],
756
- puttyRequired: puttyRequiredOn(room, surface, data.categories)
806
+ surfaceArea,
807
+ puttyRequired: puttyShare > 0,
808
+ puttyArea: surfaceArea * puttyShare
757
809
  },
758
810
  coefficients: data.coefficients
759
811
  });
@@ -786,7 +838,9 @@ function selectRule(data, categoryId, materialTypeId) {
786
838
  const rules = data.rules.filter(isMaterialRule);
787
839
  return rules.find((rule) => rule.level === "MATERIAL_TYPE" && rule.categoryId === categoryId && rule.materialTypeId === materialTypeId) ?? rules.find((rule) => rule.level === "CATEGORY" && rule.categoryId === categoryId);
788
840
  }
789
- 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;
790
844
  const init = {
791
845
  id: lineId("MATERIALS", room.clientId, categoryId),
792
846
  section: "MATERIALS",
@@ -794,6 +848,8 @@ function materialLine(configuration, data, room, categoryId, productId, objectSc
794
848
  categoryId,
795
849
  productId
796
850
  };
851
+ const shareError = areaShareError(room, selection, data.categories);
852
+ if (shareError !== null) return failedLine(init, "ERROR", shareError);
797
853
  const category = data.categories.find((item) => item.id === categoryId);
798
854
  const product = data.products.find((item) => item.id === productId && item.categoryId === categoryId);
799
855
  if (product === void 0 || category === void 0 || !product.available) return failedLine(init, "UNAVAILABLE", {
@@ -815,8 +871,12 @@ function materialLine(configuration, data, room, categoryId, productId, objectSc
815
871
  materialTypeId: product.materialTypeId
816
872
  }
817
873
  });
874
+ const base = roomVariables(room);
875
+ const share = areaShareOf(selection);
818
876
  const variables = {
819
- ...roomVariables(room),
877
+ ...base,
878
+ wallArea: category.surface === "WALLS" ? base.wallArea * share : base.wallArea,
879
+ floorArea: category.surface === "FLOOR" ? base.floorArea * share : base.floorArea,
820
880
  ...productVariables(product, category),
821
881
  ...objectScope
822
882
  };
@@ -886,7 +946,7 @@ function materialLine(configuration, data, room, categoryId, productId, objectSc
886
946
  }
887
947
  function materialLines(configuration, data, objectScope) {
888
948
  return configuration.rooms.flatMap((room) => {
889
- 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)]);
890
950
  return configuration.scope === "PARTIAL" ? applySurfacePrep(data, room, lines, objectScope) : lines;
891
951
  });
892
952
  }
@@ -984,7 +1044,8 @@ const RangeSchema = zod.z.object({
984
1044
  //#region src/schemas/configuration.ts
985
1045
  const RoomSelectionSchema = zod.z.object({
986
1046
  categoryId: zod.z.string(),
987
- productId: zod.z.string().nullable()
1047
+ productId: zod.z.string().nullable(),
1048
+ areaShare: zod.z.int().min(1).max(100).optional()
988
1049
  });
989
1050
  const RoomConfigurationSchema = zod.z.object({
990
1051
  clientId: zod.z.string(),
@@ -1224,6 +1285,7 @@ const RuleSampleSchema = zod.z.object({
1224
1285
  }).optional(),
1225
1286
  surfaceArea: zod.z.number().min(0).optional(),
1226
1287
  puttyRequired: zod.z.boolean().optional(),
1288
+ puttyArea: zod.z.number().min(0).optional(),
1227
1289
  object: zod.z.object({
1228
1290
  scope: ScopeSchema,
1229
1291
  totalAreaSqm: zod.z.number().positive().nullable(),
@@ -1302,10 +1364,32 @@ function roomsAreaWarnings(configuration, limits) {
1302
1364
  params: { missingSqm }
1303
1365
  }] : [];
1304
1366
  }
1305
- 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) {
1306
1389
  return [
1307
1390
  ...collectWarnings(lines),
1308
- ...roomsAreaWarnings(configuration, limits),
1391
+ ...roomsAreaWarnings(configuration, data.limits),
1392
+ ...areaShareWarnings(configuration, data.categories),
1309
1393
  ...configuration.currency === "UAH" && rate === null ? [{ code: "EXCHANGE_RATE_UNAVAILABLE" }] : []
1310
1394
  ];
1311
1395
  }
@@ -1333,7 +1417,7 @@ function estimate(configuration, data) {
1333
1417
  exchangeRate: data.exchangeRate,
1334
1418
  lines,
1335
1419
  totals: computeTotals(lines, rate !== null),
1336
- warnings: estimateWarnings(lines, configuration, rate, data.limits),
1420
+ warnings: estimateWarnings(lines, configuration, rate, data),
1337
1421
  incomplete: isIncomplete(lines)
1338
1422
  };
1339
1423
  }
@@ -1428,11 +1512,13 @@ function scopeOf(level, sample) {
1428
1512
  code: "SAMPLE_INCOMPLETE",
1429
1513
  params: { missing: room === void 0 ? "room" : "surfaceArea" }
1430
1514
  } };
1515
+ const puttyRequired = sample.puttyRequired ?? true;
1431
1516
  return { scope: {
1432
1517
  ...roomVariables(room),
1433
1518
  ...objectScope(sample),
1434
1519
  surfaceArea,
1435
- puttyRequired: sample.puttyRequired ?? true
1520
+ puttyRequired,
1521
+ puttyArea: sample.puttyArea ?? (puttyRequired ? surfaceArea : 0)
1436
1522
  } };
1437
1523
  }
1438
1524
  if (room === void 0 || product === void 0) return { error: {
@@ -1581,6 +1667,7 @@ const FORMULA_SCOPE_VARIABLES = {
1581
1667
  };
1582
1668
  const FORMULA_QUANTITY_VARIABLE = number("quantity");
1583
1669
  const FORMULA_SURFACE_AREA_VARIABLE = number("surfaceArea");
1670
+ const FORMULA_PUTTY_AREA_VARIABLE = number("puttyArea");
1584
1671
  const FORMULA_PUTTY_REQUIRED_VARIABLE = boolean("puttyRequired");
1585
1672
  const FORMULA_VARIABLES = {
1586
1673
  OBJECT: FORMULA_SCOPE_VARIABLES.object,
@@ -1598,7 +1685,8 @@ const FORMULA_VARIABLES = {
1598
1685
  ...FORMULA_SCOPE_VARIABLES.room,
1599
1686
  ...FORMULA_SCOPE_VARIABLES.object,
1600
1687
  FORMULA_SURFACE_AREA_VARIABLE,
1601
- FORMULA_PUTTY_REQUIRED_VARIABLE
1688
+ FORMULA_PUTTY_REQUIRED_VARIABLE,
1689
+ FORMULA_PUTTY_AREA_VARIABLE
1602
1690
  ]
1603
1691
  };
1604
1692
  const ALL_VARIABLE_NAMES = new Set([...FORMULA_VARIABLES.CATEGORY, ...FORMULA_VARIABLES.SURFACE].map((variable) => variable.name));
@@ -1826,6 +1914,7 @@ exports.ExchangeRateSchema = ExchangeRateSchema;
1826
1914
  exports.FORMULA_FUNCTIONS = FORMULA_FUNCTIONS;
1827
1915
  exports.FORMULA_FUNCTION_TABLE = FORMULA_FUNCTION_TABLE;
1828
1916
  exports.FORMULA_LIMITS = FORMULA_LIMITS;
1917
+ exports.FORMULA_PUTTY_AREA_VARIABLE = FORMULA_PUTTY_AREA_VARIABLE;
1829
1918
  exports.FORMULA_PUTTY_REQUIRED_VARIABLE = FORMULA_PUTTY_REQUIRED_VARIABLE;
1830
1919
  exports.FORMULA_QUANTITY_VARIABLE = FORMULA_QUANTITY_VARIABLE;
1831
1920
  exports.FORMULA_SCOPE_VARIABLES = FORMULA_SCOPE_VARIABLES;
@@ -1862,11 +1951,14 @@ exports.ScopeSchema = ScopeSchema;
1862
1951
  exports.SectionSchema = SectionSchema;
1863
1952
  exports.SectionTotalSchema = SectionTotalSchema;
1864
1953
  exports.SurfaceKindSchema = SurfaceKindSchema;
1954
+ exports.areaShareError = areaShareError;
1955
+ exports.areaShareOf = areaShareOf;
1865
1956
  exports.checkLimits = checkLimits;
1866
1957
  exports.convertCentsToKopecks = convertCentsToKopecks;
1867
1958
  exports.estimate = estimate;
1868
1959
  exports.evaluate = evaluate;
1869
1960
  exports.findOptionPriceSourceViolation = findOptionPriceSourceViolation;
1961
+ exports.isShareSurface = isShareSurface;
1870
1962
  exports.levenshtein = levenshtein;
1871
1963
  exports.objectVariables = objectVariables;
1872
1964
  exports.parse = parse;
@@ -1874,8 +1966,11 @@ exports.previewRule = previewRule;
1874
1966
  exports.productVariables = productVariables;
1875
1967
  exports.productVariablesOf = productVariablesOf;
1876
1968
  exports.puttyRequiredOn = puttyRequiredOn;
1969
+ exports.puttyShareOn = puttyShareOn;
1877
1970
  exports.roomVariables = roomVariables;
1878
1971
  exports.suggestNearest = suggestNearest;
1972
+ exports.surfaceAreaOf = surfaceAreaOf;
1973
+ exports.surfaceShares = surfaceShares;
1879
1974
  exports.toCents = toCents;
1880
1975
  exports.totalAreaOf = totalAreaOf;
1881
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<{
@@ -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";
@@ -484,10 +488,10 @@ export declare const PricingCategorySchema: z.ZodObject<{
484
488
  id: z.ZodString;
485
489
  wastePercent: z.ZodNumber;
486
490
  surface: z.ZodEnum<{
491
+ NONE: "NONE";
487
492
  FLOOR: "FLOOR";
488
493
  WALLS: "WALLS";
489
494
  CEILING: "CEILING";
490
- NONE: "NONE";
491
495
  }>;
492
496
  requiresPutty: z.ZodDefault<z.ZodBoolean>;
493
497
  }, z.core.$strip>;
@@ -618,10 +622,10 @@ export declare const PricingDataSchema: z.ZodObject<{
618
622
  id: z.ZodString;
619
623
  wastePercent: z.ZodNumber;
620
624
  surface: z.ZodEnum<{
625
+ NONE: "NONE";
621
626
  FLOOR: "FLOOR";
622
627
  WALLS: "WALLS";
623
628
  CEILING: "CEILING";
624
- NONE: "NONE";
625
629
  }>;
626
630
  requiresPutty: z.ZodDefault<z.ZodBoolean>;
627
631
  }, z.core.$strip>>;
@@ -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
@@ -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<{
@@ -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";
@@ -484,10 +488,10 @@ export declare const PricingCategorySchema: z.ZodObject<{
484
488
  id: z.ZodString;
485
489
  wastePercent: z.ZodNumber;
486
490
  surface: z.ZodEnum<{
491
+ NONE: "NONE";
487
492
  FLOOR: "FLOOR";
488
493
  WALLS: "WALLS";
489
494
  CEILING: "CEILING";
490
- NONE: "NONE";
491
495
  }>;
492
496
  requiresPutty: z.ZodDefault<z.ZodBoolean>;
493
497
  }, z.core.$strip>;
@@ -618,10 +622,10 @@ export declare const PricingDataSchema: z.ZodObject<{
618
622
  id: z.ZodString;
619
623
  wastePercent: z.ZodNumber;
620
624
  surface: z.ZodEnum<{
625
+ NONE: "NONE";
621
626
  FLOOR: "FLOOR";
622
627
  WALLS: "WALLS";
623
628
  CEILING: "CEILING";
624
- NONE: "NONE";
625
629
  }>;
626
630
  requiresPutty: z.ZodDefault<z.ZodBoolean>;
627
631
  }, z.core.$strip>>;
@@ -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
  };
@@ -708,11 +762,6 @@ function applySurfacePrep(data, room, lines, objectScope) {
708
762
  ...roomVariables(room),
709
763
  ...objectScope
710
764
  };
711
- const areas = {
712
- FLOOR: variables.floorArea,
713
- CEILING: variables.floorArea,
714
- WALLS: variables.wallArea
715
- };
716
765
  const surfaces = /* @__PURE__ */ new Map();
717
766
  lines.forEach((line, index) => {
718
767
  const category = data.categories.find((item) => item.id === line.categoryId);
@@ -724,11 +773,14 @@ function applySurfacePrep(data, room, lines, objectScope) {
724
773
  for (const [surface, indexes] of surfaces) {
725
774
  const rule = surfaceRuleOf(data, surface);
726
775
  if (rule === void 0) continue;
776
+ const surfaceArea = surfaceAreaOf(variables, surface);
777
+ const puttyShare = puttyShareOn(room, surface, data.categories);
727
778
  const outcome = runNumericFormula(rule.costFormula, {
728
779
  variables: {
729
780
  ...variables,
730
- surfaceArea: areas[surface],
731
- puttyRequired: puttyRequiredOn(room, surface, data.categories)
781
+ surfaceArea,
782
+ puttyRequired: puttyShare > 0,
783
+ puttyArea: surfaceArea * puttyShare
732
784
  },
733
785
  coefficients: data.coefficients
734
786
  });
@@ -761,7 +813,9 @@ function selectRule(data, categoryId, materialTypeId) {
761
813
  const rules = data.rules.filter(isMaterialRule);
762
814
  return rules.find((rule) => rule.level === "MATERIAL_TYPE" && rule.categoryId === categoryId && rule.materialTypeId === materialTypeId) ?? rules.find((rule) => rule.level === "CATEGORY" && rule.categoryId === categoryId);
763
815
  }
764
- 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;
765
819
  const init = {
766
820
  id: lineId("MATERIALS", room.clientId, categoryId),
767
821
  section: "MATERIALS",
@@ -769,6 +823,8 @@ function materialLine(configuration, data, room, categoryId, productId, objectSc
769
823
  categoryId,
770
824
  productId
771
825
  };
826
+ const shareError = areaShareError(room, selection, data.categories);
827
+ if (shareError !== null) return failedLine(init, "ERROR", shareError);
772
828
  const category = data.categories.find((item) => item.id === categoryId);
773
829
  const product = data.products.find((item) => item.id === productId && item.categoryId === categoryId);
774
830
  if (product === void 0 || category === void 0 || !product.available) return failedLine(init, "UNAVAILABLE", {
@@ -790,8 +846,12 @@ function materialLine(configuration, data, room, categoryId, productId, objectSc
790
846
  materialTypeId: product.materialTypeId
791
847
  }
792
848
  });
849
+ const base = roomVariables(room);
850
+ const share = areaShareOf(selection);
793
851
  const variables = {
794
- ...roomVariables(room),
852
+ ...base,
853
+ wallArea: category.surface === "WALLS" ? base.wallArea * share : base.wallArea,
854
+ floorArea: category.surface === "FLOOR" ? base.floorArea * share : base.floorArea,
795
855
  ...productVariables(product, category),
796
856
  ...objectScope
797
857
  };
@@ -861,7 +921,7 @@ function materialLine(configuration, data, room, categoryId, productId, objectSc
861
921
  }
862
922
  function materialLines(configuration, data, objectScope) {
863
923
  return configuration.rooms.flatMap((room) => {
864
- 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)]);
865
925
  return configuration.scope === "PARTIAL" ? applySurfacePrep(data, room, lines, objectScope) : lines;
866
926
  });
867
927
  }
@@ -959,7 +1019,8 @@ const RangeSchema = z.object({
959
1019
  //#region src/schemas/configuration.ts
960
1020
  const RoomSelectionSchema = z.object({
961
1021
  categoryId: z.string(),
962
- productId: z.string().nullable()
1022
+ productId: z.string().nullable(),
1023
+ areaShare: z.int().min(1).max(100).optional()
963
1024
  });
964
1025
  const RoomConfigurationSchema = z.object({
965
1026
  clientId: z.string(),
@@ -1199,6 +1260,7 @@ const RuleSampleSchema = z.object({
1199
1260
  }).optional(),
1200
1261
  surfaceArea: z.number().min(0).optional(),
1201
1262
  puttyRequired: z.boolean().optional(),
1263
+ puttyArea: z.number().min(0).optional(),
1202
1264
  object: z.object({
1203
1265
  scope: ScopeSchema,
1204
1266
  totalAreaSqm: z.number().positive().nullable(),
@@ -1277,10 +1339,32 @@ function roomsAreaWarnings(configuration, limits) {
1277
1339
  params: { missingSqm }
1278
1340
  }] : [];
1279
1341
  }
1280
- 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) {
1281
1364
  return [
1282
1365
  ...collectWarnings(lines),
1283
- ...roomsAreaWarnings(configuration, limits),
1366
+ ...roomsAreaWarnings(configuration, data.limits),
1367
+ ...areaShareWarnings(configuration, data.categories),
1284
1368
  ...configuration.currency === "UAH" && rate === null ? [{ code: "EXCHANGE_RATE_UNAVAILABLE" }] : []
1285
1369
  ];
1286
1370
  }
@@ -1308,7 +1392,7 @@ function estimate(configuration, data) {
1308
1392
  exchangeRate: data.exchangeRate,
1309
1393
  lines,
1310
1394
  totals: computeTotals(lines, rate !== null),
1311
- warnings: estimateWarnings(lines, configuration, rate, data.limits),
1395
+ warnings: estimateWarnings(lines, configuration, rate, data),
1312
1396
  incomplete: isIncomplete(lines)
1313
1397
  };
1314
1398
  }
@@ -1403,11 +1487,13 @@ function scopeOf(level, sample) {
1403
1487
  code: "SAMPLE_INCOMPLETE",
1404
1488
  params: { missing: room === void 0 ? "room" : "surfaceArea" }
1405
1489
  } };
1490
+ const puttyRequired = sample.puttyRequired ?? true;
1406
1491
  return { scope: {
1407
1492
  ...roomVariables(room),
1408
1493
  ...objectScope(sample),
1409
1494
  surfaceArea,
1410
- puttyRequired: sample.puttyRequired ?? true
1495
+ puttyRequired,
1496
+ puttyArea: sample.puttyArea ?? (puttyRequired ? surfaceArea : 0)
1411
1497
  } };
1412
1498
  }
1413
1499
  if (room === void 0 || product === void 0) return { error: {
@@ -1556,6 +1642,7 @@ const FORMULA_SCOPE_VARIABLES = {
1556
1642
  };
1557
1643
  const FORMULA_QUANTITY_VARIABLE = number("quantity");
1558
1644
  const FORMULA_SURFACE_AREA_VARIABLE = number("surfaceArea");
1645
+ const FORMULA_PUTTY_AREA_VARIABLE = number("puttyArea");
1559
1646
  const FORMULA_PUTTY_REQUIRED_VARIABLE = boolean("puttyRequired");
1560
1647
  const FORMULA_VARIABLES = {
1561
1648
  OBJECT: FORMULA_SCOPE_VARIABLES.object,
@@ -1573,7 +1660,8 @@ const FORMULA_VARIABLES = {
1573
1660
  ...FORMULA_SCOPE_VARIABLES.room,
1574
1661
  ...FORMULA_SCOPE_VARIABLES.object,
1575
1662
  FORMULA_SURFACE_AREA_VARIABLE,
1576
- FORMULA_PUTTY_REQUIRED_VARIABLE
1663
+ FORMULA_PUTTY_REQUIRED_VARIABLE,
1664
+ FORMULA_PUTTY_AREA_VARIABLE
1577
1665
  ]
1578
1666
  };
1579
1667
  const ALL_VARIABLE_NAMES = new Set([...FORMULA_VARIABLES.CATEGORY, ...FORMULA_VARIABLES.SURFACE].map((variable) => variable.name));
@@ -1788,4 +1876,4 @@ function validateFormula(input) {
1788
1876
  };
1789
1877
  }
1790
1878
  //#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 };
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.6.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",