merchi_sdk_ts 1.27.0 → 1.28.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/entities/variation_field.js +4 -0
- package/dist/pricing/estimate.js +59 -0
- package/dist/pricing/estimate.test.js +29 -0
- package/package.json +1 -1
- package/src/entities/variation_field.ts +3 -0
- package/src/pricing/estimate.test.ts +41 -0
- package/src/pricing/estimate.ts +77 -0
- package/src/pricing/types.ts +10 -0
|
@@ -340,6 +340,10 @@ var VariationField = /** @class */ (function (_super) {
|
|
|
340
340
|
VariationField.property(),
|
|
341
341
|
__metadata("design:type", String)
|
|
342
342
|
], VariationField.prototype, "areaInputType", void 0);
|
|
343
|
+
__decorate([
|
|
344
|
+
VariationField.property({ type: Number }),
|
|
345
|
+
__metadata("design:type", Object)
|
|
346
|
+
], VariationField.prototype, "areaStep", void 0);
|
|
343
347
|
__decorate([
|
|
344
348
|
VariationField.property(),
|
|
345
349
|
__metadata("design:type", Boolean)
|
package/dist/pricing/estimate.js
CHANGED
|
@@ -12,12 +12,27 @@ var __values = (this && this.__values) || function(o) {
|
|
|
12
12
|
import { applyDiscount } from './discount.js';
|
|
13
13
|
import { roundHalfEven } from './round.js';
|
|
14
14
|
import { resolveVisibleFields } from './visibility.js';
|
|
15
|
+
import { FieldType } from '../constants/field_types.js';
|
|
15
16
|
/** Form inputs often supply quantities as strings. Coerce before any arithmetic
|
|
16
17
|
* so `reduce((a,b)=>a+b)` cannot concatenate ("100"+"1" → "01001" → 1001). */
|
|
17
18
|
function toQuantity(value) {
|
|
18
19
|
var n = Number(value);
|
|
19
20
|
return Number.isFinite(n) ? n : 0;
|
|
20
21
|
}
|
|
22
|
+
function parseAreaMm(value) {
|
|
23
|
+
if (value === undefined || value === null || value === '')
|
|
24
|
+
return null;
|
|
25
|
+
var parts = String(value).split(',').map(function (p) { return p.trim(); });
|
|
26
|
+
if (parts.length !== 2)
|
|
27
|
+
return null;
|
|
28
|
+
var heightMm = Number(parts[0]);
|
|
29
|
+
var widthMm = Number(parts[1]);
|
|
30
|
+
if (!Number.isFinite(heightMm) || !Number.isFinite(widthMm))
|
|
31
|
+
return null;
|
|
32
|
+
if (heightMm <= 0 || widthMm <= 0)
|
|
33
|
+
return null;
|
|
34
|
+
return { heightMm: heightMm, widthMm: widthMm };
|
|
35
|
+
}
|
|
21
36
|
function unitPriceAt(rules, qty) {
|
|
22
37
|
var unitPrice = applyDiscount(rules.product.unitPrice, qty, rules.product.discountGroup);
|
|
23
38
|
var mop = rules.product.minimumPrice;
|
|
@@ -65,9 +80,53 @@ function isEmpty(field, sel) {
|
|
|
65
80
|
sel.value === '' ||
|
|
66
81
|
sel.value === 0);
|
|
67
82
|
}
|
|
83
|
+
function areaCostFactor(field, sel, groupQuantities) {
|
|
84
|
+
var _a, _b, _c, _d, _e;
|
|
85
|
+
var n = groupQuantities.length;
|
|
86
|
+
var parsed = parseAreaMm((_a = sel === null || sel === void 0 ? void 0 : sel.value) !== null && _a !== void 0 ? _a : null);
|
|
87
|
+
if (!parsed) {
|
|
88
|
+
return { setup: 0, unitList: new Array(n).fill(0) };
|
|
89
|
+
}
|
|
90
|
+
var totalQty = groupQuantities.reduce(function (a, b) { return a + b; }, 0);
|
|
91
|
+
var heightSetup = variationSetupCost({
|
|
92
|
+
variationCost: field.heightVariationCost || 0,
|
|
93
|
+
variationCostDiscountGroup: (_b = field.heightVariationCostDiscountGroup) !== null && _b !== void 0 ? _b : null,
|
|
94
|
+
}, totalQty);
|
|
95
|
+
var widthSetup = variationSetupCost({
|
|
96
|
+
variationCost: field.widthVariationCost || 0,
|
|
97
|
+
variationCostDiscountGroup: (_c = field.widthVariationCostDiscountGroup) !== null && _c !== void 0 ? _c : null,
|
|
98
|
+
}, totalQty);
|
|
99
|
+
var heightUcs = variationUnitCosts({
|
|
100
|
+
variationUnitCost: field.heightVariationUnitCost || 0,
|
|
101
|
+
variationUnitCostDiscountGroup: (_d = field.heightVariationUnitCostDiscountGroup) !== null && _d !== void 0 ? _d : null,
|
|
102
|
+
}, groupQuantities);
|
|
103
|
+
var widthUcs = variationUnitCosts({
|
|
104
|
+
variationUnitCost: field.widthVariationUnitCost || 0,
|
|
105
|
+
variationUnitCostDiscountGroup: (_e = field.widthVariationUnitCostDiscountGroup) !== null && _e !== void 0 ? _e : null,
|
|
106
|
+
}, groupQuantities);
|
|
107
|
+
var areaUnit = (field.areaUnit || 'mm').toLowerCase();
|
|
108
|
+
var mmPerUnit = areaUnit === 'm' ? 1000
|
|
109
|
+
: areaUnit === 'cm' ? 10
|
|
110
|
+
: areaUnit === 'in' || areaUnit === 'inch' || areaUnit === 'inches' ? 25.4
|
|
111
|
+
: areaUnit === 'ft' || areaUnit === 'foot' || areaUnit === 'feet' ? 304.8
|
|
112
|
+
: 1;
|
|
113
|
+
var heightU = parsed.heightMm / mmPerUnit;
|
|
114
|
+
var widthU = parsed.widthMm / mmPerUnit;
|
|
115
|
+
var area = heightU * widthU;
|
|
116
|
+
// onceOff/unit = rateH × rateW × height_u × width_u (in field areaUnit)
|
|
117
|
+
var setup = roundHalfEven(heightSetup * widthSetup * area, 3);
|
|
118
|
+
var unitList = heightUcs.map(function (hu, i) {
|
|
119
|
+
return roundHalfEven(hu * widthUcs[i] * area, 3);
|
|
120
|
+
});
|
|
121
|
+
return { setup: setup, unitList: unitList };
|
|
122
|
+
}
|
|
68
123
|
function costFactor(field, sel, groupQuantities) {
|
|
69
124
|
var e_1, _a, e_2, _b;
|
|
70
125
|
var n = groupQuantities.length;
|
|
126
|
+
// Coerce: JSON/APIs occasionally deliver fieldType as a string.
|
|
127
|
+
if (Number(field.fieldType) === FieldType.AREA) {
|
|
128
|
+
return areaCostFactor(field, sel, groupQuantities);
|
|
129
|
+
}
|
|
71
130
|
if (isEmpty(field, sel)) {
|
|
72
131
|
return { setup: 0, unitList: new Array(n).fill(0) };
|
|
73
132
|
}
|
|
@@ -72,6 +72,35 @@ test('hidden conditional field is not costed', function () {
|
|
|
72
72
|
var r = estimateQuote(rules, { quantity: 10, fieldValues: { 2: { selectedOptionIds: [201] } } });
|
|
73
73
|
expect(r.cost).toBe(100);
|
|
74
74
|
});
|
|
75
|
+
test('area field multiplies height/width rate cards when dimensions set', function () {
|
|
76
|
+
var rules = __assign(__assign({}, base), { product: { unitPrice: 1, minimumPrice: null, discountGroup: null }, fields: [{
|
|
77
|
+
id: 14, originalId: 14, position: 0, fieldType: 14, independent: true,
|
|
78
|
+
isSelectable: false, selectedBy: [],
|
|
79
|
+
variationCost: 0, variationUnitCost: 0,
|
|
80
|
+
variationCostDiscountGroup: null, variationUnitCostDiscountGroup: null,
|
|
81
|
+
areaUnit: 'mm',
|
|
82
|
+
heightVariationCost: 2, widthVariationCost: 2,
|
|
83
|
+
heightVariationUnitCost: 3, widthVariationUnitCost: 4,
|
|
84
|
+
heightVariationCostDiscountGroup: null,
|
|
85
|
+
heightVariationUnitCostDiscountGroup: null,
|
|
86
|
+
widthVariationCostDiscountGroup: null,
|
|
87
|
+
widthVariationUnitCostDiscountGroup: null,
|
|
88
|
+
options: [],
|
|
89
|
+
}] });
|
|
90
|
+
var empty = estimateQuote(rules, {
|
|
91
|
+
quantity: 1, fieldValues: { 14: { value: '' } },
|
|
92
|
+
});
|
|
93
|
+
expect(empty.cost).toBe(1);
|
|
94
|
+
var priced = estimateQuote(rules, {
|
|
95
|
+
quantity: 1, fieldValues: { 14: { value: '2,5' } },
|
|
96
|
+
});
|
|
97
|
+
// area 2*5=10; onceOff 2*2*10=40; unit 3*4*10=120 → cost = 1 + 40 + 120 = 161
|
|
98
|
+
expect(priced.cost).toBe(161);
|
|
99
|
+
expect(priced.totalCost).toBe(177.1); // +10% tax
|
|
100
|
+
// String fieldType (some JSON paths) must still hit the area calculator.
|
|
101
|
+
var asString = estimateQuote(__assign(__assign({}, rules), { fields: [__assign(__assign({}, rules.fields[0]), { fieldType: '14' })] }), { quantity: 1, fieldValues: { 14: { value: '2,5' } } });
|
|
102
|
+
expect(asString.cost).toBe(161);
|
|
103
|
+
});
|
|
75
104
|
test('non-selectable field with value adds field cost; empty adds nothing', function () {
|
|
76
105
|
var mk = function (sel) {
|
|
77
106
|
var rules = __assign(__assign({}, base), { fields: [{
|
package/package.json
CHANGED
|
@@ -174,6 +174,9 @@ export class VariationField extends Entity {
|
|
|
174
174
|
@VariationField.property()
|
|
175
175
|
public areaInputType?: string;
|
|
176
176
|
|
|
177
|
+
@VariationField.property({type: Number})
|
|
178
|
+
public areaStep?: number | null;
|
|
179
|
+
|
|
177
180
|
@VariationField.property()
|
|
178
181
|
public aspectRatioLock?: boolean;
|
|
179
182
|
|
|
@@ -79,6 +79,47 @@ test('hidden conditional field is not costed', () => {
|
|
|
79
79
|
expect(r.cost).toBe(100);
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
+
test('area field multiplies height/width rate cards when dimensions set', () => {
|
|
83
|
+
const rules: PricingRules = {
|
|
84
|
+
...base,
|
|
85
|
+
product: { unitPrice: 1, minimumPrice: null, discountGroup: null },
|
|
86
|
+
fields: [{
|
|
87
|
+
id: 14, originalId: 14, position: 0, fieldType: 14, independent: true,
|
|
88
|
+
isSelectable: false, selectedBy: [],
|
|
89
|
+
variationCost: 0, variationUnitCost: 0,
|
|
90
|
+
variationCostDiscountGroup: null, variationUnitCostDiscountGroup: null,
|
|
91
|
+
areaUnit: 'mm',
|
|
92
|
+
heightVariationCost: 2, widthVariationCost: 2,
|
|
93
|
+
heightVariationUnitCost: 3, widthVariationUnitCost: 4,
|
|
94
|
+
heightVariationCostDiscountGroup: null,
|
|
95
|
+
heightVariationUnitCostDiscountGroup: null,
|
|
96
|
+
widthVariationCostDiscountGroup: null,
|
|
97
|
+
widthVariationUnitCostDiscountGroup: null,
|
|
98
|
+
options: [],
|
|
99
|
+
}],
|
|
100
|
+
};
|
|
101
|
+
const empty = estimateQuote(rules, {
|
|
102
|
+
quantity: 1, fieldValues: { 14: { value: '' } },
|
|
103
|
+
}) as QuoteResult;
|
|
104
|
+
expect(empty.cost).toBe(1);
|
|
105
|
+
const priced = estimateQuote(rules, {
|
|
106
|
+
quantity: 1, fieldValues: { 14: { value: '2,5' } },
|
|
107
|
+
}) as QuoteResult;
|
|
108
|
+
// area 2*5=10; onceOff 2*2*10=40; unit 3*4*10=120 → cost = 1 + 40 + 120 = 161
|
|
109
|
+
expect(priced.cost).toBe(161);
|
|
110
|
+
expect(priced.totalCost).toBe(177.1); // +10% tax
|
|
111
|
+
|
|
112
|
+
// String fieldType (some JSON paths) must still hit the area calculator.
|
|
113
|
+
const asString = estimateQuote(
|
|
114
|
+
{
|
|
115
|
+
...rules,
|
|
116
|
+
fields: [{ ...rules.fields[0], fieldType: '14' as unknown as number }],
|
|
117
|
+
},
|
|
118
|
+
{ quantity: 1, fieldValues: { 14: { value: '2,5' } } },
|
|
119
|
+
) as QuoteResult;
|
|
120
|
+
expect(asString.cost).toBe(161);
|
|
121
|
+
});
|
|
122
|
+
|
|
82
123
|
test('non-selectable field with value adds field cost; empty adds nothing', () => {
|
|
83
124
|
const mk = (sel: any) => {
|
|
84
125
|
const rules: PricingRules = {
|
package/src/pricing/estimate.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
import { applyDiscount } from './discount.js';
|
|
6
6
|
import { roundHalfEven } from './round.js';
|
|
7
7
|
import { resolveVisibleFields } from './visibility.js';
|
|
8
|
+
import { FieldType } from '../constants/field_types.js';
|
|
8
9
|
|
|
9
10
|
/** Form inputs often supply quantities as strings. Coerce before any arithmetic
|
|
10
11
|
* so `reduce((a,b)=>a+b)` cannot concatenate ("100"+"1" → "01001" → 1001). */
|
|
@@ -13,6 +14,19 @@ function toQuantity(value: unknown): number {
|
|
|
13
14
|
return Number.isFinite(n) ? n : 0;
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
function parseAreaMm(
|
|
18
|
+
value: string | number | null | undefined
|
|
19
|
+
): { heightMm: number; widthMm: number } | null {
|
|
20
|
+
if (value === undefined || value === null || value === '') return null;
|
|
21
|
+
const parts = String(value).split(',').map((p) => p.trim());
|
|
22
|
+
if (parts.length !== 2) return null;
|
|
23
|
+
const heightMm = Number(parts[0]);
|
|
24
|
+
const widthMm = Number(parts[1]);
|
|
25
|
+
if (!Number.isFinite(heightMm) || !Number.isFinite(widthMm)) return null;
|
|
26
|
+
if (heightMm <= 0 || widthMm <= 0) return null;
|
|
27
|
+
return { heightMm, widthMm };
|
|
28
|
+
}
|
|
29
|
+
|
|
16
30
|
function unitPriceAt(rules: PricingRules, qty: number): number {
|
|
17
31
|
let unitPrice = applyDiscount(rules.product.unitPrice, qty, rules.product.discountGroup);
|
|
18
32
|
const mop = rules.product.minimumPrice;
|
|
@@ -69,12 +83,75 @@ function isEmpty(field: PricingField, sel: FieldSelection | undefined): boolean
|
|
|
69
83
|
);
|
|
70
84
|
}
|
|
71
85
|
|
|
86
|
+
function areaCostFactor(
|
|
87
|
+
field: PricingField,
|
|
88
|
+
sel: FieldSelection | undefined,
|
|
89
|
+
groupQuantities: number[]
|
|
90
|
+
): { setup: number; unitList: number[] } {
|
|
91
|
+
const n = groupQuantities.length;
|
|
92
|
+
const parsed = parseAreaMm(sel?.value ?? null);
|
|
93
|
+
if (!parsed) {
|
|
94
|
+
return { setup: 0, unitList: new Array(n).fill(0) };
|
|
95
|
+
}
|
|
96
|
+
const totalQty = groupQuantities.reduce((a, b) => a + b, 0);
|
|
97
|
+
const heightSetup = variationSetupCost(
|
|
98
|
+
{
|
|
99
|
+
variationCost: field.heightVariationCost || 0,
|
|
100
|
+
variationCostDiscountGroup: field.heightVariationCostDiscountGroup ?? null,
|
|
101
|
+
},
|
|
102
|
+
totalQty
|
|
103
|
+
);
|
|
104
|
+
const widthSetup = variationSetupCost(
|
|
105
|
+
{
|
|
106
|
+
variationCost: field.widthVariationCost || 0,
|
|
107
|
+
variationCostDiscountGroup: field.widthVariationCostDiscountGroup ?? null,
|
|
108
|
+
},
|
|
109
|
+
totalQty
|
|
110
|
+
);
|
|
111
|
+
const heightUcs = variationUnitCosts(
|
|
112
|
+
{
|
|
113
|
+
variationUnitCost: field.heightVariationUnitCost || 0,
|
|
114
|
+
variationUnitCostDiscountGroup:
|
|
115
|
+
field.heightVariationUnitCostDiscountGroup ?? null,
|
|
116
|
+
},
|
|
117
|
+
groupQuantities
|
|
118
|
+
);
|
|
119
|
+
const widthUcs = variationUnitCosts(
|
|
120
|
+
{
|
|
121
|
+
variationUnitCost: field.widthVariationUnitCost || 0,
|
|
122
|
+
variationUnitCostDiscountGroup:
|
|
123
|
+
field.widthVariationUnitCostDiscountGroup ?? null,
|
|
124
|
+
},
|
|
125
|
+
groupQuantities
|
|
126
|
+
);
|
|
127
|
+
const areaUnit = (field.areaUnit || 'mm').toLowerCase();
|
|
128
|
+
const mmPerUnit =
|
|
129
|
+
areaUnit === 'm' ? 1000
|
|
130
|
+
: areaUnit === 'cm' ? 10
|
|
131
|
+
: areaUnit === 'in' || areaUnit === 'inch' || areaUnit === 'inches' ? 25.4
|
|
132
|
+
: areaUnit === 'ft' || areaUnit === 'foot' || areaUnit === 'feet' ? 304.8
|
|
133
|
+
: 1;
|
|
134
|
+
const heightU = parsed.heightMm / mmPerUnit;
|
|
135
|
+
const widthU = parsed.widthMm / mmPerUnit;
|
|
136
|
+
const area = heightU * widthU;
|
|
137
|
+
// onceOff/unit = rateH × rateW × height_u × width_u (in field areaUnit)
|
|
138
|
+
const setup = roundHalfEven(heightSetup * widthSetup * area, 3);
|
|
139
|
+
const unitList = heightUcs.map((hu, i) =>
|
|
140
|
+
roundHalfEven(hu * widthUcs[i] * area, 3)
|
|
141
|
+
);
|
|
142
|
+
return { setup, unitList };
|
|
143
|
+
}
|
|
144
|
+
|
|
72
145
|
function costFactor(
|
|
73
146
|
field: PricingField,
|
|
74
147
|
sel: FieldSelection | undefined,
|
|
75
148
|
groupQuantities: number[]
|
|
76
149
|
): { setup: number; unitList: number[] } {
|
|
77
150
|
const n = groupQuantities.length;
|
|
151
|
+
// Coerce: JSON/APIs occasionally deliver fieldType as a string.
|
|
152
|
+
if (Number(field.fieldType) === FieldType.AREA) {
|
|
153
|
+
return areaCostFactor(field, sel, groupQuantities);
|
|
154
|
+
}
|
|
78
155
|
if (isEmpty(field, sel)) {
|
|
79
156
|
return { setup: 0, unitList: new Array(n).fill(0) };
|
|
80
157
|
}
|
package/src/pricing/types.ts
CHANGED
|
@@ -35,6 +35,16 @@ export interface PricingField {
|
|
|
35
35
|
variationUnitCost: number;
|
|
36
36
|
variationCostDiscountGroup: DiscountGroup | null;
|
|
37
37
|
variationUnitCostDiscountGroup: DiscountGroup | null;
|
|
38
|
+
/** Area field (type 14): once-off and per-edge unit costs. */
|
|
39
|
+
areaUnit?: 'mm' | 'cm' | 'm' | string;
|
|
40
|
+
heightVariationCost?: number;
|
|
41
|
+
heightVariationUnitCost?: number;
|
|
42
|
+
widthVariationCost?: number;
|
|
43
|
+
widthVariationUnitCost?: number;
|
|
44
|
+
heightVariationCostDiscountGroup?: DiscountGroup | null;
|
|
45
|
+
heightVariationUnitCostDiscountGroup?: DiscountGroup | null;
|
|
46
|
+
widthVariationCostDiscountGroup?: DiscountGroup | null;
|
|
47
|
+
widthVariationUnitCostDiscountGroup?: DiscountGroup | null;
|
|
38
48
|
options: PricingOption[];
|
|
39
49
|
}
|
|
40
50
|
|