@merchi/product-form-sdk 1.0.25 → 1.0.27
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/components/Field.js +23 -0
- package/dist/helpers/constants.d.ts +2 -0
- package/dist/helpers/constants.js +3 -1
- package/dist/runtime/clientPricing.d.ts +7 -3
- package/dist/runtime/clientPricing.js +107 -9
- package/dist/runtime/pricing.js +3 -2
- package/dist/types/variation.d.ts +4 -0
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/components/Field.js
CHANGED
|
@@ -19,7 +19,27 @@ const FIELD = {
|
|
|
19
19
|
TURNAROUND_TIME: 12,
|
|
20
20
|
COLOUR_EXTRACT: 13,
|
|
21
21
|
AREA: 14,
|
|
22
|
+
EMBROIDERY: 16,
|
|
22
23
|
};
|
|
24
|
+
function parseEmbroideryValue(value) {
|
|
25
|
+
if (!value)
|
|
26
|
+
return {};
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(value);
|
|
29
|
+
return parsed && typeof parsed === 'object' ? parsed : { text: value };
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return { text: value };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function EmbroideryFieldControl({ field, value, onChange, }) {
|
|
36
|
+
const saved = parseEmbroideryValue(value);
|
|
37
|
+
const fileMode = field.embroideryInputMode !== 'text';
|
|
38
|
+
const write = (patch) => {
|
|
39
|
+
onChange(JSON.stringify({ ...saved, ...patch }));
|
|
40
|
+
};
|
|
41
|
+
return (_jsxs("div", { children: [fileMode ? (_jsx("p", { style: { fontFamily: theme.font, fontSize: 13, color: theme.muted, margin: '0 0 8px' }, children: "Upload the logo on the live product form. Adjust the size here when the measured size should change the price." })) : (_jsx("input", { type: "text", placeholder: "Wording", value: saved.text || '', onChange: (event) => write({ text: event.target.value }), style: { ...inputStyle(), marginBottom: 8 } })), _jsxs("div", { style: { display: 'flex', gap: 8 }, children: [_jsx("input", { type: "number", min: 0, placeholder: "Width (mm)", "aria-label": "Width (mm)", value: saved.widthMm ?? '', onChange: (event) => write({ widthMm: Number(event.target.value) || undefined }), style: inputStyle() }), _jsx("input", { type: "number", min: 0, placeholder: "Height (mm)", "aria-label": "Height (mm)", value: saved.heightMm ?? '', onChange: (event) => write({ heightMm: Number(event.target.value) || undefined }), style: inputStyle() })] })] }));
|
|
42
|
+
}
|
|
23
43
|
function Label({ field }) {
|
|
24
44
|
if (!field.name)
|
|
25
45
|
return null;
|
|
@@ -315,6 +335,9 @@ export function Field({ field, groupIndex, }) {
|
|
|
315
335
|
if (fieldType === FIELD.NUMBER_INPUT) {
|
|
316
336
|
return (_jsxs("div", { children: [_jsx(Label, { field: field }), _jsx("input", { type: "number", value: current?.value ?? '', onChange: (e) => setText(e.target.value), style: inputStyle() })] }));
|
|
317
337
|
}
|
|
338
|
+
if (fieldType === FIELD.EMBROIDERY) {
|
|
339
|
+
return (_jsxs("div", { children: [_jsx(Label, { field: field }), _jsx(EmbroideryFieldControl, { field: field, value: current?.value ?? '', onChange: setText })] }));
|
|
340
|
+
}
|
|
318
341
|
if (fieldType === FIELD.AREA) {
|
|
319
342
|
return (_jsxs("div", { children: [_jsx(Label, { field: field }), _jsx(AreaFieldControl, { field: field, value: current?.value ?? '', onChange: setText })] }));
|
|
320
343
|
}
|
|
@@ -4,5 +4,7 @@ export declare const OPTION_FIELD_TYPES: Set<number>;
|
|
|
4
4
|
export declare const COLOUR_EXTRACT = 13;
|
|
5
5
|
/** Area: height × width stored as millimetres (`"heightMm,widthMm"`). */
|
|
6
6
|
export declare const AREA = 14;
|
|
7
|
+
/** Embroidery: text or an uploaded design, priced by stitches and colours. */
|
|
8
|
+
export declare const EMBROIDERY = 16;
|
|
7
9
|
/** Field types that carry uploaded files on the variation. */
|
|
8
10
|
export declare const FILE_FIELD_TYPES: Set<number>;
|
|
@@ -4,5 +4,7 @@ export const OPTION_FIELD_TYPES = new Set([2, 6, 7, 9, 11]);
|
|
|
4
4
|
export const COLOUR_EXTRACT = 13;
|
|
5
5
|
/** Area: height × width stored as millimetres (`"heightMm,widthMm"`). */
|
|
6
6
|
export const AREA = 14;
|
|
7
|
+
/** Embroidery: text or an uploaded design, priced by stitches and colours. */
|
|
8
|
+
export const EMBROIDERY = 16;
|
|
7
9
|
/** Field types that carry uploaded files on the variation. */
|
|
8
|
-
export const FILE_FIELD_TYPES = new Set([3, COLOUR_EXTRACT]);
|
|
10
|
+
export const FILE_FIELD_TYPES = new Set([3, COLOUR_EXTRACT, EMBROIDERY]);
|
|
@@ -2,6 +2,9 @@ import { JobJson } from '../types/job.js';
|
|
|
2
2
|
interface FieldSelection {
|
|
3
3
|
selectedOptionIds?: number[];
|
|
4
4
|
value?: string | number | null;
|
|
5
|
+
colourCount?: number;
|
|
6
|
+
coverage?: number;
|
|
7
|
+
hasFiles?: boolean;
|
|
5
8
|
}
|
|
6
9
|
interface Selections {
|
|
7
10
|
quantity?: number;
|
|
@@ -14,16 +17,17 @@ interface Selections {
|
|
|
14
17
|
interface PricingField {
|
|
15
18
|
id: number;
|
|
16
19
|
isSelectable: boolean;
|
|
20
|
+
fieldType?: number;
|
|
17
21
|
}
|
|
18
22
|
export interface PricingRules {
|
|
19
23
|
fields: PricingField[];
|
|
20
24
|
groupFields: PricingField[];
|
|
21
25
|
hasGroups: boolean;
|
|
22
26
|
}
|
|
23
|
-
/** Convert a job's form values into the `Selections` shape estimateQuote needs.
|
|
24
|
-
* Ported from @merchi/product-form's selections helper so a custom form gets
|
|
25
|
-
* identical client-side pricing behaviour. */
|
|
26
27
|
export declare function toSelections(job: JobJson, rules: PricingRules): Selections;
|
|
28
|
+
/** Fill coverage and measured size before the local quote. Stitch price needs
|
|
29
|
+
* those figures, and they come from the artwork rather than from arithmetic. */
|
|
30
|
+
export declare function withEmbroideryEstimates(apiUrl: string, rules: PricingRules, job: JobJson): Promise<JobJson>;
|
|
27
31
|
/** Compute a quote entirely client-side from pricing rules and merge the pricing
|
|
28
32
|
* fields into the job. Returns the priced job, or null when client-side pricing
|
|
29
33
|
* is unsupported for these rules (caller then falls back to a server quote). */
|
|
@@ -7,14 +7,36 @@ function parseOptionIds(value) {
|
|
|
7
7
|
.map((s) => parseInt(s.trim(), 10))
|
|
8
8
|
.filter((n) => !isNaN(n));
|
|
9
9
|
}
|
|
10
|
-
function
|
|
10
|
+
function embroiderySelection(variation) {
|
|
11
|
+
let parsed = null;
|
|
12
|
+
try {
|
|
13
|
+
parsed = variation?.value ? JSON.parse(String(variation.value)) : null;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
parsed = null;
|
|
17
|
+
}
|
|
18
|
+
const colours = Array.isArray(parsed?.colours) ? parsed.colours : [];
|
|
19
|
+
const files = variation?.variationFiles || [];
|
|
20
|
+
return {
|
|
21
|
+
value: variation?.value ?? null,
|
|
22
|
+
colourCount: colours.length,
|
|
23
|
+
coverage: Number(parsed?.coverage) || undefined,
|
|
24
|
+
hasFiles: files.some((file) => Boolean(file && file.id)),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function buildFieldValues(variations, metaByField) {
|
|
11
28
|
const out = {};
|
|
12
29
|
for (const variation of variations || []) {
|
|
13
30
|
const field = variation && variation.variationField;
|
|
14
31
|
const fieldId = typeof field === 'object' && field !== null ? field.id : field;
|
|
15
32
|
if (fieldId === undefined || fieldId === null)
|
|
16
33
|
continue;
|
|
17
|
-
|
|
34
|
+
const meta = metaByField[fieldId];
|
|
35
|
+
if (Number(meta?.fieldType) === 16) {
|
|
36
|
+
out[fieldId] = embroiderySelection(variation);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (meta?.isSelectable) {
|
|
18
40
|
out[fieldId] = { selectedOptionIds: parseOptionIds(variation.value) };
|
|
19
41
|
}
|
|
20
42
|
else {
|
|
@@ -26,25 +48,101 @@ function buildFieldValues(variations, selectableByField) {
|
|
|
26
48
|
/** Convert a job's form values into the `Selections` shape estimateQuote needs.
|
|
27
49
|
* Ported from @merchi/product-form's selections helper so a custom form gets
|
|
28
50
|
* identical client-side pricing behaviour. */
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
for (const
|
|
32
|
-
|
|
51
|
+
function fieldMeta(rules) {
|
|
52
|
+
const meta = {};
|
|
53
|
+
for (const field of [...(rules.fields || []), ...(rules.groupFields || [])]) {
|
|
54
|
+
meta[field.id] = field;
|
|
33
55
|
}
|
|
56
|
+
return meta;
|
|
57
|
+
}
|
|
58
|
+
export function toSelections(job, rules) {
|
|
59
|
+
const meta = fieldMeta(rules);
|
|
34
60
|
if (rules.hasGroups) {
|
|
35
61
|
return {
|
|
36
|
-
fieldValues: buildFieldValues(job.variations || [],
|
|
62
|
+
fieldValues: buildFieldValues(job.variations || [], meta),
|
|
37
63
|
groups: (job.variationsGroups || []).map((g) => ({
|
|
38
64
|
quantity: g.quantity || 0,
|
|
39
|
-
fieldValues: buildFieldValues(g.variations || [],
|
|
65
|
+
fieldValues: buildFieldValues(g.variations || [], meta),
|
|
40
66
|
})),
|
|
41
67
|
};
|
|
42
68
|
}
|
|
43
69
|
return {
|
|
44
70
|
quantity: job.quantity || 0,
|
|
45
|
-
fieldValues: buildFieldValues(job.variations || [],
|
|
71
|
+
fieldValues: buildFieldValues(job.variations || [], meta),
|
|
46
72
|
};
|
|
47
73
|
}
|
|
74
|
+
/** Fill coverage and measured size before the local quote. Stitch price needs
|
|
75
|
+
* those figures, and they come from the artwork rather than from arithmetic. */
|
|
76
|
+
export async function withEmbroideryEstimates(apiUrl, rules, job) {
|
|
77
|
+
const embroideryIds = new Set([...(rules.fields || []), ...(rules.groupFields || [])]
|
|
78
|
+
.filter((field) => Number(field.fieldType) === 16)
|
|
79
|
+
.map((field) => field.id));
|
|
80
|
+
if (!embroideryIds.size)
|
|
81
|
+
return job;
|
|
82
|
+
const base = apiUrl.endsWith('/') ? apiUrl : `${apiUrl}/`;
|
|
83
|
+
async function fill(variation) {
|
|
84
|
+
const field = variation && variation.variationField;
|
|
85
|
+
const fieldId = typeof field === 'object' && field !== null ? field.id : field;
|
|
86
|
+
if (fieldId == null || !embroideryIds.has(Number(fieldId)))
|
|
87
|
+
return variation;
|
|
88
|
+
let parsed = null;
|
|
89
|
+
try {
|
|
90
|
+
parsed = variation.value ? JSON.parse(String(variation.value)) : null;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
parsed = null;
|
|
94
|
+
}
|
|
95
|
+
const saved = parsed && typeof parsed === 'object' ? parsed : {};
|
|
96
|
+
const fileId = (variation.variationFiles || [])[0]?.id;
|
|
97
|
+
const colours = Array.isArray(saved.colours) ? saved.colours : [];
|
|
98
|
+
if (saved.coverage
|
|
99
|
+
&& saved.widthMm
|
|
100
|
+
&& saved.heightMm
|
|
101
|
+
&& (String(saved.text || '').trim() || fileId || colours.length)) {
|
|
102
|
+
return variation;
|
|
103
|
+
}
|
|
104
|
+
if (!fileId && !String(saved.text || '').trim() && !(saved.widthMm && saved.heightMm)) {
|
|
105
|
+
return variation;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const response = await fetch(`${base}variation-fields/${fieldId}/estimate-embroidery/`, {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
headers: { 'Content-Type': 'application/json' },
|
|
111
|
+
body: JSON.stringify({
|
|
112
|
+
text: saved.text || '',
|
|
113
|
+
widthMm: saved.widthMm,
|
|
114
|
+
heightMm: saved.heightMm,
|
|
115
|
+
colours: saved.colours,
|
|
116
|
+
fileId,
|
|
117
|
+
}),
|
|
118
|
+
});
|
|
119
|
+
if (!response.ok)
|
|
120
|
+
return variation;
|
|
121
|
+
const data = await response.json();
|
|
122
|
+
const merged = {
|
|
123
|
+
...saved,
|
|
124
|
+
colours: data.colours || saved.colours,
|
|
125
|
+
coverage: data.coverage,
|
|
126
|
+
pixelWidth: data.pixelWidth || saved.pixelWidth,
|
|
127
|
+
pixelHeight: data.pixelHeight || saved.pixelHeight,
|
|
128
|
+
widthMm: saved.widthMm || data.widthMm || data.measuredWidthMm,
|
|
129
|
+
heightMm: saved.heightMm || data.heightMm || data.measuredHeightMm,
|
|
130
|
+
};
|
|
131
|
+
return { ...variation, value: JSON.stringify(merged) };
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return variation;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const variations = await Promise.all((job.variations || []).map(fill));
|
|
138
|
+
const variationsGroups = job.variationsGroups
|
|
139
|
+
? await Promise.all(job.variationsGroups.map(async (group) => ({
|
|
140
|
+
...group,
|
|
141
|
+
variations: await Promise.all((group.variations || []).map(fill)),
|
|
142
|
+
})))
|
|
143
|
+
: job.variationsGroups;
|
|
144
|
+
return { ...job, variations, variationsGroups };
|
|
145
|
+
}
|
|
48
146
|
/** Compute a quote entirely client-side from pricing rules and merge the pricing
|
|
49
147
|
* fields into the job. Returns the priced job, or null when client-side pricing
|
|
50
148
|
* is unsupported for these rules (caller then falls back to a server quote). */
|
package/dist/runtime/pricing.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Merchi } from '@merchi/sdk';
|
|
2
2
|
import { serializeJob } from '../helpers/serialize.js';
|
|
3
|
-
import { clientQuote } from './clientPricing.js';
|
|
3
|
+
import { clientQuote, withEmbroideryEstimates } from './clientPricing.js';
|
|
4
4
|
/** Build a pricing helper bound to a backend api url.
|
|
5
5
|
*
|
|
6
6
|
* When `product.clientSideCalculation` is set, getQuote computes the quote
|
|
@@ -59,7 +59,8 @@ export function createPricing(apiUrl, options = {}) {
|
|
|
59
59
|
if (product && product.clientSideCalculation) {
|
|
60
60
|
const rules = await getRules();
|
|
61
61
|
if (rules) {
|
|
62
|
-
const
|
|
62
|
+
const prepared = await withEmbroideryEstimates(apiUrl, rules, job);
|
|
63
|
+
const local = clientQuote(rules, prepared);
|
|
63
64
|
if (local)
|
|
64
65
|
return local;
|
|
65
66
|
}
|
|
@@ -23,6 +23,10 @@ export interface VariationFieldJson {
|
|
|
23
23
|
simplifyColours?: boolean;
|
|
24
24
|
colourVariationCost?: number;
|
|
25
25
|
colourVariationUnitCost?: number;
|
|
26
|
+
embroideryInputMode?: string;
|
|
27
|
+
stitchDensity?: number;
|
|
28
|
+
stitchVariationCost?: number;
|
|
29
|
+
stitchVariationUnitCost?: number;
|
|
26
30
|
}
|
|
27
31
|
/** A variation selection within a job (one chosen field value). */
|
|
28
32
|
export interface VariationJson {
|
package/dist/version.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Auto-synced from package.json — do not edit; run `npm run build`. */
|
|
2
2
|
export function getProductFormSdkVersion() {
|
|
3
|
-
return '1.0.
|
|
3
|
+
return '1.0.27';
|
|
4
4
|
}
|
|
5
5
|
/** @deprecated Prefer getProductFormSdkVersion(); kept for gate/export compatibility. */
|
|
6
6
|
export const SDK_VERSION = getProductFormSdkVersion();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@merchi/product-form-sdk",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.27",
|
|
4
4
|
"description": "Contract SDK for AI-built Merchi custom product forms.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"react": "^18.0.0 || ^19.0.0"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@merchi/sdk": "^1.42.
|
|
33
|
+
"@merchi/sdk": "^1.42.16"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/jest": "^29.0.0",
|