@roomwise/calc-engine 0.8.0 → 0.9.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
@@ -203,11 +203,12 @@ The base cost has no per-area rate table. It is built from rules whose formulas
203
203
 
204
204
  A material line is the sum of its components:
205
205
 
206
- - `MATERIAL`: the rule's `costFormula`.
206
+ - `MATERIAL`: the rule's `costFormula`. When the pricing data lists accompanying materials in `accessories`, this is the product itself and the accessories are separate components.
207
+ - `ACCESSORY`: an accompanying material such as underlay or tile glue, with the `coefficientKey` it comes from. Only accessory coefficients referenced by the formula count. The formula is evaluated with all of them set to zero for the `MATERIAL` part and once per accessory with only that coefficient restored; when the parts do not add up to the full cost, as in `(price + coef.a) * (1 + coef.b)`, the line is not split. Parts are rounded to cents and the rounding remainder goes to the last `ACCESSORY`; an accessory left at zero is dropped. A formula that uses `heatedFloor ? coef.a : coef.b` yields only the branch in effect. If any evaluation fails, the parts do not add up or a part would be negative, the line keeps a single `MATERIAL` component. Surface, object and option lines are not split.
207
208
  - `LABOR`: the rule's `laborFormula`, which only `CATEGORY` and `MATERIAL_TYPE` rules can have. It is evaluated in `PARTIAL` only. Without a formula the component is 0 and the line gets `LABOR_RATE_MISSING`.
208
209
  - `PREP`: surface preparation, from a `SURFACE` rule.
209
210
 
210
- Every component is rounded to whole cents on its own and `amountCents` is the sum of the rounded components. A `FULL` line has a single `MATERIAL` component. Each component carries the `ruleId` of the rule that produced it; ids are plain strings.
211
+ Every component is rounded to whole cents on its own and `amountCents` is the sum of the rounded components. A `FULL` line has a `MATERIAL` component and its `ACCESSORY` components, if any. Each component carries the `ruleId` of the rule that produced it; ids are plain strings.
211
212
 
212
213
  ### Surface preparation
213
214
 
@@ -256,7 +257,7 @@ Availability by rule level:
256
257
  - `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`.
257
258
  - `SURFACE`: room and object variables plus `surfaceArea` and `puttyRequired`. `quantity` and the product variables are not available.
258
259
 
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.
260
+ Coefficients are read as `coef.<key>`; keys come from the pricing data. `PricingData.accessories` is an optional map from coefficient key to a `{ en, uk }` name; the keys that exist in `coefficients` are the accompanying materials shown as `ACCESSORY` components, and the name is looked up by `coefficientKey`. The cost and labor formulas receive `quantity` unrounded, while the line shows it rounded to 3 decimals.
260
261
 
261
262
  ### Syntax
262
263
 
package/dist/index.cjs CHANGED
@@ -363,121 +363,6 @@ const FORMULA_FUNCTION_TABLE = new Map([
363
363
  ].map((fn) => [fn.name, fn]));
364
364
  const FORMULA_FUNCTIONS = [...FORMULA_FUNCTION_TABLE.keys()];
365
365
  //#endregion
366
- //#region src/formula/evaluate.ts
367
- var EvaluationFailure = class {
368
- error;
369
- constructor(error) {
370
- this.error = error;
371
- }
372
- };
373
- function fail(node, code, params) {
374
- throw new EvaluationFailure({
375
- code,
376
- position: node.start,
377
- length: node.end - node.start,
378
- ...params === void 0 ? {} : { params }
379
- });
380
- }
381
- function runtimeFail(node, reason, params) {
382
- return fail(node, "FORMULA_RUNTIME_ERROR", {
383
- ...params,
384
- reason
385
- });
386
- }
387
- function mismatch(node, expected) {
388
- return runtimeFail(node, "TYPE_MISMATCH", { expected });
389
- }
390
- function finite(node, value) {
391
- return Number.isFinite(value) ? value : runtimeFail(node, "NOT_FINITE");
392
- }
393
- function asNumber(node, value) {
394
- return typeof value === "number" ? value : mismatch(node, "number");
395
- }
396
- function asBoolean(node, value) {
397
- return typeof value === "boolean" ? value : mismatch(node, "boolean");
398
- }
399
- function lookupVariable(node, scope) {
400
- if (!Object.hasOwn(scope.variables, node.name)) return fail(node, "UNKNOWN_VARIABLE", { name: node.name });
401
- return scope.variables[node.name];
402
- }
403
- function evaluateMember(node, scope) {
404
- if (node.object.type !== "Identifier" || node.object.name !== "coef") return fail(node, "UNKNOWN_VARIABLE");
405
- const key = node.property.name;
406
- if (!Object.hasOwn(scope.coefficients, key)) return fail(node, "UNKNOWN_COEFFICIENT", { key });
407
- return scope.coefficients[key];
408
- }
409
- function evaluateCall(node, scope) {
410
- const spec = node.callee.type === "Identifier" ? FORMULA_FUNCTION_TABLE.get(node.callee.name) : void 0;
411
- if (spec === void 0) return fail(node.callee, "UNKNOWN_FUNCTION");
412
- if (node.args.length < spec.minArgs || node.args.length > spec.maxArgs) return mismatch(node, "argument count");
413
- const args = node.args.map((arg) => asNumber(arg, walk(arg, scope)));
414
- return finite(node, spec.apply(args));
415
- }
416
- function evaluateEquality(node, scope) {
417
- const { left, right } = node;
418
- const literal = [left, right].find((side) => side.type === "Literal" && typeof side.value === "string") ?? null;
419
- if (literal !== null) {
420
- const other = literal === left ? right : left;
421
- const variable = other.type === "Identifier" ? lookupVariable(other, scope) : void 0;
422
- if (typeof variable !== "string") return mismatch(other, "enum variable");
423
- return node.operator === "==" ? variable === literal.value : variable !== literal.value;
424
- }
425
- const leftValue = walk(left, scope);
426
- const rightValue = walk(right, scope);
427
- if (typeof leftValue !== typeof rightValue) return mismatch(right, typeof leftValue);
428
- return node.operator === "==" ? leftValue === rightValue : leftValue !== rightValue;
429
- }
430
- function evaluateBinary(node, scope) {
431
- const { operator, left, right } = node;
432
- if (operator === "==" || operator === "!=") return evaluateEquality(node, scope);
433
- if (operator === "&&" || operator === "||") {
434
- const leftValue = asBoolean(left, walk(left, scope));
435
- if (operator === "&&" ? !leftValue : leftValue) return leftValue;
436
- return asBoolean(right, walk(right, scope));
437
- }
438
- const a = asNumber(left, walk(left, scope));
439
- const b = asNumber(right, walk(right, scope));
440
- switch (operator) {
441
- case "<": return a < b;
442
- case ">": return a > b;
443
- case "<=": return a <= b;
444
- case ">=": return a >= b;
445
- case "+": return finite(node, a + b);
446
- case "-": return finite(node, a - b);
447
- case "*": return finite(node, a * b);
448
- case "/": return b === 0 ? runtimeFail(node, "DIVISION_BY_ZERO") : finite(node, a / b);
449
- }
450
- }
451
- function walk(node, scope) {
452
- switch (node.type) {
453
- case "Literal": return typeof node.value === "string" ? mismatch(node, "number or boolean") : node.value;
454
- case "Identifier": return lookupVariable(node, scope);
455
- case "Member": return evaluateMember(node, scope);
456
- case "Call": return evaluateCall(node, scope);
457
- case "Unary": {
458
- const argument = walk(node.argument, scope);
459
- return node.operator === "!" ? !asBoolean(node.argument, argument) : finite(node, -asNumber(node.argument, argument));
460
- }
461
- case "Binary": return evaluateBinary(node, scope);
462
- case "Conditional": return asBoolean(node.test, walk(node.test, scope)) ? walk(node.consequent, scope) : walk(node.alternate, scope);
463
- default: return runtimeFail(node, "TYPE_MISMATCH", { expected: "known node" });
464
- }
465
- }
466
- function evaluate(ast, scope) {
467
- try {
468
- return {
469
- ok: true,
470
- value: walk(ast, scope)
471
- };
472
- } catch (error) {
473
- if (error instanceof EvaluationFailure) return {
474
- ok: false,
475
- error: error.error
476
- };
477
- throw error;
478
- }
479
- }
480
- //#endregion
481
366
  //#region src/formula/grammar.ts
482
367
  const SEMICOLON_CODE = 59;
483
368
  const REMOVED_BINARY_OPERATORS = [
@@ -726,6 +611,121 @@ function parse(source) {
726
611
  }
727
612
  }
728
613
  //#endregion
614
+ //#region src/formula/evaluate.ts
615
+ var EvaluationFailure = class {
616
+ error;
617
+ constructor(error) {
618
+ this.error = error;
619
+ }
620
+ };
621
+ function fail(node, code, params) {
622
+ throw new EvaluationFailure({
623
+ code,
624
+ position: node.start,
625
+ length: node.end - node.start,
626
+ ...params === void 0 ? {} : { params }
627
+ });
628
+ }
629
+ function runtimeFail(node, reason, params) {
630
+ return fail(node, "FORMULA_RUNTIME_ERROR", {
631
+ ...params,
632
+ reason
633
+ });
634
+ }
635
+ function mismatch(node, expected) {
636
+ return runtimeFail(node, "TYPE_MISMATCH", { expected });
637
+ }
638
+ function finite(node, value) {
639
+ return Number.isFinite(value) ? value : runtimeFail(node, "NOT_FINITE");
640
+ }
641
+ function asNumber(node, value) {
642
+ return typeof value === "number" ? value : mismatch(node, "number");
643
+ }
644
+ function asBoolean(node, value) {
645
+ return typeof value === "boolean" ? value : mismatch(node, "boolean");
646
+ }
647
+ function lookupVariable(node, scope) {
648
+ if (!Object.hasOwn(scope.variables, node.name)) return fail(node, "UNKNOWN_VARIABLE", { name: node.name });
649
+ return scope.variables[node.name];
650
+ }
651
+ function evaluateMember(node, scope) {
652
+ if (node.object.type !== "Identifier" || node.object.name !== "coef") return fail(node, "UNKNOWN_VARIABLE");
653
+ const key = node.property.name;
654
+ if (!Object.hasOwn(scope.coefficients, key)) return fail(node, "UNKNOWN_COEFFICIENT", { key });
655
+ return scope.coefficients[key];
656
+ }
657
+ function evaluateCall(node, scope) {
658
+ const spec = node.callee.type === "Identifier" ? FORMULA_FUNCTION_TABLE.get(node.callee.name) : void 0;
659
+ if (spec === void 0) return fail(node.callee, "UNKNOWN_FUNCTION");
660
+ if (node.args.length < spec.minArgs || node.args.length > spec.maxArgs) return mismatch(node, "argument count");
661
+ const args = node.args.map((arg) => asNumber(arg, walk(arg, scope)));
662
+ return finite(node, spec.apply(args));
663
+ }
664
+ function evaluateEquality(node, scope) {
665
+ const { left, right } = node;
666
+ const literal = [left, right].find((side) => side.type === "Literal" && typeof side.value === "string") ?? null;
667
+ if (literal !== null) {
668
+ const other = literal === left ? right : left;
669
+ const variable = other.type === "Identifier" ? lookupVariable(other, scope) : void 0;
670
+ if (typeof variable !== "string") return mismatch(other, "enum variable");
671
+ return node.operator === "==" ? variable === literal.value : variable !== literal.value;
672
+ }
673
+ const leftValue = walk(left, scope);
674
+ const rightValue = walk(right, scope);
675
+ if (typeof leftValue !== typeof rightValue) return mismatch(right, typeof leftValue);
676
+ return node.operator === "==" ? leftValue === rightValue : leftValue !== rightValue;
677
+ }
678
+ function evaluateBinary(node, scope) {
679
+ const { operator, left, right } = node;
680
+ if (operator === "==" || operator === "!=") return evaluateEquality(node, scope);
681
+ if (operator === "&&" || operator === "||") {
682
+ const leftValue = asBoolean(left, walk(left, scope));
683
+ if (operator === "&&" ? !leftValue : leftValue) return leftValue;
684
+ return asBoolean(right, walk(right, scope));
685
+ }
686
+ const a = asNumber(left, walk(left, scope));
687
+ const b = asNumber(right, walk(right, scope));
688
+ switch (operator) {
689
+ case "<": return a < b;
690
+ case ">": return a > b;
691
+ case "<=": return a <= b;
692
+ case ">=": return a >= b;
693
+ case "+": return finite(node, a + b);
694
+ case "-": return finite(node, a - b);
695
+ case "*": return finite(node, a * b);
696
+ case "/": return b === 0 ? runtimeFail(node, "DIVISION_BY_ZERO") : finite(node, a / b);
697
+ }
698
+ }
699
+ function walk(node, scope) {
700
+ switch (node.type) {
701
+ case "Literal": return typeof node.value === "string" ? mismatch(node, "number or boolean") : node.value;
702
+ case "Identifier": return lookupVariable(node, scope);
703
+ case "Member": return evaluateMember(node, scope);
704
+ case "Call": return evaluateCall(node, scope);
705
+ case "Unary": {
706
+ const argument = walk(node.argument, scope);
707
+ return node.operator === "!" ? !asBoolean(node.argument, argument) : finite(node, -asNumber(node.argument, argument));
708
+ }
709
+ case "Binary": return evaluateBinary(node, scope);
710
+ case "Conditional": return asBoolean(node.test, walk(node.test, scope)) ? walk(node.consequent, scope) : walk(node.alternate, scope);
711
+ default: return runtimeFail(node, "TYPE_MISMATCH", { expected: "known node" });
712
+ }
713
+ }
714
+ function evaluate(ast, scope) {
715
+ try {
716
+ return {
717
+ ok: true,
718
+ value: walk(ast, scope)
719
+ };
720
+ } catch (error) {
721
+ if (error instanceof EvaluationFailure) return {
722
+ ok: false,
723
+ error: error.error
724
+ };
725
+ throw error;
726
+ }
727
+ }
728
+ //#endregion
729
729
  //#region src/estimate/run-formula.ts
730
730
  function message(code, params) {
731
731
  return params === void 0 ? { code } : {
@@ -835,11 +835,108 @@ function applySurfacePrep(data, room, lines, objectScope) {
835
835
  }
836
836
  //#endregion
837
837
  //#region src/estimate/materials.ts
838
+ const ADDITIVITY_TOLERANCE = 1e-9;
838
839
  const isMaterialRule = (rule) => rule.quantityFormula !== null;
839
840
  function selectRule(data, categoryId, materialTypeId) {
840
841
  const rules = data.rules.filter(isMaterialRule);
841
842
  return rules.find((rule) => rule.level === "MATERIAL_TYPE" && rule.categoryId === categoryId && rule.materialTypeId === materialTypeId) ?? rules.find((rule) => rule.level === "CATEGORY" && rule.categoryId === categoryId);
842
843
  }
844
+ function referencedCoefficients(source) {
845
+ const keys = /* @__PURE__ */ new Set();
846
+ const parsed = parse(source);
847
+ if (!parsed.ok) return keys;
848
+ const visit = (node) => {
849
+ switch (node.type) {
850
+ case "Member":
851
+ if (node.object.type === "Identifier" && node.object.name === "coef") keys.add(node.property.name);
852
+ visit(node.object);
853
+ return;
854
+ case "Call":
855
+ visit(node.callee);
856
+ node.args.forEach(visit);
857
+ return;
858
+ case "Unary":
859
+ visit(node.argument);
860
+ return;
861
+ case "Binary":
862
+ visit(node.left);
863
+ visit(node.right);
864
+ return;
865
+ case "Conditional":
866
+ visit(node.test);
867
+ visit(node.consequent);
868
+ visit(node.alternate);
869
+ return;
870
+ default: return;
871
+ }
872
+ };
873
+ visit(parsed.ast);
874
+ return keys;
875
+ }
876
+ function splitAccessories(rule, data, variables, fullValue, fullCents) {
877
+ const whole = [{
878
+ kind: "MATERIAL",
879
+ ruleId: rule.id,
880
+ amountCents: fullCents
881
+ }];
882
+ const accessories = data.accessories ?? {};
883
+ const keys = [...referencedCoefficients(rule.costFormula)].filter((key) => Object.hasOwn(accessories, key) && Object.hasOwn(data.coefficients, key));
884
+ if (keys.length === 0) return whole;
885
+ const zeroed = Object.fromEntries(keys.map((key) => [key, 0]));
886
+ const costValue = (overrides) => {
887
+ const outcome = runNumericFormula(rule.costFormula, {
888
+ variables,
889
+ coefficients: {
890
+ ...data.coefficients,
891
+ ...overrides
892
+ }
893
+ });
894
+ return outcome.ok ? outcome.value : null;
895
+ };
896
+ const productValue = costValue(zeroed);
897
+ if (productValue === null) return whole;
898
+ const productCents = componentCents(productValue);
899
+ if (productCents === null) return whole;
900
+ const parts = [];
901
+ for (const key of keys) {
902
+ const withKey = costValue({
903
+ ...zeroed,
904
+ [key]: data.coefficients[key]
905
+ });
906
+ if (withKey === null || withKey < productValue) return whole;
907
+ parts.push({
908
+ key,
909
+ value: withKey - productValue
910
+ });
911
+ }
912
+ const partsValue = parts.reduce((sum, part) => sum + part.value, 0);
913
+ if (Math.abs(productValue + partsValue - fullValue) > ADDITIVITY_TOLERANCE * Math.max(1, Math.abs(fullValue))) return whole;
914
+ const priced = [];
915
+ for (const part of parts) {
916
+ const cents = componentCents(part.value);
917
+ if (cents === null) return whole;
918
+ priced.push({
919
+ key: part.key,
920
+ cents
921
+ });
922
+ }
923
+ const remainder = fullCents - productCents - priced.reduce((sum, part) => sum + part.cents, 0);
924
+ const last = priced[priced.length - 1];
925
+ last.cents += remainder;
926
+ if (last.cents < 0) return whole;
927
+ const kept = priced.filter((part) => part.cents > 0);
928
+ if (kept.length === 0) return whole;
929
+ return [{
930
+ kind: "MATERIAL",
931
+ ruleId: rule.id,
932
+ amountCents: productCents
933
+ }, ...kept.map((part) => ({
934
+ kind: "ACCESSORY",
935
+ ruleId: rule.id,
936
+ coefficientKey: part.key,
937
+ amountCents: part.cents
938
+ }))];
939
+ }
843
940
  function materialLine(configuration, data, room, selection, objectScope) {
844
941
  const { categoryId } = selection;
845
942
  const productId = selection.productId;
@@ -903,11 +1000,10 @@ function materialLine(configuration, data, room, selection, objectScope) {
903
1000
  if (!cost.ok) return failedLine(ruled, "ERROR", cost.error);
904
1001
  const materialCents = componentCents(cost.value);
905
1002
  if (materialCents === null) return failedLine(ruled, "ERROR", outOfRange());
906
- const components = [{
907
- kind: "MATERIAL",
908
- ruleId: rule.id,
909
- amountCents: materialCents
910
- }];
1003
+ const components = splitAccessories(rule, data, {
1004
+ ...variables,
1005
+ quantity: quantity.value
1006
+ }, cost.value, materialCents);
911
1007
  const lineWarnings = [...warnings];
912
1008
  if (configuration.scope === "PARTIAL") {
913
1009
  let laborCents = 0;
@@ -1027,6 +1123,7 @@ const SectionSchema = zod.z.enum([
1027
1123
  ]);
1028
1124
  const LineComponentKindSchema = zod.z.enum([
1029
1125
  "MATERIAL",
1126
+ "ACCESSORY",
1030
1127
  "LABOR",
1031
1128
  "PREP"
1032
1129
  ]);
@@ -1212,6 +1309,7 @@ const PricingDataSchema = zod.z.object({
1212
1309
  exchangeRate: ExchangeRateSchema.nullable(),
1213
1310
  limits: PricingLimitsSchema,
1214
1311
  coefficients: zod.z.record(zod.z.string(), zod.z.number()),
1312
+ accessories: zod.z.record(zod.z.string(), LocalizedTextSchema).optional(),
1215
1313
  rules: zod.z.array(PricingRuleSchema),
1216
1314
  roomTypes: zod.z.array(PricingRoomTypeSchema),
1217
1315
  categories: zod.z.array(PricingCategorySchema),
@@ -1230,7 +1328,11 @@ const LineComponentSchema = zod.z.object({
1230
1328
  kind: LineComponentKindSchema,
1231
1329
  ruleId: zod.z.string(),
1232
1330
  amountCents: zod.z.number().int().min(0),
1233
- sharedWithCategoryId: zod.z.string().optional()
1331
+ sharedWithCategoryId: zod.z.string().optional(),
1332
+ coefficientKey: zod.z.string().optional()
1333
+ }).refine((component) => component.kind === "ACCESSORY" === (component.coefficientKey !== void 0), {
1334
+ path: ["coefficientKey"],
1335
+ message: "coefficientKey is required for ACCESSORY and only for it"
1234
1336
  });
1235
1337
  const EstimateLineSchema = zod.z.object({
1236
1338
  id: zod.z.string(),
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<{
@@ -82,6 +82,7 @@ export declare const SectionSchema: z.ZodEnum<{
82
82
  export type Section = z.infer<typeof SectionSchema>;
83
83
  export declare const LineComponentKindSchema: z.ZodEnum<{
84
84
  MATERIAL: "MATERIAL";
85
+ ACCESSORY: "ACCESSORY";
85
86
  LABOR: "LABOR";
86
87
  PREP: "PREP";
87
88
  }>;
@@ -194,12 +195,14 @@ export type EstimateMessage = z.infer<typeof EstimateMessageSchema>;
194
195
  export declare const LineComponentSchema: z.ZodObject<{
195
196
  kind: z.ZodEnum<{
196
197
  MATERIAL: "MATERIAL";
198
+ ACCESSORY: "ACCESSORY";
197
199
  LABOR: "LABOR";
198
200
  PREP: "PREP";
199
201
  }>;
200
202
  ruleId: z.ZodString;
201
203
  amountCents: z.ZodNumber;
202
204
  sharedWithCategoryId: z.ZodOptional<z.ZodString>;
205
+ coefficientKey: z.ZodOptional<z.ZodString>;
203
206
  }, z.core.$strip>;
204
207
  export type LineComponent = z.infer<typeof LineComponentSchema>;
205
208
  export declare const EstimateLineSchema: z.ZodObject<{
@@ -232,12 +235,14 @@ export declare const EstimateLineSchema: z.ZodObject<{
232
235
  components: z.ZodOptional<z.ZodArray<z.ZodObject<{
233
236
  kind: z.ZodEnum<{
234
237
  MATERIAL: "MATERIAL";
238
+ ACCESSORY: "ACCESSORY";
235
239
  LABOR: "LABOR";
236
240
  PREP: "PREP";
237
241
  }>;
238
242
  ruleId: z.ZodString;
239
243
  amountCents: z.ZodNumber;
240
244
  sharedWithCategoryId: z.ZodOptional<z.ZodString>;
245
+ coefficientKey: z.ZodOptional<z.ZodString>;
241
246
  }, z.core.$strip>>>;
242
247
  warnings: z.ZodArray<z.ZodObject<{
243
248
  code: z.ZodString;
@@ -319,12 +324,14 @@ export declare const EstimateSchema: z.ZodObject<{
319
324
  components: z.ZodOptional<z.ZodArray<z.ZodObject<{
320
325
  kind: z.ZodEnum<{
321
326
  MATERIAL: "MATERIAL";
327
+ ACCESSORY: "ACCESSORY";
322
328
  LABOR: "LABOR";
323
329
  PREP: "PREP";
324
330
  }>;
325
331
  ruleId: z.ZodString;
326
332
  amountCents: z.ZodNumber;
327
333
  sharedWithCategoryId: z.ZodOptional<z.ZodString>;
334
+ coefficientKey: z.ZodOptional<z.ZodString>;
328
335
  }, z.core.$strip>>>;
329
336
  warnings: z.ZodArray<z.ZodObject<{
330
337
  code: z.ZodString;
@@ -488,10 +495,10 @@ export declare const PricingCategorySchema: z.ZodObject<{
488
495
  id: z.ZodString;
489
496
  wastePercent: z.ZodNumber;
490
497
  surface: z.ZodEnum<{
498
+ NONE: "NONE";
491
499
  FLOOR: "FLOOR";
492
500
  WALLS: "WALLS";
493
501
  CEILING: "CEILING";
494
- NONE: "NONE";
495
502
  }>;
496
503
  requiresPutty: z.ZodDefault<z.ZodBoolean>;
497
504
  }, z.core.$strip>;
@@ -589,6 +596,10 @@ export declare const PricingDataSchema: z.ZodObject<{
589
596
  missingAreaHintSqm: z.ZodNumber;
590
597
  }, z.core.$strip>;
591
598
  coefficients: z.ZodRecord<z.ZodString, z.ZodNumber>;
599
+ accessories: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
600
+ en: z.ZodString;
601
+ uk: z.ZodString;
602
+ }, z.core.$strip>>>;
592
603
  rules: z.ZodArray<z.ZodObject<{
593
604
  id: z.ZodString;
594
605
  level: z.ZodEnum<{
@@ -622,10 +633,10 @@ export declare const PricingDataSchema: z.ZodObject<{
622
633
  id: z.ZodString;
623
634
  wastePercent: z.ZodNumber;
624
635
  surface: z.ZodEnum<{
636
+ NONE: "NONE";
625
637
  FLOOR: "FLOOR";
626
638
  WALLS: "WALLS";
627
639
  CEILING: "CEILING";
628
- NONE: "NONE";
629
640
  }>;
630
641
  requiresPutty: z.ZodDefault<z.ZodBoolean>;
631
642
  }, z.core.$strip>>;
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<{
@@ -82,6 +82,7 @@ export declare const SectionSchema: z.ZodEnum<{
82
82
  export type Section = z.infer<typeof SectionSchema>;
83
83
  export declare const LineComponentKindSchema: z.ZodEnum<{
84
84
  MATERIAL: "MATERIAL";
85
+ ACCESSORY: "ACCESSORY";
85
86
  LABOR: "LABOR";
86
87
  PREP: "PREP";
87
88
  }>;
@@ -194,12 +195,14 @@ export type EstimateMessage = z.infer<typeof EstimateMessageSchema>;
194
195
  export declare const LineComponentSchema: z.ZodObject<{
195
196
  kind: z.ZodEnum<{
196
197
  MATERIAL: "MATERIAL";
198
+ ACCESSORY: "ACCESSORY";
197
199
  LABOR: "LABOR";
198
200
  PREP: "PREP";
199
201
  }>;
200
202
  ruleId: z.ZodString;
201
203
  amountCents: z.ZodNumber;
202
204
  sharedWithCategoryId: z.ZodOptional<z.ZodString>;
205
+ coefficientKey: z.ZodOptional<z.ZodString>;
203
206
  }, z.core.$strip>;
204
207
  export type LineComponent = z.infer<typeof LineComponentSchema>;
205
208
  export declare const EstimateLineSchema: z.ZodObject<{
@@ -232,12 +235,14 @@ export declare const EstimateLineSchema: z.ZodObject<{
232
235
  components: z.ZodOptional<z.ZodArray<z.ZodObject<{
233
236
  kind: z.ZodEnum<{
234
237
  MATERIAL: "MATERIAL";
238
+ ACCESSORY: "ACCESSORY";
235
239
  LABOR: "LABOR";
236
240
  PREP: "PREP";
237
241
  }>;
238
242
  ruleId: z.ZodString;
239
243
  amountCents: z.ZodNumber;
240
244
  sharedWithCategoryId: z.ZodOptional<z.ZodString>;
245
+ coefficientKey: z.ZodOptional<z.ZodString>;
241
246
  }, z.core.$strip>>>;
242
247
  warnings: z.ZodArray<z.ZodObject<{
243
248
  code: z.ZodString;
@@ -319,12 +324,14 @@ export declare const EstimateSchema: z.ZodObject<{
319
324
  components: z.ZodOptional<z.ZodArray<z.ZodObject<{
320
325
  kind: z.ZodEnum<{
321
326
  MATERIAL: "MATERIAL";
327
+ ACCESSORY: "ACCESSORY";
322
328
  LABOR: "LABOR";
323
329
  PREP: "PREP";
324
330
  }>;
325
331
  ruleId: z.ZodString;
326
332
  amountCents: z.ZodNumber;
327
333
  sharedWithCategoryId: z.ZodOptional<z.ZodString>;
334
+ coefficientKey: z.ZodOptional<z.ZodString>;
328
335
  }, z.core.$strip>>>;
329
336
  warnings: z.ZodArray<z.ZodObject<{
330
337
  code: z.ZodString;
@@ -488,10 +495,10 @@ export declare const PricingCategorySchema: z.ZodObject<{
488
495
  id: z.ZodString;
489
496
  wastePercent: z.ZodNumber;
490
497
  surface: z.ZodEnum<{
498
+ NONE: "NONE";
491
499
  FLOOR: "FLOOR";
492
500
  WALLS: "WALLS";
493
501
  CEILING: "CEILING";
494
- NONE: "NONE";
495
502
  }>;
496
503
  requiresPutty: z.ZodDefault<z.ZodBoolean>;
497
504
  }, z.core.$strip>;
@@ -589,6 +596,10 @@ export declare const PricingDataSchema: z.ZodObject<{
589
596
  missingAreaHintSqm: z.ZodNumber;
590
597
  }, z.core.$strip>;
591
598
  coefficients: z.ZodRecord<z.ZodString, z.ZodNumber>;
599
+ accessories: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
600
+ en: z.ZodString;
601
+ uk: z.ZodString;
602
+ }, z.core.$strip>>>;
592
603
  rules: z.ZodArray<z.ZodObject<{
593
604
  id: z.ZodString;
594
605
  level: z.ZodEnum<{
@@ -622,10 +633,10 @@ export declare const PricingDataSchema: z.ZodObject<{
622
633
  id: z.ZodString;
623
634
  wastePercent: z.ZodNumber;
624
635
  surface: z.ZodEnum<{
636
+ NONE: "NONE";
625
637
  FLOOR: "FLOOR";
626
638
  WALLS: "WALLS";
627
639
  CEILING: "CEILING";
628
- NONE: "NONE";
629
640
  }>;
630
641
  requiresPutty: z.ZodDefault<z.ZodBoolean>;
631
642
  }, z.core.$strip>>;
package/dist/index.js CHANGED
@@ -338,121 +338,6 @@ const FORMULA_FUNCTION_TABLE = new Map([
338
338
  ].map((fn) => [fn.name, fn]));
339
339
  const FORMULA_FUNCTIONS = [...FORMULA_FUNCTION_TABLE.keys()];
340
340
  //#endregion
341
- //#region src/formula/evaluate.ts
342
- var EvaluationFailure = class {
343
- error;
344
- constructor(error) {
345
- this.error = error;
346
- }
347
- };
348
- function fail(node, code, params) {
349
- throw new EvaluationFailure({
350
- code,
351
- position: node.start,
352
- length: node.end - node.start,
353
- ...params === void 0 ? {} : { params }
354
- });
355
- }
356
- function runtimeFail(node, reason, params) {
357
- return fail(node, "FORMULA_RUNTIME_ERROR", {
358
- ...params,
359
- reason
360
- });
361
- }
362
- function mismatch(node, expected) {
363
- return runtimeFail(node, "TYPE_MISMATCH", { expected });
364
- }
365
- function finite(node, value) {
366
- return Number.isFinite(value) ? value : runtimeFail(node, "NOT_FINITE");
367
- }
368
- function asNumber(node, value) {
369
- return typeof value === "number" ? value : mismatch(node, "number");
370
- }
371
- function asBoolean(node, value) {
372
- return typeof value === "boolean" ? value : mismatch(node, "boolean");
373
- }
374
- function lookupVariable(node, scope) {
375
- if (!Object.hasOwn(scope.variables, node.name)) return fail(node, "UNKNOWN_VARIABLE", { name: node.name });
376
- return scope.variables[node.name];
377
- }
378
- function evaluateMember(node, scope) {
379
- if (node.object.type !== "Identifier" || node.object.name !== "coef") return fail(node, "UNKNOWN_VARIABLE");
380
- const key = node.property.name;
381
- if (!Object.hasOwn(scope.coefficients, key)) return fail(node, "UNKNOWN_COEFFICIENT", { key });
382
- return scope.coefficients[key];
383
- }
384
- function evaluateCall(node, scope) {
385
- const spec = node.callee.type === "Identifier" ? FORMULA_FUNCTION_TABLE.get(node.callee.name) : void 0;
386
- if (spec === void 0) return fail(node.callee, "UNKNOWN_FUNCTION");
387
- if (node.args.length < spec.minArgs || node.args.length > spec.maxArgs) return mismatch(node, "argument count");
388
- const args = node.args.map((arg) => asNumber(arg, walk(arg, scope)));
389
- return finite(node, spec.apply(args));
390
- }
391
- function evaluateEquality(node, scope) {
392
- const { left, right } = node;
393
- const literal = [left, right].find((side) => side.type === "Literal" && typeof side.value === "string") ?? null;
394
- if (literal !== null) {
395
- const other = literal === left ? right : left;
396
- const variable = other.type === "Identifier" ? lookupVariable(other, scope) : void 0;
397
- if (typeof variable !== "string") return mismatch(other, "enum variable");
398
- return node.operator === "==" ? variable === literal.value : variable !== literal.value;
399
- }
400
- const leftValue = walk(left, scope);
401
- const rightValue = walk(right, scope);
402
- if (typeof leftValue !== typeof rightValue) return mismatch(right, typeof leftValue);
403
- return node.operator === "==" ? leftValue === rightValue : leftValue !== rightValue;
404
- }
405
- function evaluateBinary(node, scope) {
406
- const { operator, left, right } = node;
407
- if (operator === "==" || operator === "!=") return evaluateEquality(node, scope);
408
- if (operator === "&&" || operator === "||") {
409
- const leftValue = asBoolean(left, walk(left, scope));
410
- if (operator === "&&" ? !leftValue : leftValue) return leftValue;
411
- return asBoolean(right, walk(right, scope));
412
- }
413
- const a = asNumber(left, walk(left, scope));
414
- const b = asNumber(right, walk(right, scope));
415
- switch (operator) {
416
- case "<": return a < b;
417
- case ">": return a > b;
418
- case "<=": return a <= b;
419
- case ">=": return a >= b;
420
- case "+": return finite(node, a + b);
421
- case "-": return finite(node, a - b);
422
- case "*": return finite(node, a * b);
423
- case "/": return b === 0 ? runtimeFail(node, "DIVISION_BY_ZERO") : finite(node, a / b);
424
- }
425
- }
426
- function walk(node, scope) {
427
- switch (node.type) {
428
- case "Literal": return typeof node.value === "string" ? mismatch(node, "number or boolean") : node.value;
429
- case "Identifier": return lookupVariable(node, scope);
430
- case "Member": return evaluateMember(node, scope);
431
- case "Call": return evaluateCall(node, scope);
432
- case "Unary": {
433
- const argument = walk(node.argument, scope);
434
- return node.operator === "!" ? !asBoolean(node.argument, argument) : finite(node, -asNumber(node.argument, argument));
435
- }
436
- case "Binary": return evaluateBinary(node, scope);
437
- case "Conditional": return asBoolean(node.test, walk(node.test, scope)) ? walk(node.consequent, scope) : walk(node.alternate, scope);
438
- default: return runtimeFail(node, "TYPE_MISMATCH", { expected: "known node" });
439
- }
440
- }
441
- function evaluate(ast, scope) {
442
- try {
443
- return {
444
- ok: true,
445
- value: walk(ast, scope)
446
- };
447
- } catch (error) {
448
- if (error instanceof EvaluationFailure) return {
449
- ok: false,
450
- error: error.error
451
- };
452
- throw error;
453
- }
454
- }
455
- //#endregion
456
341
  //#region src/formula/grammar.ts
457
342
  const SEMICOLON_CODE = 59;
458
343
  const REMOVED_BINARY_OPERATORS = [
@@ -701,6 +586,121 @@ function parse(source) {
701
586
  }
702
587
  }
703
588
  //#endregion
589
+ //#region src/formula/evaluate.ts
590
+ var EvaluationFailure = class {
591
+ error;
592
+ constructor(error) {
593
+ this.error = error;
594
+ }
595
+ };
596
+ function fail(node, code, params) {
597
+ throw new EvaluationFailure({
598
+ code,
599
+ position: node.start,
600
+ length: node.end - node.start,
601
+ ...params === void 0 ? {} : { params }
602
+ });
603
+ }
604
+ function runtimeFail(node, reason, params) {
605
+ return fail(node, "FORMULA_RUNTIME_ERROR", {
606
+ ...params,
607
+ reason
608
+ });
609
+ }
610
+ function mismatch(node, expected) {
611
+ return runtimeFail(node, "TYPE_MISMATCH", { expected });
612
+ }
613
+ function finite(node, value) {
614
+ return Number.isFinite(value) ? value : runtimeFail(node, "NOT_FINITE");
615
+ }
616
+ function asNumber(node, value) {
617
+ return typeof value === "number" ? value : mismatch(node, "number");
618
+ }
619
+ function asBoolean(node, value) {
620
+ return typeof value === "boolean" ? value : mismatch(node, "boolean");
621
+ }
622
+ function lookupVariable(node, scope) {
623
+ if (!Object.hasOwn(scope.variables, node.name)) return fail(node, "UNKNOWN_VARIABLE", { name: node.name });
624
+ return scope.variables[node.name];
625
+ }
626
+ function evaluateMember(node, scope) {
627
+ if (node.object.type !== "Identifier" || node.object.name !== "coef") return fail(node, "UNKNOWN_VARIABLE");
628
+ const key = node.property.name;
629
+ if (!Object.hasOwn(scope.coefficients, key)) return fail(node, "UNKNOWN_COEFFICIENT", { key });
630
+ return scope.coefficients[key];
631
+ }
632
+ function evaluateCall(node, scope) {
633
+ const spec = node.callee.type === "Identifier" ? FORMULA_FUNCTION_TABLE.get(node.callee.name) : void 0;
634
+ if (spec === void 0) return fail(node.callee, "UNKNOWN_FUNCTION");
635
+ if (node.args.length < spec.minArgs || node.args.length > spec.maxArgs) return mismatch(node, "argument count");
636
+ const args = node.args.map((arg) => asNumber(arg, walk(arg, scope)));
637
+ return finite(node, spec.apply(args));
638
+ }
639
+ function evaluateEquality(node, scope) {
640
+ const { left, right } = node;
641
+ const literal = [left, right].find((side) => side.type === "Literal" && typeof side.value === "string") ?? null;
642
+ if (literal !== null) {
643
+ const other = literal === left ? right : left;
644
+ const variable = other.type === "Identifier" ? lookupVariable(other, scope) : void 0;
645
+ if (typeof variable !== "string") return mismatch(other, "enum variable");
646
+ return node.operator === "==" ? variable === literal.value : variable !== literal.value;
647
+ }
648
+ const leftValue = walk(left, scope);
649
+ const rightValue = walk(right, scope);
650
+ if (typeof leftValue !== typeof rightValue) return mismatch(right, typeof leftValue);
651
+ return node.operator === "==" ? leftValue === rightValue : leftValue !== rightValue;
652
+ }
653
+ function evaluateBinary(node, scope) {
654
+ const { operator, left, right } = node;
655
+ if (operator === "==" || operator === "!=") return evaluateEquality(node, scope);
656
+ if (operator === "&&" || operator === "||") {
657
+ const leftValue = asBoolean(left, walk(left, scope));
658
+ if (operator === "&&" ? !leftValue : leftValue) return leftValue;
659
+ return asBoolean(right, walk(right, scope));
660
+ }
661
+ const a = asNumber(left, walk(left, scope));
662
+ const b = asNumber(right, walk(right, scope));
663
+ switch (operator) {
664
+ case "<": return a < b;
665
+ case ">": return a > b;
666
+ case "<=": return a <= b;
667
+ case ">=": return a >= b;
668
+ case "+": return finite(node, a + b);
669
+ case "-": return finite(node, a - b);
670
+ case "*": return finite(node, a * b);
671
+ case "/": return b === 0 ? runtimeFail(node, "DIVISION_BY_ZERO") : finite(node, a / b);
672
+ }
673
+ }
674
+ function walk(node, scope) {
675
+ switch (node.type) {
676
+ case "Literal": return typeof node.value === "string" ? mismatch(node, "number or boolean") : node.value;
677
+ case "Identifier": return lookupVariable(node, scope);
678
+ case "Member": return evaluateMember(node, scope);
679
+ case "Call": return evaluateCall(node, scope);
680
+ case "Unary": {
681
+ const argument = walk(node.argument, scope);
682
+ return node.operator === "!" ? !asBoolean(node.argument, argument) : finite(node, -asNumber(node.argument, argument));
683
+ }
684
+ case "Binary": return evaluateBinary(node, scope);
685
+ case "Conditional": return asBoolean(node.test, walk(node.test, scope)) ? walk(node.consequent, scope) : walk(node.alternate, scope);
686
+ default: return runtimeFail(node, "TYPE_MISMATCH", { expected: "known node" });
687
+ }
688
+ }
689
+ function evaluate(ast, scope) {
690
+ try {
691
+ return {
692
+ ok: true,
693
+ value: walk(ast, scope)
694
+ };
695
+ } catch (error) {
696
+ if (error instanceof EvaluationFailure) return {
697
+ ok: false,
698
+ error: error.error
699
+ };
700
+ throw error;
701
+ }
702
+ }
703
+ //#endregion
704
704
  //#region src/estimate/run-formula.ts
705
705
  function message(code, params) {
706
706
  return params === void 0 ? { code } : {
@@ -810,11 +810,108 @@ function applySurfacePrep(data, room, lines, objectScope) {
810
810
  }
811
811
  //#endregion
812
812
  //#region src/estimate/materials.ts
813
+ const ADDITIVITY_TOLERANCE = 1e-9;
813
814
  const isMaterialRule = (rule) => rule.quantityFormula !== null;
814
815
  function selectRule(data, categoryId, materialTypeId) {
815
816
  const rules = data.rules.filter(isMaterialRule);
816
817
  return rules.find((rule) => rule.level === "MATERIAL_TYPE" && rule.categoryId === categoryId && rule.materialTypeId === materialTypeId) ?? rules.find((rule) => rule.level === "CATEGORY" && rule.categoryId === categoryId);
817
818
  }
819
+ function referencedCoefficients(source) {
820
+ const keys = /* @__PURE__ */ new Set();
821
+ const parsed = parse(source);
822
+ if (!parsed.ok) return keys;
823
+ const visit = (node) => {
824
+ switch (node.type) {
825
+ case "Member":
826
+ if (node.object.type === "Identifier" && node.object.name === "coef") keys.add(node.property.name);
827
+ visit(node.object);
828
+ return;
829
+ case "Call":
830
+ visit(node.callee);
831
+ node.args.forEach(visit);
832
+ return;
833
+ case "Unary":
834
+ visit(node.argument);
835
+ return;
836
+ case "Binary":
837
+ visit(node.left);
838
+ visit(node.right);
839
+ return;
840
+ case "Conditional":
841
+ visit(node.test);
842
+ visit(node.consequent);
843
+ visit(node.alternate);
844
+ return;
845
+ default: return;
846
+ }
847
+ };
848
+ visit(parsed.ast);
849
+ return keys;
850
+ }
851
+ function splitAccessories(rule, data, variables, fullValue, fullCents) {
852
+ const whole = [{
853
+ kind: "MATERIAL",
854
+ ruleId: rule.id,
855
+ amountCents: fullCents
856
+ }];
857
+ const accessories = data.accessories ?? {};
858
+ const keys = [...referencedCoefficients(rule.costFormula)].filter((key) => Object.hasOwn(accessories, key) && Object.hasOwn(data.coefficients, key));
859
+ if (keys.length === 0) return whole;
860
+ const zeroed = Object.fromEntries(keys.map((key) => [key, 0]));
861
+ const costValue = (overrides) => {
862
+ const outcome = runNumericFormula(rule.costFormula, {
863
+ variables,
864
+ coefficients: {
865
+ ...data.coefficients,
866
+ ...overrides
867
+ }
868
+ });
869
+ return outcome.ok ? outcome.value : null;
870
+ };
871
+ const productValue = costValue(zeroed);
872
+ if (productValue === null) return whole;
873
+ const productCents = componentCents(productValue);
874
+ if (productCents === null) return whole;
875
+ const parts = [];
876
+ for (const key of keys) {
877
+ const withKey = costValue({
878
+ ...zeroed,
879
+ [key]: data.coefficients[key]
880
+ });
881
+ if (withKey === null || withKey < productValue) return whole;
882
+ parts.push({
883
+ key,
884
+ value: withKey - productValue
885
+ });
886
+ }
887
+ const partsValue = parts.reduce((sum, part) => sum + part.value, 0);
888
+ if (Math.abs(productValue + partsValue - fullValue) > ADDITIVITY_TOLERANCE * Math.max(1, Math.abs(fullValue))) return whole;
889
+ const priced = [];
890
+ for (const part of parts) {
891
+ const cents = componentCents(part.value);
892
+ if (cents === null) return whole;
893
+ priced.push({
894
+ key: part.key,
895
+ cents
896
+ });
897
+ }
898
+ const remainder = fullCents - productCents - priced.reduce((sum, part) => sum + part.cents, 0);
899
+ const last = priced[priced.length - 1];
900
+ last.cents += remainder;
901
+ if (last.cents < 0) return whole;
902
+ const kept = priced.filter((part) => part.cents > 0);
903
+ if (kept.length === 0) return whole;
904
+ return [{
905
+ kind: "MATERIAL",
906
+ ruleId: rule.id,
907
+ amountCents: productCents
908
+ }, ...kept.map((part) => ({
909
+ kind: "ACCESSORY",
910
+ ruleId: rule.id,
911
+ coefficientKey: part.key,
912
+ amountCents: part.cents
913
+ }))];
914
+ }
818
915
  function materialLine(configuration, data, room, selection, objectScope) {
819
916
  const { categoryId } = selection;
820
917
  const productId = selection.productId;
@@ -878,11 +975,10 @@ function materialLine(configuration, data, room, selection, objectScope) {
878
975
  if (!cost.ok) return failedLine(ruled, "ERROR", cost.error);
879
976
  const materialCents = componentCents(cost.value);
880
977
  if (materialCents === null) return failedLine(ruled, "ERROR", outOfRange());
881
- const components = [{
882
- kind: "MATERIAL",
883
- ruleId: rule.id,
884
- amountCents: materialCents
885
- }];
978
+ const components = splitAccessories(rule, data, {
979
+ ...variables,
980
+ quantity: quantity.value
981
+ }, cost.value, materialCents);
886
982
  const lineWarnings = [...warnings];
887
983
  if (configuration.scope === "PARTIAL") {
888
984
  let laborCents = 0;
@@ -1002,6 +1098,7 @@ const SectionSchema = z.enum([
1002
1098
  ]);
1003
1099
  const LineComponentKindSchema = z.enum([
1004
1100
  "MATERIAL",
1101
+ "ACCESSORY",
1005
1102
  "LABOR",
1006
1103
  "PREP"
1007
1104
  ]);
@@ -1187,6 +1284,7 @@ const PricingDataSchema = z.object({
1187
1284
  exchangeRate: ExchangeRateSchema.nullable(),
1188
1285
  limits: PricingLimitsSchema,
1189
1286
  coefficients: z.record(z.string(), z.number()),
1287
+ accessories: z.record(z.string(), LocalizedTextSchema).optional(),
1190
1288
  rules: z.array(PricingRuleSchema),
1191
1289
  roomTypes: z.array(PricingRoomTypeSchema),
1192
1290
  categories: z.array(PricingCategorySchema),
@@ -1205,7 +1303,11 @@ const LineComponentSchema = z.object({
1205
1303
  kind: LineComponentKindSchema,
1206
1304
  ruleId: z.string(),
1207
1305
  amountCents: z.number().int().min(0),
1208
- sharedWithCategoryId: z.string().optional()
1306
+ sharedWithCategoryId: z.string().optional(),
1307
+ coefficientKey: z.string().optional()
1308
+ }).refine((component) => component.kind === "ACCESSORY" === (component.coefficientKey !== void 0), {
1309
+ path: ["coefficientKey"],
1310
+ message: "coefficientKey is required for ACCESSORY and only for it"
1209
1311
  });
1210
1312
  const EstimateLineSchema = z.object({
1211
1313
  id: z.string(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roomwise/calc-engine",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",