@misiki/gocommerce-connector 0.1.0 → 0.2.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 +155 -152
- package/dist/index-CJ836ZI3.d.cts +5424 -0
- package/dist/index-CJ836ZI3.d.ts +5424 -0
- package/dist/index.cjs +2308 -2066
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +85 -1
- package/dist/index.d.ts +85 -1
- package/dist/index.js +2273 -2037
- package/dist/index.js.map +1 -1
- package/dist/services/index.cjs +2256 -2061
- package/dist/services/index.cjs.map +1 -1
- package/dist/services/index.d.cts +1 -4742
- package/dist/services/index.d.ts +1 -4742
- package/dist/services/index.js +2247 -2027
- package/dist/services/index.js.map +1 -1
- package/package.json +9 -7
package/dist/index.cjs
CHANGED
|
@@ -1,2352 +1,2587 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// src/
|
|
4
|
-
var API_PROXY_PREFIX = "/proxy/gocommerce";
|
|
3
|
+
// src/errors.ts
|
|
5
4
|
var GoCommerceError = class extends Error {
|
|
5
|
+
status;
|
|
6
6
|
code;
|
|
7
7
|
details;
|
|
8
|
-
status
|
|
9
|
-
constructor(message, code = "internal_error", status = 0, details) {
|
|
8
|
+
constructor(message, status, code, details) {
|
|
10
9
|
super(message);
|
|
11
10
|
this.name = "GoCommerceError";
|
|
12
|
-
this.code = code;
|
|
13
11
|
this.status = status;
|
|
12
|
+
this.code = code;
|
|
14
13
|
this.details = details;
|
|
15
14
|
}
|
|
16
15
|
};
|
|
17
|
-
var
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
this.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
this._fetch = fetchFn;
|
|
26
|
-
return this;
|
|
27
|
-
}
|
|
28
|
-
getFetch() {
|
|
29
|
-
return this._fetch;
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* The fetch a GoCommerce call actually goes out on.
|
|
33
|
-
*
|
|
34
|
-
* In the browser it is whatever was handed in, against this storefront's own origin. On the
|
|
35
|
-
* server it is deliberately the platform's, not SvelteKit's: a universal `+page.ts` load is
|
|
36
|
-
* given a `fetch` that simulates a browser's CORS check, and GoCommerce sends no
|
|
37
|
-
* `Access-Control-Allow-Origin`, so the same product page that renders from a `+page.server.ts`
|
|
38
|
-
* would throw from a `+page.ts`. Nothing is lost by stepping around it — SvelteKit's fetch adds
|
|
39
|
-
* cookie forwarding and internal routing, and both are about this storefront's own origin, not
|
|
40
|
-
* a separate API host.
|
|
41
|
-
*/
|
|
42
|
-
transport() {
|
|
43
|
-
return _BaseService.isBrowser() ? this._fetch : (...args) => globalThis.fetch(...args);
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* The hook the storefront's `src/lib/core/connectors/init.ts` calls with `PUBLIC_GOCOMMERCE_*`. Merges, so a
|
|
47
|
-
* later call with a subset never erases the base url an earlier one established.
|
|
48
|
-
*/
|
|
49
|
-
static setCredentials(creds) {
|
|
50
|
-
_BaseService._credentials = { ..._BaseService._credentials, ...creds };
|
|
51
|
-
}
|
|
52
|
-
static getCredentials() {
|
|
53
|
-
return _BaseService._credentials;
|
|
54
|
-
}
|
|
55
|
-
static setBaseUrl(url) {
|
|
56
|
-
if (url) _BaseService._credentials.apiUrl = url;
|
|
57
|
-
}
|
|
58
|
-
/** Trailing slashes stripped so `${base}/api/products` never doubles up. */
|
|
59
|
-
static getBaseUrl() {
|
|
60
|
-
return (_BaseService._credentials.apiUrl || "").replace(/\/+$/, "");
|
|
61
|
-
}
|
|
62
|
-
static setLanguageCode(code) {
|
|
63
|
-
_BaseService._languageCode = code;
|
|
64
|
-
}
|
|
65
|
-
static getLanguageCode() {
|
|
66
|
-
return _BaseService._languageCode;
|
|
67
|
-
}
|
|
68
|
-
setBaseUrl(url) {
|
|
69
|
-
_BaseService.setBaseUrl(url);
|
|
70
|
-
return this;
|
|
71
|
-
}
|
|
72
|
-
getBaseUrl() {
|
|
73
|
-
return _BaseService.getBaseUrl();
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Where a request for `path` actually goes.
|
|
77
|
-
*
|
|
78
|
-
* GoCommerce sends no CORS headers and answers a preflight with 405, so a browser cannot reach
|
|
79
|
-
* it cross-origin at all — and neither can a universal `+page.ts` load, because SvelteKit
|
|
80
|
-
* simulates the same CORS check server-side for consistency. So anything the browser issues goes
|
|
81
|
-
* through this storefront's own origin (`src/routes/proxy/gocommerce/[...path]`), which is
|
|
82
|
-
* same-origin and needs no headers from the API.
|
|
83
|
-
*
|
|
84
|
-
* On the server there is no such rule, and the extra hop through the storefront's own handler
|
|
85
|
-
* would be a request the process makes to itself, so it calls the API directly.
|
|
86
|
-
*/
|
|
87
|
-
static apiUrl(path) {
|
|
88
|
-
if (/^https?:\/\//i.test(path)) return path;
|
|
89
|
-
const suffix = path.startsWith("/") ? path : `/${path}`;
|
|
90
|
-
if (_BaseService.isBrowser()) return `${API_PROXY_PREFIX}${suffix}`;
|
|
91
|
-
const base = _BaseService.getBaseUrl();
|
|
92
|
-
if (!base) {
|
|
93
|
-
throw new GoCommerceError("PUBLIC_GOCOMMERCE_API_URL is not configured \u2014 add it to .env (see docs/GOCOMMERCE.md).", "not_ready");
|
|
94
|
-
}
|
|
95
|
-
return `${base}${suffix}`;
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Where an `<img>` points.
|
|
99
|
-
*
|
|
100
|
-
* Media urls come back site-relative for files this store holds (`/media/42.jpg`). Unlike an API
|
|
101
|
-
* call, the consumer is always the browser — a server-rendered `<img src>` is still fetched by
|
|
102
|
-
* it — so this always resolves through the proxy, and the picture loads wherever the API happens
|
|
103
|
-
* to be reachable from. A url that is already absolute belongs to someone else's host and is
|
|
104
|
-
* left exactly as it is.
|
|
105
|
-
*/
|
|
106
|
-
static mediaUrl(path) {
|
|
107
|
-
if (/^https?:\/\//i.test(path)) return path;
|
|
108
|
-
return `${API_PROXY_PREFIX}${path.startsWith("/") ? path : `/${path}`}`;
|
|
109
|
-
}
|
|
110
|
-
static isBrowser() {
|
|
111
|
-
return typeof window !== "undefined" && typeof window.document !== "undefined";
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* One request against the GoCommerce API.
|
|
115
|
-
*
|
|
116
|
-
* Unwraps `{ data, meta }` and turns `{ error: { code, message } }` into a thrown
|
|
117
|
-
* GoCommerceError carrying the server's own wording — a 409 at checkout says which line went
|
|
118
|
-
* stale, and that sentence is what the shopper needs to see.
|
|
119
|
-
*/
|
|
120
|
-
async api(method, path, body, headers = {}) {
|
|
121
|
-
const url = _BaseService.apiUrl(path);
|
|
122
|
-
const lang = _BaseService.getLanguageCode();
|
|
123
|
-
let res;
|
|
124
|
-
try {
|
|
125
|
-
res = await this.transport()(url, {
|
|
126
|
-
method,
|
|
127
|
-
headers: {
|
|
128
|
-
Accept: "application/json",
|
|
129
|
-
...body === void 0 ? {} : { "Content-Type": "application/json" },
|
|
130
|
-
...lang ? { "Accept-Language": lang } : {},
|
|
131
|
-
...headers
|
|
132
|
-
},
|
|
133
|
-
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
134
|
-
});
|
|
135
|
-
} catch (cause) {
|
|
136
|
-
const reason = cause?.cause?.code ?? cause?.message ?? "unknown error";
|
|
137
|
-
throw new GoCommerceError(
|
|
138
|
-
`Could not reach the GoCommerce server at ${_BaseService.getBaseUrl() || url} \u2014 ${reason}. Is it running?`,
|
|
139
|
-
"not_ready",
|
|
140
|
-
0,
|
|
141
|
-
cause
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
const text = await res.text();
|
|
145
|
-
let parsed = void 0;
|
|
146
|
-
if (text) {
|
|
147
|
-
try {
|
|
148
|
-
parsed = JSON.parse(text);
|
|
149
|
-
} catch {
|
|
150
|
-
throw new GoCommerceError(`GoCommerce returned a non-JSON response for ${method} ${path} (${res.status}).`, "internal_error", res.status);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
const envelope = parsed ?? {};
|
|
154
|
-
if (!res.ok || envelope.error) {
|
|
155
|
-
const error = envelope.error;
|
|
156
|
-
throw new GoCommerceError(
|
|
157
|
-
error?.message || envelope.message || `GoCommerce request failed: ${method} ${path} (${res.status}).`,
|
|
158
|
-
error?.code || "internal_error",
|
|
159
|
-
res.status,
|
|
160
|
-
error?.details
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
return { data: envelope.data, meta: envelope.meta };
|
|
164
|
-
}
|
|
165
|
-
/**
|
|
166
|
-
* Litekart-era raw-url reads. Deliberately not routed to GoCommerce: a bare `/api/...` here means
|
|
167
|
-
* a path from the storefront's own REST API, which rest-guard.ts answers from local data or
|
|
168
|
-
* reports. Absolute urls pass straight through.
|
|
169
|
-
*/
|
|
170
|
-
async get(url) {
|
|
171
|
-
const res = await this._fetch(url, { headers: { Accept: "application/json" } });
|
|
172
|
-
return await res.json();
|
|
173
|
-
}
|
|
174
|
-
async post(url, data) {
|
|
175
|
-
const res = await this._fetch(url, {
|
|
176
|
-
method: "POST",
|
|
177
|
-
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
178
|
-
body: JSON.stringify(data)
|
|
179
|
-
});
|
|
180
|
-
return await res.json();
|
|
181
|
-
}
|
|
182
|
-
async put(url, data) {
|
|
183
|
-
const res = await this._fetch(url, {
|
|
184
|
-
method: "PUT",
|
|
185
|
-
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
186
|
-
body: JSON.stringify(data)
|
|
187
|
-
});
|
|
188
|
-
return await res.json();
|
|
189
|
-
}
|
|
190
|
-
async patch(url, data) {
|
|
191
|
-
const res = await this._fetch(url, {
|
|
192
|
-
method: "PATCH",
|
|
193
|
-
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
194
|
-
body: JSON.stringify(data)
|
|
195
|
-
});
|
|
196
|
-
return await res.json();
|
|
197
|
-
}
|
|
198
|
-
async delete(url) {
|
|
199
|
-
const res = await this._fetch(url, { method: "DELETE", headers: { Accept: "application/json" } });
|
|
200
|
-
return await res.json();
|
|
16
|
+
var NotSupportedError = class extends Error {
|
|
17
|
+
service;
|
|
18
|
+
method;
|
|
19
|
+
constructor(service, method, detail) {
|
|
20
|
+
super(`${service}.${method}() is not supported by GoCommerce${detail ? `: ${detail}` : "."}`);
|
|
21
|
+
this.name = "NotSupportedError";
|
|
22
|
+
this.service = service;
|
|
23
|
+
this.method = method;
|
|
201
24
|
}
|
|
202
25
|
};
|
|
203
26
|
|
|
204
|
-
// src/
|
|
27
|
+
// src/money.ts
|
|
28
|
+
var ZERO_DECIMAL = /* @__PURE__ */ new Set([
|
|
29
|
+
"BIF",
|
|
30
|
+
"CLP",
|
|
31
|
+
"DJF",
|
|
32
|
+
"GNF",
|
|
33
|
+
"ISK",
|
|
34
|
+
"JPY",
|
|
35
|
+
"KMF",
|
|
36
|
+
"KRW",
|
|
37
|
+
"PYG",
|
|
38
|
+
"RWF",
|
|
39
|
+
"UGX",
|
|
40
|
+
"VND",
|
|
41
|
+
"VUV",
|
|
42
|
+
"XAF",
|
|
43
|
+
"XOF",
|
|
44
|
+
"XPF"
|
|
45
|
+
]);
|
|
46
|
+
var THREE_DECIMAL = /* @__PURE__ */ new Set(["BHD", "IQD", "JOD", "KWD", "LYD", "OMR", "TND"]);
|
|
47
|
+
function currencyExponent(code) {
|
|
48
|
+
const c = (code ?? "").toUpperCase();
|
|
49
|
+
if (ZERO_DECIMAL.has(c)) return 0;
|
|
50
|
+
if (THREE_DECIMAL.has(c)) return 3;
|
|
51
|
+
return 2;
|
|
52
|
+
}
|
|
53
|
+
function toMajor(money) {
|
|
54
|
+
if (!money) return 0;
|
|
55
|
+
return minorToMajor(money.amount_minor, money.currency);
|
|
56
|
+
}
|
|
57
|
+
function minorToMajor(minor, currency) {
|
|
58
|
+
if (minor === null || minor === void 0 || !Number.isFinite(minor)) return 0;
|
|
59
|
+
const exp = currencyExponent(currency);
|
|
60
|
+
if (exp === 0) return minor;
|
|
61
|
+
return Number((minor / 10 ** exp).toFixed(exp));
|
|
62
|
+
}
|
|
63
|
+
function toMinor(amount, currency) {
|
|
64
|
+
const exp = currencyExponent(currency);
|
|
65
|
+
return Math.round(amount * 10 ** exp);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/static-store.ts
|
|
205
69
|
var provider;
|
|
206
70
|
var setStaticStore = (fn) => {
|
|
207
71
|
provider = fn;
|
|
208
72
|
};
|
|
209
73
|
var hasStaticStore = () => Boolean(provider);
|
|
210
|
-
var IMPOSSIBLE_FEATURES = {
|
|
211
|
-
accounts: { active: false },
|
|
212
|
-
isDiscountCoupons: { active: false },
|
|
213
|
-
isWishlist: { active: false },
|
|
214
|
-
isProductReviewsAndRatings: { active: false },
|
|
215
|
-
isMultiVendor: { active: false }
|
|
216
|
-
};
|
|
217
74
|
var readStaticStore = async () => {
|
|
218
|
-
if (!provider) return
|
|
219
|
-
|
|
220
|
-
if (!store) return store;
|
|
221
|
-
return {
|
|
222
|
-
...store,
|
|
223
|
-
plugins: { ...store.plugins ?? {}, ...IMPOSSIBLE_FEATURES }
|
|
224
|
-
};
|
|
75
|
+
if (!provider) return {};
|
|
76
|
+
return await provider() ?? {};
|
|
225
77
|
};
|
|
226
|
-
var
|
|
78
|
+
var connectorName = "gocommerce";
|
|
227
79
|
|
|
228
|
-
// src/
|
|
229
|
-
var
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
80
|
+
// src/rest-guard.ts
|
|
81
|
+
var COLLECTION_SEGMENTS = /* @__PURE__ */ new Set([
|
|
82
|
+
"all",
|
|
83
|
+
"list",
|
|
84
|
+
"list-by-parent",
|
|
85
|
+
"list-public",
|
|
86
|
+
"latest",
|
|
87
|
+
"search",
|
|
88
|
+
"public",
|
|
89
|
+
"public-details",
|
|
90
|
+
"me",
|
|
91
|
+
"multiple"
|
|
92
|
+
]);
|
|
93
|
+
var emptyResult = () => ({ data: [], count: 0, pageSize: 0, noOfPage: 0, page: 1 });
|
|
94
|
+
var addressesOneRecord = (url) => {
|
|
95
|
+
const segments = url.split("?")[0].split("/").filter(Boolean);
|
|
96
|
+
if (segments.length < 3) return false;
|
|
97
|
+
return !COLLECTION_SEGMENTS.has(segments[segments.length - 1].toLowerCase());
|
|
98
|
+
};
|
|
99
|
+
var reported = /* @__PURE__ */ new Set();
|
|
100
|
+
var report = (method, url) => {
|
|
101
|
+
const key = `${method} ${url.split("?")[0]}`;
|
|
102
|
+
if (reported.has(key)) return;
|
|
103
|
+
reported.add(key);
|
|
104
|
+
console.warn(
|
|
105
|
+
`[gocommerce] no native implementation for \`${key}\` \u2014 that is a Litekart REST path, and it was not called.`
|
|
106
|
+
);
|
|
107
|
+
};
|
|
108
|
+
var localResolver;
|
|
109
|
+
var serveRestLocally = (resolver) => {
|
|
110
|
+
localResolver = resolver;
|
|
111
|
+
};
|
|
112
|
+
var resolveRestLocally = async (method, url) => {
|
|
113
|
+
if (method === "get") {
|
|
114
|
+
const local = await localResolver?.(url);
|
|
115
|
+
if (local !== void 0) return local;
|
|
116
|
+
}
|
|
117
|
+
report(method, url);
|
|
118
|
+
if (method !== "get" || addressesOneRecord(url)) {
|
|
119
|
+
throw new Error(`This feature is not available on this store (gocommerce).`);
|
|
120
|
+
}
|
|
121
|
+
return emptyResult();
|
|
249
122
|
};
|
|
250
123
|
|
|
251
|
-
// src/
|
|
252
|
-
var
|
|
253
|
-
var
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
var
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
var variantStock = (variant) => {
|
|
284
|
-
if (!variant) return 0;
|
|
285
|
-
if (variant.track_inventory === false) return UNTRACKED_STOCK;
|
|
286
|
-
const available = Math.max(0, variant.available ?? 0);
|
|
287
|
-
return available === 0 && variant.continue_selling ? UNTRACKED_STOCK : available;
|
|
288
|
-
};
|
|
289
|
-
var productImages = (product) => {
|
|
290
|
-
const seen = /* @__PURE__ */ new Set();
|
|
291
|
-
const featured = absoluteMedia(product?.image_url);
|
|
292
|
-
if (featured) seen.add(featured);
|
|
293
|
-
for (const variant of product?.variants || []) {
|
|
294
|
-
const url = absoluteMedia(variant?.image?.url);
|
|
295
|
-
if (url) seen.add(url);
|
|
296
|
-
}
|
|
297
|
-
return [...seen];
|
|
298
|
-
};
|
|
299
|
-
var flattenMetadata = (metadata) => {
|
|
300
|
-
if (!metadata || typeof metadata !== "object") return null;
|
|
301
|
-
const out = {};
|
|
302
|
-
const printable = (value) => {
|
|
303
|
-
if (value === null || value === void 0 || value === "") return void 0;
|
|
304
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return String(value);
|
|
305
|
-
if (Array.isArray(value)) {
|
|
306
|
-
const items = value.map(printable).filter(Boolean);
|
|
307
|
-
return items.length ? items.join(", ") : void 0;
|
|
308
|
-
}
|
|
309
|
-
return void 0;
|
|
310
|
-
};
|
|
311
|
-
const walk = (node, namespace = "") => {
|
|
312
|
-
for (const [key, value] of Object.entries(node)) {
|
|
313
|
-
const text = printable(value);
|
|
314
|
-
if (text !== void 0) {
|
|
315
|
-
out[key in out && namespace ? `${namespace} ${key}` : key] = text;
|
|
316
|
-
continue;
|
|
317
|
-
}
|
|
318
|
-
if (value && typeof value === "object" && !Array.isArray(value)) walk(value, key);
|
|
319
|
-
}
|
|
124
|
+
// src/cache.ts
|
|
125
|
+
var TTL_MS = 6e4;
|
|
126
|
+
var entries = /* @__PURE__ */ new Map();
|
|
127
|
+
function remember(key, load, ttl = TTL_MS) {
|
|
128
|
+
const hit = entries.get(key);
|
|
129
|
+
if (hit && Date.now() - hit.at < ttl) return hit.value;
|
|
130
|
+
const value = load();
|
|
131
|
+
entries.set(key, { at: Date.now(), value });
|
|
132
|
+
value.catch(() => {
|
|
133
|
+
if (entries.get(key)?.value === value) entries.delete(key);
|
|
134
|
+
});
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
function forgetAll() {
|
|
138
|
+
entries.clear();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/map.ts
|
|
142
|
+
var str = (v) => v === null || v === void 0 ? "" : String(v);
|
|
143
|
+
var arr = (v) => v ?? [];
|
|
144
|
+
var liveVariants = (p) => arr(p.variants).filter((v) => v.active);
|
|
145
|
+
function variantImages(v) {
|
|
146
|
+
const out = [];
|
|
147
|
+
for (const img of [v.image, ...arr(v.images)]) {
|
|
148
|
+
if (img?.url && (img.kind ?? "image") === "image" && !out.includes(img.url)) out.push(img.url);
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
function productImages(p) {
|
|
153
|
+
const out = [];
|
|
154
|
+
const push = (url) => {
|
|
155
|
+
if (url && !out.includes(url)) out.push(url);
|
|
320
156
|
};
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
const
|
|
328
|
-
const image = absoluteMedia(variant.image?.url) || absoluteMedia(product.image_url);
|
|
157
|
+
push(p.image_url);
|
|
158
|
+
for (const v of arr(p.variants)) for (const url of variantImages(v)) push(url);
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
function toVariant(v, p, optionless) {
|
|
162
|
+
const images = variantImages(v);
|
|
163
|
+
const options = arr(p.options);
|
|
329
164
|
return {
|
|
330
|
-
id:
|
|
331
|
-
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
title:
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
height: null,
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
// No compare-at price recorded means nothing is on sale; mrp equals price so the components
|
|
357
|
-
// print one number instead of a struck-through duplicate.
|
|
358
|
-
mrp: compareAt > price ? compareAt : price,
|
|
359
|
-
img: image,
|
|
360
|
-
description: null,
|
|
361
|
-
storeId: null,
|
|
362
|
-
len: null,
|
|
363
|
-
rank: variant.position ?? 0,
|
|
364
|
-
shippingWeight: null,
|
|
365
|
-
shippingHeight: null,
|
|
366
|
-
shippingLen: null,
|
|
367
|
-
shippingWidth: null,
|
|
368
|
-
shippingCost: null,
|
|
369
|
-
metadata: variant.metadata ?? null,
|
|
370
|
-
variantRank: variant.position ?? 0,
|
|
371
|
-
// A variant's `options` are the VALUES, positionally aligned with the product's option list.
|
|
372
|
-
options: (variant.options || []).map((value, index) => ({
|
|
373
|
-
id: optionValueId(product, index, value),
|
|
374
|
-
optionId: product.options?.[index]?.id ?? index,
|
|
375
|
-
value,
|
|
376
|
-
variantId: variant.id
|
|
377
|
-
}))
|
|
165
|
+
id: str(v.id),
|
|
166
|
+
productId: str(v.product_id),
|
|
167
|
+
// The storefront picks the default variant of an optionless product by
|
|
168
|
+
// this exact title, and hides the variant name when it sees it.
|
|
169
|
+
title: optionless ? "default" : v.label || v.sku,
|
|
170
|
+
sku: v.sku,
|
|
171
|
+
barcode: v.barcode ?? null,
|
|
172
|
+
price: toMajor(v.price),
|
|
173
|
+
mrp: toMajor(v.compare_at_price ?? v.price),
|
|
174
|
+
// The engine's own vocabulary, unchanged: `available` is on hand minus
|
|
175
|
+
// reserved; an untracked variant is never managed, so its count is moot.
|
|
176
|
+
stock: v.active ? v.available : 0,
|
|
177
|
+
manageInventory: v.active ? v.track_inventory : true,
|
|
178
|
+
allowBackorder: v.active && v.continue_selling,
|
|
179
|
+
active: v.active,
|
|
180
|
+
weight: v.weight_grams ?? null,
|
|
181
|
+
length: v.dimensions?.length_mm ?? null,
|
|
182
|
+
width: v.dimensions?.width_mm ?? null,
|
|
183
|
+
height: v.dimensions?.height_mm ?? null,
|
|
184
|
+
images: images.join(","),
|
|
185
|
+
img: images[0] ?? null,
|
|
186
|
+
image: images[0] ?? null,
|
|
187
|
+
thumbnail: images[0] ?? null,
|
|
188
|
+
// Values in option order — `options[i]` answers `p.options[i]`.
|
|
189
|
+
options: arr(v.options).map((value, i) => ({ optionId: str(options[i]?.id), value })),
|
|
190
|
+
metadata: v.metadata ?? null
|
|
378
191
|
};
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const
|
|
383
|
-
const
|
|
384
|
-
const
|
|
385
|
-
const
|
|
386
|
-
const
|
|
387
|
-
const
|
|
388
|
-
const
|
|
389
|
-
const featured = images[0] || null;
|
|
390
|
-
const first = primaryVariant(product);
|
|
192
|
+
}
|
|
193
|
+
function toProduct(p, extra = {}) {
|
|
194
|
+
const live = liveVariants(p);
|
|
195
|
+
const lead = live[0] ?? arr(p.variants)[0];
|
|
196
|
+
const optionless = arr(p.options).length === 0;
|
|
197
|
+
const images = productImages(p);
|
|
198
|
+
const prices = live.map((v) => toMajor(v.price));
|
|
199
|
+
const tracked = live.filter((v) => v.track_inventory);
|
|
200
|
+
const category = p.category;
|
|
201
|
+
const tags = arr(p.tags);
|
|
391
202
|
return {
|
|
392
|
-
id:
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
active:
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
203
|
+
id: str(p.id),
|
|
204
|
+
slug: p.slug,
|
|
205
|
+
title: p.title,
|
|
206
|
+
name: p.title,
|
|
207
|
+
subtitle: null,
|
|
208
|
+
description: p.description ?? "",
|
|
209
|
+
status: p.status === "active" ? "published" : p.status,
|
|
210
|
+
active: p.status === "active",
|
|
211
|
+
type: p.product_type ?? "",
|
|
212
|
+
currency: p.currency,
|
|
213
|
+
currencyCode: p.currency,
|
|
214
|
+
price: lead ? toMajor(lead.price) : 0,
|
|
215
|
+
mrp: lead ? toMajor(lead.compare_at_price ?? lead.price) : 0,
|
|
216
|
+
minPrice: prices.length ? Math.min(...prices) : 0,
|
|
217
|
+
maxPrice: prices.length ? Math.max(...prices) : 0,
|
|
218
|
+
sku: lead?.sku ?? null,
|
|
219
|
+
barcode: lead?.barcode ?? null,
|
|
220
|
+
weight: lead?.weight_grams ?? null,
|
|
221
|
+
originCountry: lead?.origin_country ?? null,
|
|
222
|
+
thumbnail: images[0] ?? null,
|
|
223
|
+
image_url: images[0] ?? null,
|
|
408
224
|
images: images.join(","),
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
replaceAllowed: false,
|
|
436
|
-
originCountry: first?.origin_country || null,
|
|
437
|
-
weightUnit: first?.weight_unit || "g",
|
|
438
|
-
dimensionUnit: "cm",
|
|
439
|
-
metadata: flattenMetadata(product.metadata),
|
|
440
|
-
collectionId: product.collections?.[0]?.id ?? null,
|
|
441
|
-
tags: product.tags || [],
|
|
442
|
-
vendor: product.vendor || null,
|
|
443
|
-
categories: product.category ? [{ category: { id: product.category.id, name: product.category.title, slug: product.category.slug } }] : [],
|
|
444
|
-
createdAt: product.created_at || null,
|
|
445
|
-
updatedAt: product.updated_at || null,
|
|
446
|
-
options: (product.options || []).map((option) => ({
|
|
447
|
-
id: option.id,
|
|
448
|
-
title: option.name,
|
|
449
|
-
type: "radio",
|
|
450
|
-
values: (option.values || []).map((v) => ({ id: v.id, value: v.value }))
|
|
225
|
+
imageList: images,
|
|
226
|
+
// Product-level stock is what a card shows before a variant is picked:
|
|
227
|
+
// buyable when any live variant is, counted only where counting means
|
|
228
|
+
// something.
|
|
229
|
+
manageInventory: live.length > 0 && tracked.length === live.length,
|
|
230
|
+
allowBackorder: live.some((v) => v.continue_selling),
|
|
231
|
+
stock: tracked.reduce((n, v) => n + Math.max(0, v.available), 0),
|
|
232
|
+
hasStock: live.some((v) => !v.track_inventory || v.continue_selling || v.available > 0),
|
|
233
|
+
categoryId: category ? str(category.id) : null,
|
|
234
|
+
category: category ? { id: str(category.id), name: category.title, slug: category.slug } : null,
|
|
235
|
+
categories: category ? [{ category: { id: str(category.id), name: category.title, slug: category.slug } }] : [],
|
|
236
|
+
categoryHierarchy: extra.categoryHierarchy ?? (category ? [{ name: category.title, slug: category.slug }] : []),
|
|
237
|
+
collections: arr(p.collections).map((c) => ({ id: str(c.id), name: c.title, slug: c.slug })),
|
|
238
|
+
// Free text on the engine's product, not a vendor record.
|
|
239
|
+
vendor: p.vendor ? { businessName: p.vendor } : null,
|
|
240
|
+
productTags: tags.join(","),
|
|
241
|
+
keywords: tags.join(","),
|
|
242
|
+
metaTitle: p.seo_title || p.title,
|
|
243
|
+
metaDescription: p.seo_description || null,
|
|
244
|
+
link: `/products/${p.slug}`,
|
|
245
|
+
options: arr(p.options).map((o) => ({
|
|
246
|
+
id: str(o.id),
|
|
247
|
+
title: o.name,
|
|
248
|
+
name: o.name,
|
|
249
|
+
type: "text",
|
|
250
|
+
values: arr(o.values).map((val) => ({ id: str(val.id), value: val.value }))
|
|
451
251
|
})),
|
|
452
|
-
variants:
|
|
252
|
+
variants: live.map((v) => toVariant(v, p, optionless)),
|
|
253
|
+
// Filled by the product page from ext/reviews when it is installed.
|
|
254
|
+
ratings: [],
|
|
255
|
+
rating: null,
|
|
256
|
+
ratingCount: 0,
|
|
257
|
+
reviewCount: 0,
|
|
258
|
+
attributes: [],
|
|
259
|
+
metadata: p.metadata ?? null,
|
|
260
|
+
createdAt: p.created_at,
|
|
261
|
+
updatedAt: p.updated_at
|
|
453
262
|
};
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
|
|
263
|
+
}
|
|
264
|
+
function fromSearchDoc(d) {
|
|
265
|
+
const price = minorToMajor(d.price_minor, d.currency);
|
|
266
|
+
return {
|
|
267
|
+
id: str(d.id),
|
|
268
|
+
slug: d.slug,
|
|
269
|
+
title: d.title,
|
|
270
|
+
name: d.title,
|
|
271
|
+
description: d.description,
|
|
272
|
+
currency: d.currency,
|
|
273
|
+
price,
|
|
274
|
+
mrp: price,
|
|
275
|
+
minPrice: price,
|
|
276
|
+
maxPrice: minorToMajor(d.price_max_minor, d.currency),
|
|
277
|
+
thumbnail: d.image ?? null,
|
|
278
|
+
image_url: d.image ?? null,
|
|
279
|
+
images: d.image ?? "",
|
|
280
|
+
stock: d.in_stock ? 1 : 0,
|
|
281
|
+
hasStock: d.in_stock,
|
|
282
|
+
// The index cannot say how much is left, only whether any is; a card
|
|
283
|
+
// built from it must not claim to count.
|
|
284
|
+
manageInventory: false,
|
|
285
|
+
allowBackorder: false,
|
|
286
|
+
productTags: arr(d.tags).join(","),
|
|
287
|
+
category: d.category ? { name: d.category.split(" / ").pop() ?? d.category, slug: "" } : null,
|
|
288
|
+
vendor: d.vendor ? { businessName: d.vendor } : null,
|
|
289
|
+
variants: [],
|
|
290
|
+
options: [],
|
|
291
|
+
ratings: [],
|
|
292
|
+
link: `/products/${d.slug}`,
|
|
293
|
+
updatedAt: new Date(d.updated_at * 1e3).toISOString()
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function toCategory(c) {
|
|
457
297
|
return {
|
|
458
|
-
id:
|
|
298
|
+
id: str(c.id),
|
|
299
|
+
name: c.title,
|
|
300
|
+
title: c.title,
|
|
301
|
+
slug: c.slug,
|
|
302
|
+
link: `/${c.slug}`,
|
|
303
|
+
parentCategoryId: c.parent_id === null || c.parent_id === void 0 ? null : str(c.parent_id),
|
|
304
|
+
path: c.full_name,
|
|
305
|
+
level: c.depth,
|
|
306
|
+
rank: c.position,
|
|
307
|
+
childCount: c.child_count,
|
|
308
|
+
thumbnail: c.metadata?.image ?? null,
|
|
309
|
+
description: c.metadata?.description ?? null,
|
|
459
310
|
isActive: true,
|
|
460
|
-
isInternal: false,
|
|
461
|
-
isMegamenu: false,
|
|
462
|
-
thumbnail: null,
|
|
463
|
-
path: category.full_name || null,
|
|
464
|
-
level: category.depth ?? 0,
|
|
465
|
-
description: null,
|
|
466
311
|
isFeatured: false,
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
metaTitle: null,
|
|
473
|
-
name: category.title,
|
|
474
|
-
// The storefront's filter composable rebuilds the tree from this field.
|
|
475
|
-
parentCategoryId: category.parent_id ?? null,
|
|
476
|
-
store: null,
|
|
477
|
-
slug: category.slug,
|
|
478
|
-
userId: "",
|
|
479
|
-
activeProducts: 0,
|
|
480
|
-
inactiveProducts: 0,
|
|
481
|
-
createdAt: category.created_at || null,
|
|
482
|
-
updatedAt: category.updated_at || null
|
|
312
|
+
isMegamenu: false,
|
|
313
|
+
metaTitle: c.title,
|
|
314
|
+
children: arr(c.children).map(toCategory),
|
|
315
|
+
createdAt: c.created_at,
|
|
316
|
+
updatedAt: c.updated_at
|
|
483
317
|
};
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
if (!collection) return null;
|
|
487
|
-
const products = collection.products || [];
|
|
318
|
+
}
|
|
319
|
+
function toCollection(c) {
|
|
488
320
|
return {
|
|
489
|
-
id:
|
|
490
|
-
name:
|
|
491
|
-
|
|
492
|
-
|
|
321
|
+
id: str(c.id),
|
|
322
|
+
name: c.title,
|
|
323
|
+
title: c.title,
|
|
324
|
+
slug: c.slug,
|
|
325
|
+
description: c.description || null,
|
|
326
|
+
subTitle: null,
|
|
327
|
+
rank: str(c.position),
|
|
328
|
+
thumbnail: c.metadata?.image ?? null,
|
|
493
329
|
isActive: true,
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
metaDescription: null,
|
|
499
|
-
rank: collection.position ?? 0,
|
|
500
|
-
collectionvalues: products.map((product) => {
|
|
501
|
-
const mapped = mapProduct(product);
|
|
502
|
-
return {
|
|
503
|
-
products: {
|
|
504
|
-
id: product.id,
|
|
505
|
-
name: product.title,
|
|
506
|
-
title: product.title,
|
|
507
|
-
slug: product.slug,
|
|
508
|
-
thumbnail: mapped?.thumbnail || null,
|
|
509
|
-
price: mapped?.price ?? 0,
|
|
510
|
-
mrp: mapped?.mrp ?? 0
|
|
511
|
-
}
|
|
512
|
-
};
|
|
513
|
-
}),
|
|
514
|
-
productCount: products.length,
|
|
515
|
-
createdAt: collection.created_at || null,
|
|
516
|
-
updatedAt: collection.updated_at || null
|
|
330
|
+
metaTitle: c.title,
|
|
331
|
+
metaDescription: c.description || null,
|
|
332
|
+
createdAt: c.created_at,
|
|
333
|
+
updatedAt: c.updated_at
|
|
517
334
|
};
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
const
|
|
522
|
-
const mapped = product ? mapProduct(product) : null;
|
|
523
|
-
const variant = product?.variants?.find((v) => v.id === line.variant_id);
|
|
524
|
-
const price = fromMoney(line.unit_price);
|
|
525
|
-
const compareAt = fromMoney(variant?.compare_at_price);
|
|
526
|
-
const thumbnail = absoluteMedia(variant?.image?.url) || mapped?.thumbnail || null;
|
|
335
|
+
}
|
|
336
|
+
function fromEngineAddress(a, email) {
|
|
337
|
+
if (!a) return null;
|
|
338
|
+
const [firstName, ...rest] = (a.name ?? "").trim().split(/\s+/);
|
|
527
339
|
return {
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
340
|
+
firstName: firstName || "",
|
|
341
|
+
lastName: rest.join(" "),
|
|
342
|
+
email: email ?? null,
|
|
343
|
+
phone: a.phone ?? null,
|
|
344
|
+
address_1: a.line1,
|
|
345
|
+
address_2: a.line2 ?? "",
|
|
346
|
+
city: a.city,
|
|
347
|
+
state: a.state ?? "",
|
|
348
|
+
zip: a.postal_code,
|
|
349
|
+
country: a.country,
|
|
350
|
+
countryCode: a.country
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
function toEngineAddress(a) {
|
|
354
|
+
const name = [a.firstName, a.lastName].filter(Boolean).join(" ").trim();
|
|
355
|
+
return {
|
|
356
|
+
...name ? { name } : {},
|
|
357
|
+
...a.phone ? { phone: String(a.phone) } : {},
|
|
358
|
+
line1: String(a.address_1 ?? "").trim(),
|
|
359
|
+
...a.address_2 || a.locality ? { line2: [a.address_2, a.locality].filter(Boolean).join(", ") } : {},
|
|
360
|
+
city: String(a.city ?? "").trim(),
|
|
361
|
+
...a.state ? { state: String(a.state) } : {},
|
|
362
|
+
postal_code: String(a.zip ?? "").trim(),
|
|
363
|
+
country: String(a.countryCode || a.country || "").trim().toUpperCase()
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
function fromIdentityAddress(a, email) {
|
|
367
|
+
const [firstName, ...rest] = (a.name ?? "").trim().split(/\s+/);
|
|
368
|
+
return {
|
|
369
|
+
id: str(a.id),
|
|
370
|
+
firstName: firstName || "",
|
|
371
|
+
lastName: rest.join(" "),
|
|
372
|
+
email: email ?? null,
|
|
373
|
+
phone: a.phone || null,
|
|
374
|
+
address_1: a.line1,
|
|
375
|
+
address_2: a.line2,
|
|
376
|
+
city: a.city,
|
|
377
|
+
state: a.state,
|
|
378
|
+
zip: a.postal_code,
|
|
379
|
+
country: a.country,
|
|
380
|
+
countryCode: a.country,
|
|
381
|
+
label: a.label,
|
|
382
|
+
isPrimary: a.is_default,
|
|
383
|
+
createdAt: a.created_at,
|
|
384
|
+
updatedAt: a.updated_at
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
function toIdentityAddress(a) {
|
|
388
|
+
const e = toEngineAddress(a);
|
|
389
|
+
return {
|
|
390
|
+
name: e.name ?? "",
|
|
391
|
+
phone: e.phone ?? "",
|
|
392
|
+
line1: e.line1,
|
|
393
|
+
line2: e.line2 ?? "",
|
|
394
|
+
city: e.city,
|
|
395
|
+
state: e.state ?? "",
|
|
396
|
+
postal_code: e.postal_code,
|
|
397
|
+
country: e.country,
|
|
398
|
+
...a.label ? { label: String(a.label) } : {},
|
|
399
|
+
...a.isPrimary ? { is_default: true } : {}
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
function toCartLine(l, extra = {}) {
|
|
403
|
+
return {
|
|
404
|
+
id: str(l.id),
|
|
405
|
+
productId: str(l.product_id),
|
|
406
|
+
variantId: str(l.variant_id),
|
|
407
|
+
sku: l.sku,
|
|
408
|
+
title: l.title,
|
|
409
|
+
name: l.title,
|
|
410
|
+
slug: extra.slug ?? "",
|
|
411
|
+
thumbnail: extra.thumbnail ?? null,
|
|
412
|
+
variantTitle: l.variant_label ?? "",
|
|
413
|
+
qty: l.quantity,
|
|
414
|
+
price: toMajor(l.unit_price),
|
|
415
|
+
mrp: toMajor(l.unit_price),
|
|
416
|
+
subtotal: toMajor(l.total),
|
|
417
|
+
total: toMajor(l.total),
|
|
418
|
+
inStock: l.in_stock,
|
|
419
|
+
priceChanged: l.price_changed,
|
|
420
|
+
currentPrice: toMajor(l.current_price),
|
|
421
|
+
// The storefront's review step lists only lines marked for checkout. The
|
|
422
|
+
// engine checks a cart out whole, so every line is.
|
|
555
423
|
isSelectedForCheckout: true,
|
|
556
|
-
// Surfaced so the cart can warn, instead of letting checkout refuse the whole order later.
|
|
557
|
-
priceChanged: Boolean(line.price_changed),
|
|
558
|
-
inStock: line.in_stock !== false,
|
|
559
|
-
createdAt: null,
|
|
560
|
-
product: {
|
|
561
|
-
id: line.product_id,
|
|
562
|
-
title: line.title || mapped?.title || "",
|
|
563
|
-
thumbnail,
|
|
564
|
-
slug: mapped?.slug || "",
|
|
565
|
-
sku: line.sku || "",
|
|
566
|
-
categories: mapped?.categories || []
|
|
567
|
-
},
|
|
568
424
|
variant: {
|
|
569
|
-
id:
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
height: null,
|
|
574
|
-
width: null,
|
|
575
|
-
len: null,
|
|
576
|
-
shippingWeight: null,
|
|
577
|
-
shippingHeight: null,
|
|
578
|
-
shippingLen: null,
|
|
579
|
-
shippingWidth: null,
|
|
580
|
-
sku: line.sku || "",
|
|
581
|
-
title: line.variant_label || variant?.label || "default",
|
|
582
|
-
options: variant && product ? mapVariant(product, variant).options : []
|
|
583
|
-
}
|
|
425
|
+
id: str(l.variant_id),
|
|
426
|
+
options: l.variant_label ? l.variant_label.split(" / ").map((value) => ({ option: { title: "" }, value })) : []
|
|
427
|
+
},
|
|
428
|
+
product: { id: str(l.product_id), categories: [] }
|
|
584
429
|
};
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
const currency =
|
|
589
|
-
const
|
|
590
|
-
const
|
|
591
|
-
const
|
|
592
|
-
const
|
|
430
|
+
}
|
|
431
|
+
function toCart(c, extras = {}, lineExtra = () => ({})) {
|
|
432
|
+
const lines = c.status === "converted" ? [] : arr(c.line_items);
|
|
433
|
+
const currency = c.currency;
|
|
434
|
+
const subtotalMinor = lines.reduce((n, l) => n + l.total.amount_minor, 0);
|
|
435
|
+
const discountMinor = extras.discount && !extras.discount.free_shipping ? extras.discount.amount_minor : 0;
|
|
436
|
+
const shippingMinor = extras.discount?.free_shipping ? 0 : extras.shippingRate?.minor ?? 0;
|
|
437
|
+
const totalMinor = Math.max(0, subtotalMinor - discountMinor) + shippingMinor;
|
|
438
|
+
const money = (minor) => minorToMajor(minor, currency);
|
|
439
|
+
const ship = extras.shippingAddress ?? null;
|
|
593
440
|
return {
|
|
594
|
-
id:
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
441
|
+
id: c.id,
|
|
442
|
+
status: c.status,
|
|
443
|
+
email: c.email ?? ship?.email ?? null,
|
|
444
|
+
phone: extras.phone ?? ship?.phone ?? null,
|
|
445
|
+
name: ship ? [ship.firstName, ship.lastName].filter(Boolean).join(" ") : null,
|
|
446
|
+
lineItems: lines.map((l) => toCartLine(l, lineExtra(l))),
|
|
447
|
+
qty: lines.reduce((n, l) => n + l.quantity, 0),
|
|
448
|
+
currencyCode: currency,
|
|
449
|
+
currency,
|
|
450
|
+
subtotal: money(subtotalMinor),
|
|
451
|
+
discountAmount: money(discountMinor),
|
|
452
|
+
savingAmount: money(discountMinor),
|
|
453
|
+
couponCode: extras.discount?.code ?? null,
|
|
454
|
+
discount: extras.discount ?? null,
|
|
455
|
+
shippingCharges: money(shippingMinor),
|
|
456
|
+
shippingRateId: extras.shippingRateId ?? null,
|
|
457
|
+
shippingMethod: extras.shippingRate?.name ?? null,
|
|
458
|
+
tax: 0,
|
|
459
|
+
taxIncluded: false,
|
|
460
|
+
codCharges: 0,
|
|
461
|
+
total: money(totalMinor),
|
|
462
|
+
shippingAddress: ship,
|
|
463
|
+
shippingAddressId: ship ? ship.id && ship.id !== "new" ? String(ship.id) : "cart" : null,
|
|
464
|
+
billingAddress: extras.billingAddress ?? ship,
|
|
465
|
+
billingAddressId: extras.billingAddress ? extras.billingAddress.id && extras.billingAddress.id !== "new" ? String(extras.billingAddress.id) : "cart-billing" : ship ? ship.id && ship.id !== "new" ? String(ship.id) : "cart" : null,
|
|
466
|
+
paymentMethod: null,
|
|
467
|
+
needAddress: lines.length > 0,
|
|
468
|
+
completedAt: c.status === "converted" ? c.updated_at : null,
|
|
469
|
+
createdAt: c.created_at,
|
|
470
|
+
updatedAt: c.updated_at,
|
|
471
|
+
expiresAt: c.expires_at
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
function emptyCart(currency = "USD") {
|
|
475
|
+
return {
|
|
476
|
+
id: "",
|
|
477
|
+
status: "open",
|
|
478
|
+
email: null,
|
|
479
|
+
phone: null,
|
|
480
|
+
lineItems: [],
|
|
481
|
+
qty: 0,
|
|
482
|
+
currencyCode: currency,
|
|
483
|
+
subtotal: 0,
|
|
605
484
|
discountAmount: 0,
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
isCodAvailable: true,
|
|
609
|
-
paymentId: null,
|
|
610
|
-
type: null,
|
|
611
|
-
completedAt: cart.status === "converted" ? cart.updated_at || null : null,
|
|
612
|
-
paymentAuthorizedAt: null,
|
|
613
|
-
idempotencyKey: null,
|
|
614
|
-
salesChannelId: null,
|
|
615
|
-
qty: cart.item_count ?? lines.reduce((sum, l) => sum + (l.quantity || 0), 0),
|
|
616
|
-
// GoCommerce prices shipping at checkout, not on the cart, so there is nothing honest to put
|
|
617
|
-
// here yet — and a fabricated number is the one the shopper would check the total against.
|
|
485
|
+
savingAmount: 0,
|
|
486
|
+
couponCode: null,
|
|
618
487
|
shippingCharges: 0,
|
|
619
|
-
paymentMethod: null,
|
|
620
|
-
shippingMethod: null,
|
|
621
|
-
subtotal,
|
|
622
|
-
codCharges: 0,
|
|
623
488
|
tax: 0,
|
|
624
|
-
total:
|
|
625
|
-
|
|
626
|
-
|
|
489
|
+
total: 0,
|
|
490
|
+
shippingAddress: null,
|
|
491
|
+
shippingAddressId: null,
|
|
492
|
+
billingAddress: null,
|
|
493
|
+
billingAddressId: null
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
var STATUS = {
|
|
497
|
+
pending: "Pending",
|
|
498
|
+
confirmed: "Confirmed",
|
|
499
|
+
partial: "Partially shipped",
|
|
500
|
+
shipped: "Shipped",
|
|
501
|
+
delivered: "Delivered",
|
|
502
|
+
cancelled: "Cancelled"
|
|
503
|
+
};
|
|
504
|
+
var PAYMENT_STATUS = {
|
|
505
|
+
pending: "Pending",
|
|
506
|
+
paid: "Paid",
|
|
507
|
+
failed: "Failed",
|
|
508
|
+
refunded: "Refunded"
|
|
509
|
+
};
|
|
510
|
+
function toOrderLine(l, currency) {
|
|
511
|
+
return {
|
|
512
|
+
id: str(l.id),
|
|
513
|
+
productId: str(l.product_id),
|
|
514
|
+
variantId: str(l.variant_id),
|
|
515
|
+
sku: l.sku,
|
|
516
|
+
title: l.title,
|
|
517
|
+
name: l.title,
|
|
518
|
+
slug: "",
|
|
519
|
+
thumbnail: l.image_url ?? null,
|
|
520
|
+
img: l.image_url ?? null,
|
|
521
|
+
variantTitle: l.variant_label ?? "",
|
|
522
|
+
qty: l.quantity,
|
|
523
|
+
shippedQty: l.shipped_quantity,
|
|
524
|
+
price: toMajor(l.unit_price),
|
|
525
|
+
mrp: toMajor(l.unit_price),
|
|
526
|
+
subtotal: toMajor(l.total),
|
|
527
|
+
total: toMajor(l.total),
|
|
627
528
|
currencyCode: currency,
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
storeId: store?.id ?? null,
|
|
632
|
-
status: cart.status || "open",
|
|
633
|
-
createdAt: cart.created_at || null,
|
|
634
|
-
updatedAt: cart.updated_at || null,
|
|
635
|
-
deletedAt: null,
|
|
636
|
-
lineItems: lines.map((line) => mapCartLine(line, index)),
|
|
637
|
-
shippingAddress,
|
|
638
|
-
billingAddress
|
|
529
|
+
status: l.shipped_quantity >= l.quantity ? "Shipped" : "Pending",
|
|
530
|
+
files: [],
|
|
531
|
+
usedOptions: []
|
|
639
532
|
};
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
};
|
|
645
|
-
var toStorefrontAddress = (address, email, phone) => {
|
|
646
|
-
if (!address) return null;
|
|
647
|
-
const { firstName, lastName } = splitName(address.name);
|
|
533
|
+
}
|
|
534
|
+
function toOrder(o) {
|
|
535
|
+
const address = fromEngineAddress(o.address, o.email);
|
|
536
|
+
const code = (o.discounts ?? []).find((d) => d.code)?.code;
|
|
648
537
|
return {
|
|
649
|
-
id:
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
538
|
+
id: str(o.id),
|
|
539
|
+
orderNo: o.number,
|
|
540
|
+
orderNumber: o.number,
|
|
541
|
+
number: o.number,
|
|
542
|
+
// The storefront links an order by this; the engine has no sub-orders,
|
|
543
|
+
// so an order is its own parent.
|
|
544
|
+
parentOrderNo: o.number,
|
|
545
|
+
status: STATUS[o.status] ?? o.status,
|
|
546
|
+
engineStatus: o.status,
|
|
547
|
+
paymentStatus: PAYMENT_STATUS[o.payment_status] ?? o.payment_status,
|
|
548
|
+
paymentMethod: o.payment_provider.toUpperCase(),
|
|
549
|
+
currencyCode: o.currency,
|
|
550
|
+
subtotal: toMajor(o.subtotal),
|
|
551
|
+
shippingCharges: toMajor(o.shipping),
|
|
552
|
+
discount: toMajor(o.discount),
|
|
553
|
+
tax: toMajor(o.tax),
|
|
554
|
+
taxIncluded: o.tax_inclusive,
|
|
555
|
+
total: toMajor(o.total),
|
|
556
|
+
refunded: toMajor(o.refunded),
|
|
557
|
+
codCharges: 0,
|
|
558
|
+
coupon: code ? { code } : null,
|
|
559
|
+
userEmail: o.email,
|
|
560
|
+
email: o.email,
|
|
561
|
+
phone: o.phone ?? null,
|
|
562
|
+
shippingAddress: address,
|
|
563
|
+
billingAddress: address,
|
|
564
|
+
shippingMethod: o.shipping_method ?? null,
|
|
565
|
+
shippingRate: o.shipping_method ? { name: o.shipping_method } : null,
|
|
566
|
+
lineItems: arr(o.line_items).map((l) => toOrderLine(l, o.currency)),
|
|
567
|
+
fulfillments: arr(o.fulfillments).map((f) => ({
|
|
568
|
+
id: str(f.id),
|
|
569
|
+
status: f.status,
|
|
570
|
+
carrier: f.carrier_name || f.carrier || f.provider,
|
|
571
|
+
trackingNumber: f.tracking ?? null,
|
|
572
|
+
trackingUrl: f.tracking_url ?? null,
|
|
573
|
+
lineItems: arr(f.lines).map((l) => ({ id: str(l.order_line_id), title: l.title, qty: l.quantity })),
|
|
574
|
+
createdAt: f.created_at
|
|
575
|
+
})),
|
|
576
|
+
tracking: [],
|
|
577
|
+
orderHistory: [],
|
|
578
|
+
isReplaceOrReturn: false,
|
|
579
|
+
createdAt: o.created_at,
|
|
580
|
+
updatedAt: o.updated_at
|
|
664
581
|
};
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
if (!address) return null;
|
|
668
|
-
const name = [address.firstName, address.lastName].filter(Boolean).join(" ").trim() || address.name || "";
|
|
669
|
-
const line1 = address.address_1 || address.line1 || "";
|
|
670
|
-
const city = address.city || "";
|
|
671
|
-
const postal = address.zip || address.postal_code || "";
|
|
672
|
-
const country = address.countryCode || address.country_code || address.country || "";
|
|
673
|
-
if (!line1 || !city || !postal || !country) return null;
|
|
582
|
+
}
|
|
583
|
+
function toRating(r) {
|
|
674
584
|
return {
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
585
|
+
id: str(r.id),
|
|
586
|
+
rating: r.rating,
|
|
587
|
+
name: r.name,
|
|
588
|
+
title: r.title ?? "",
|
|
589
|
+
review: r.body,
|
|
590
|
+
message: r.body,
|
|
591
|
+
reply: r.reply ?? null,
|
|
592
|
+
verified: r.verified,
|
|
593
|
+
img: "",
|
|
594
|
+
createdAt: r.created_at
|
|
684
595
|
};
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
if (!order) return null;
|
|
688
|
-
const shippingAddress = toStorefrontAddress(order.address, order.email, order.phone);
|
|
596
|
+
}
|
|
597
|
+
function toCmsPage(p) {
|
|
689
598
|
return {
|
|
690
|
-
id:
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
discount: fromMoney(order.discount),
|
|
705
|
-
total: fromMoney(order.total),
|
|
706
|
-
currencyCode: order.currency,
|
|
707
|
-
currencyDecimalDigits: currencyFractionDigits(order.currency),
|
|
708
|
-
shippingRate: null,
|
|
709
|
-
fulfillments: (order.fulfillments || []).map((f) => ({
|
|
710
|
-
id: f.id,
|
|
711
|
-
status: f.status || "",
|
|
712
|
-
provider: f.provider || "",
|
|
713
|
-
trackingNumber: f.tracking || "",
|
|
714
|
-
labelUrl: f.label_url || "",
|
|
715
|
-
createdAt: f.created_at || null
|
|
716
|
-
})),
|
|
717
|
-
shippingAddress,
|
|
718
|
-
billingAddress: shippingAddress,
|
|
719
|
-
lineItems: (order.line_items || []).map((line) => {
|
|
720
|
-
const product = lineProduct(index, line.product_id);
|
|
721
|
-
const mapped = product ? mapProduct(product) : null;
|
|
722
|
-
const variant = product?.variants?.find((v) => v.id === line.variant_id);
|
|
723
|
-
return {
|
|
724
|
-
id: line.id,
|
|
725
|
-
productId: line.product_id,
|
|
726
|
-
variantId: line.variant_id,
|
|
727
|
-
title: line.title || mapped?.title || "",
|
|
728
|
-
variantTitle: line.variant_label || line.sku || "",
|
|
729
|
-
slug: mapped?.slug || "",
|
|
730
|
-
sku: line.sku || "",
|
|
731
|
-
thumbnail: absoluteMedia(variant?.image?.url) || mapped?.thumbnail || null,
|
|
732
|
-
qty: line.quantity,
|
|
733
|
-
price: fromMoney(line.unit_price),
|
|
734
|
-
// The orders list prints `total`; the confirmation page prints `subtotal`.
|
|
735
|
-
subtotal: fromMoney(line.total),
|
|
736
|
-
total: fromMoney(line.total)
|
|
737
|
-
};
|
|
738
|
-
})
|
|
599
|
+
id: str(p.id),
|
|
600
|
+
slug: p.slug,
|
|
601
|
+
name: p.title,
|
|
602
|
+
title: p.title,
|
|
603
|
+
content: p.body,
|
|
604
|
+
excerpt: p.excerpt ?? null,
|
|
605
|
+
layouts: [],
|
|
606
|
+
language: p.language,
|
|
607
|
+
metaTitle: p.seo?.title || p.title,
|
|
608
|
+
metaDescription: p.seo?.description || p.excerpt || null,
|
|
609
|
+
metaKeywords: p.seo?.keywords || null,
|
|
610
|
+
publishedAt: p.published_at ?? null,
|
|
611
|
+
createdAt: p.created_at,
|
|
612
|
+
updatedAt: p.updated_at
|
|
739
613
|
};
|
|
740
|
-
}
|
|
614
|
+
}
|
|
615
|
+
function toVendor(v) {
|
|
616
|
+
return {
|
|
617
|
+
id: str(v.id),
|
|
618
|
+
slug: v.slug,
|
|
619
|
+
name: v.name,
|
|
620
|
+
businessName: v.name,
|
|
621
|
+
about: v.about,
|
|
622
|
+
description: v.about,
|
|
623
|
+
website: v.website,
|
|
624
|
+
address: v.address,
|
|
625
|
+
banners: [],
|
|
626
|
+
rating: null
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
function toUser(c) {
|
|
630
|
+
const [firstName, ...rest] = (c.name ?? "").trim().split(/\s+/);
|
|
631
|
+
return {
|
|
632
|
+
id: str(c.id),
|
|
633
|
+
userId: str(c.id),
|
|
634
|
+
email: c.email,
|
|
635
|
+
emailVerified: c.email_verified,
|
|
636
|
+
phone: c.phone || null,
|
|
637
|
+
firstName: firstName || "",
|
|
638
|
+
lastName: rest.join(" "),
|
|
639
|
+
name: c.name,
|
|
640
|
+
fullName: c.name,
|
|
641
|
+
avatar: null,
|
|
642
|
+
role: "USER",
|
|
643
|
+
createdAt: c.created_at,
|
|
644
|
+
updatedAt: c.updated_at
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
var priceOf = (m) => m ? toMajor(m) : 0;
|
|
741
648
|
|
|
742
|
-
// src/
|
|
743
|
-
var
|
|
744
|
-
|
|
745
|
-
|
|
649
|
+
// src/browser.ts
|
|
650
|
+
var inBrowser = () => typeof window !== "undefined" && typeof document !== "undefined";
|
|
651
|
+
function readLocal(key) {
|
|
652
|
+
if (!inBrowser()) return null;
|
|
746
653
|
try {
|
|
747
|
-
return
|
|
654
|
+
return window.localStorage.getItem(key);
|
|
748
655
|
} catch {
|
|
749
|
-
return
|
|
656
|
+
return null;
|
|
750
657
|
}
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
if (!store) return void 0;
|
|
658
|
+
}
|
|
659
|
+
function writeLocal(key, value) {
|
|
660
|
+
if (!inBrowser()) return;
|
|
755
661
|
try {
|
|
756
|
-
|
|
757
|
-
return raw ? JSON.parse(raw) : void 0;
|
|
662
|
+
window.localStorage.setItem(key, value);
|
|
758
663
|
} catch {
|
|
759
|
-
return void 0;
|
|
760
664
|
}
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
if (!store) return;
|
|
665
|
+
}
|
|
666
|
+
function removeLocal(key) {
|
|
667
|
+
if (!inBrowser()) return;
|
|
765
668
|
try {
|
|
766
|
-
|
|
669
|
+
window.localStorage.removeItem(key);
|
|
767
670
|
} catch {
|
|
768
671
|
}
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
};
|
|
774
|
-
var writeCheckoutDetails = (cartId, patch) => {
|
|
775
|
-
if (!cartId) return patch;
|
|
776
|
-
const merged = { ...readCheckoutDetails(cartId), ...patch };
|
|
777
|
-
writeJson(`${DETAILS_PREFIX}${cartId}`, merged);
|
|
778
|
-
return merged;
|
|
779
|
-
};
|
|
780
|
-
var clearCheckoutDetails = (cartId) => {
|
|
781
|
-
const store = storage();
|
|
782
|
-
if (!store || !cartId) return;
|
|
672
|
+
}
|
|
673
|
+
function readJSON(key) {
|
|
674
|
+
const raw = readLocal(key);
|
|
675
|
+
if (!raw) return null;
|
|
783
676
|
try {
|
|
784
|
-
|
|
677
|
+
return JSON.parse(raw);
|
|
785
678
|
} catch {
|
|
679
|
+
return null;
|
|
786
680
|
}
|
|
787
|
-
}
|
|
788
|
-
var
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
681
|
+
}
|
|
682
|
+
var YEAR = 60 * 60 * 24 * 365;
|
|
683
|
+
function setCookie(name, value, maxAge = YEAR) {
|
|
684
|
+
if (!inBrowser()) return;
|
|
685
|
+
document.cookie = `${name}=${value}; path=/; max-age=${maxAge}; samesite=lax`;
|
|
686
|
+
}
|
|
687
|
+
function clearCookie(name) {
|
|
688
|
+
if (!inBrowser()) return;
|
|
689
|
+
document.cookie = `${name}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`;
|
|
690
|
+
}
|
|
691
|
+
function readCookie(name) {
|
|
692
|
+
if (!inBrowser()) return null;
|
|
693
|
+
for (const pair of document.cookie.split(";")) {
|
|
694
|
+
const at = pair.indexOf("=");
|
|
695
|
+
if (at < 0) continue;
|
|
696
|
+
if (pair.slice(0, at).trim() === name) return pair.slice(at + 1).trim();
|
|
697
|
+
}
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
var TOKEN = "gocommerce_token";
|
|
701
|
+
var sessionToken = () => readLocal(TOKEN);
|
|
702
|
+
function toMe(c) {
|
|
703
|
+
const [firstName, ...rest] = (c.name ?? "").trim().split(/\s+/);
|
|
704
|
+
return {
|
|
705
|
+
id: String(c.id),
|
|
706
|
+
userId: String(c.id),
|
|
707
|
+
email: c.email,
|
|
708
|
+
phone: c.phone || null,
|
|
709
|
+
firstName: firstName || "",
|
|
710
|
+
lastName: rest.join(" "),
|
|
711
|
+
name: c.name ?? "",
|
|
712
|
+
fullName: c.name ?? "",
|
|
713
|
+
avatar: null,
|
|
714
|
+
role: "USER",
|
|
715
|
+
emailVerified: c.email_verified,
|
|
716
|
+
storeId: null
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
function startSession(auth) {
|
|
720
|
+
writeLocal(TOKEN, auth.token);
|
|
721
|
+
const me = toMe(auth.record);
|
|
722
|
+
const expires = Math.max(60, Math.floor((Date.parse(auth.expires_at) - Date.now()) / 1e3)) || YEAR;
|
|
723
|
+
setCookie("me", encodeURIComponent(JSON.stringify(me)), expires);
|
|
724
|
+
setCookie("connect.sid", "gocommerce", expires);
|
|
725
|
+
return me;
|
|
726
|
+
}
|
|
727
|
+
function updateMe(c) {
|
|
728
|
+
const me = toMe(c);
|
|
729
|
+
setCookie("me", encodeURIComponent(JSON.stringify(me)));
|
|
730
|
+
return me;
|
|
731
|
+
}
|
|
732
|
+
function endSession() {
|
|
733
|
+
removeLocal(TOKEN);
|
|
734
|
+
clearCookie("me");
|
|
735
|
+
clearCookie("connect.sid");
|
|
736
|
+
}
|
|
737
|
+
function currentMe() {
|
|
738
|
+
const raw = readCookie("me");
|
|
739
|
+
if (!raw) return null;
|
|
803
740
|
try {
|
|
804
|
-
return
|
|
741
|
+
return JSON.parse(decodeURIComponent(raw));
|
|
805
742
|
} catch {
|
|
806
|
-
return
|
|
743
|
+
return null;
|
|
807
744
|
}
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
const
|
|
811
|
-
if (!
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
745
|
+
}
|
|
746
|
+
function storedCartId() {
|
|
747
|
+
const v = readLocal("cart_id");
|
|
748
|
+
if (!v || v === "undefined" || v === "null") return null;
|
|
749
|
+
return v;
|
|
750
|
+
}
|
|
751
|
+
var rememberCartId = (id) => writeLocal("cart_id", id);
|
|
752
|
+
var extrasKey = (cartId) => `gocommerce_cart_${cartId}`;
|
|
753
|
+
var cartExtras = (cartId) => readJSON(extrasKey(cartId)) ?? {};
|
|
754
|
+
function saveCartExtras(cartId, patch) {
|
|
755
|
+
const next = { ...cartExtras(cartId), ...patch };
|
|
756
|
+
writeLocal(extrasKey(cartId), JSON.stringify(next));
|
|
757
|
+
return next;
|
|
758
|
+
}
|
|
759
|
+
var forgetCartExtras = (cartId) => removeLocal(extrasKey(cartId));
|
|
760
|
+
var ORDERS = "gocommerce_orders";
|
|
761
|
+
var placedOrders = () => readJSON(ORDERS) ?? [];
|
|
762
|
+
function rememberOrder(o) {
|
|
763
|
+
const kept = placedOrders().filter((p) => p.number !== o.number);
|
|
764
|
+
writeLocal(ORDERS, JSON.stringify([o, ...kept].slice(0, 50)));
|
|
765
|
+
}
|
|
766
|
+
var placedOrder = (number) => placedOrders().find((p) => p.number === number);
|
|
767
|
+
var placedOrderForCart = (cartId) => placedOrders().find((p) => p.cartId === cartId);
|
|
768
|
+
var placedOrderByReference = (reference) => placedOrders().find((p) => p.reference === reference);
|
|
769
|
+
var WISHLIST = "gocommerce_wishlist";
|
|
770
|
+
var wishlistToken = () => readLocal(WISHLIST);
|
|
771
|
+
var rememberWishlistToken = (token) => writeLocal(WISHLIST, token);
|
|
772
|
+
var forgetWishlistToken = () => removeLocal(WISHLIST);
|
|
773
|
+
|
|
774
|
+
// src/services/base.service.ts
|
|
775
|
+
var BaseService = class _BaseService {
|
|
776
|
+
static _credentials = { apiUrl: "" };
|
|
777
|
+
_fetch;
|
|
778
|
+
constructor(fetchFn) {
|
|
779
|
+
this._fetch = fetchFn ?? ((input, init) => globalThis.fetch(input, init));
|
|
818
780
|
}
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
const store = storage2();
|
|
822
|
-
if (!store) return;
|
|
823
|
-
try {
|
|
824
|
-
store.setItem(ADDRESSES_KEY, JSON.stringify(addresses));
|
|
825
|
-
} catch {
|
|
781
|
+
static setCredentials(creds) {
|
|
782
|
+
_BaseService._credentials = { ..._BaseService._credentials, ...creds };
|
|
826
783
|
}
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
var newId = () => `addr_${Math.random().toString(36).slice(2, 10)}${Date.now().toString(36)}`;
|
|
830
|
-
var listAddresses = () => readAll();
|
|
831
|
-
var findAddress = (id) => readAll().find((a) => a.id === id);
|
|
832
|
-
var putAddress = (address) => {
|
|
833
|
-
const addresses = readAll();
|
|
834
|
-
const id = isSavedId(address.id) ? address.id : newId();
|
|
835
|
-
const saved = { ...address, id };
|
|
836
|
-
const index = addresses.findIndex((a) => a.id === id);
|
|
837
|
-
if (index >= 0) addresses[index] = saved;
|
|
838
|
-
else addresses.push(saved);
|
|
839
|
-
writeAll(addresses);
|
|
840
|
-
return saved;
|
|
841
|
-
};
|
|
842
|
-
var patchAddress = (id, patch) => {
|
|
843
|
-
const existing = findAddress(id);
|
|
844
|
-
if (!existing) return void 0;
|
|
845
|
-
return putAddress({ ...existing, ...patch, id });
|
|
846
|
-
};
|
|
847
|
-
var removeAddress = (id) => {
|
|
848
|
-
writeAll(readAll().filter((a) => a.id !== id));
|
|
849
|
-
};
|
|
850
|
-
var SESSION_COOKIES = ["connect.sid", "me"];
|
|
851
|
-
var clearPhantomSession = () => {
|
|
852
|
-
if (typeof document === "undefined") return;
|
|
853
|
-
for (const name of SESSION_COOKIES) {
|
|
854
|
-
document.cookie = `${name}=; Max-Age=0; path=/`;
|
|
855
|
-
document.cookie = `${name}=; Max-Age=0`;
|
|
784
|
+
static getCredentials() {
|
|
785
|
+
return _BaseService._credentials;
|
|
856
786
|
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
// src/services/catalog.ts
|
|
861
|
-
var productCache = /* @__PURE__ */ new Map();
|
|
862
|
-
var categorySlugById = /* @__PURE__ */ new Map();
|
|
863
|
-
var remember = (products) => {
|
|
864
|
-
for (const product of products) {
|
|
865
|
-
if (product?.id != null) productCache.set(product.id, product);
|
|
866
|
-
if (product?.category?.id != null && product.category.slug) categorySlugById.set(product.category.id, product.category.slug);
|
|
867
|
-
}
|
|
868
|
-
return products;
|
|
869
|
-
};
|
|
870
|
-
var rememberCategories = (categories) => {
|
|
871
|
-
for (const category of categories) {
|
|
872
|
-
if (category?.id != null && category.slug) categorySlugById.set(category.id, category.slug);
|
|
873
|
-
if (category?.children?.length) rememberCategories(category.children);
|
|
787
|
+
get creds() {
|
|
788
|
+
return _BaseService._credentials;
|
|
874
789
|
}
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
return
|
|
896
|
-
}
|
|
897
|
-
/**
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
790
|
+
/**
|
|
791
|
+
* The engine's base URL from wherever this code runs. In the browser a
|
|
792
|
+
* configured proxy path wins, because the engine answers no CORS preflight.
|
|
793
|
+
*/
|
|
794
|
+
engineBase() {
|
|
795
|
+
const { apiUrl, proxyPath } = this.creds;
|
|
796
|
+
if (proxyPath && typeof window !== "undefined" && window.location) {
|
|
797
|
+
return new URL(proxyPath, window.location.origin).href.replace(/\/+$/, "");
|
|
798
|
+
}
|
|
799
|
+
const url = (apiUrl ?? "").trim();
|
|
800
|
+
if (!url) {
|
|
801
|
+
throw new GoCommerceError(
|
|
802
|
+
"PUBLIC_GOCOMMERCE_API_URL is not set, so this storefront does not know where its commerce engine is. Point it at a running GoCommerce, e.g. http://127.0.0.1:8080.",
|
|
803
|
+
0
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
return url.replace(/\/+$/, "");
|
|
807
|
+
}
|
|
808
|
+
/** An absolute engine URL for a path and query, without sending anything. */
|
|
809
|
+
engineURL(path, query) {
|
|
810
|
+
return this.engineBase() + path + qs(query);
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* One request to the engine, envelope and all.
|
|
814
|
+
*
|
|
815
|
+
* The body is read as text first, so a proxy's HTML error page is reported
|
|
816
|
+
* as what it is rather than as a JSON parse error three frames away.
|
|
817
|
+
*/
|
|
818
|
+
async engine(path, req = {}) {
|
|
819
|
+
const url = this.engineURL(path, req.query);
|
|
820
|
+
const headers = { Accept: "application/json", ...req.headers };
|
|
821
|
+
if (req.body !== void 0) headers["Content-Type"] = "application/json";
|
|
822
|
+
if (this.creds.store) headers["X-Store"] = this.creds.store;
|
|
823
|
+
if (req.auth) {
|
|
824
|
+
const token = sessionToken();
|
|
825
|
+
if (!token) throw new GoCommerceError("Please sign in to continue.", 401, "unauthorized");
|
|
826
|
+
headers.Authorization = `Bearer ${token}`;
|
|
827
|
+
}
|
|
828
|
+
const init = {
|
|
829
|
+
method: req.method ?? "GET",
|
|
830
|
+
headers,
|
|
831
|
+
body: req.body === void 0 ? void 0 : JSON.stringify(req.body)
|
|
832
|
+
};
|
|
833
|
+
let res;
|
|
901
834
|
try {
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
835
|
+
res = await this._fetch.call(globalThis, url, init);
|
|
836
|
+
} catch (cause) {
|
|
837
|
+
if (typeof window === "undefined" && /CORS/i.test(String(cause?.message))) {
|
|
838
|
+
try {
|
|
839
|
+
res = await globalThis.fetch(url, init);
|
|
840
|
+
} catch (again) {
|
|
841
|
+
throw unreachable(this.engineBase(), again);
|
|
842
|
+
}
|
|
843
|
+
} else {
|
|
844
|
+
throw unreachable(this.engineBase(), cause);
|
|
845
|
+
}
|
|
907
846
|
}
|
|
847
|
+
return this.unwrap(res);
|
|
908
848
|
}
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
849
|
+
/** The envelope opened: the payload, or the engine's error as a GoCommerceError. */
|
|
850
|
+
async unwrap(res) {
|
|
851
|
+
const text = await res.text();
|
|
852
|
+
let parsed = null;
|
|
853
|
+
if (text) {
|
|
854
|
+
try {
|
|
855
|
+
parsed = JSON.parse(text);
|
|
856
|
+
} catch {
|
|
857
|
+
throw new GoCommerceError(
|
|
858
|
+
`The commerce engine answered ${res.status} with something that is not JSON: ${text.slice(0, 120)}`,
|
|
859
|
+
res.status
|
|
860
|
+
);
|
|
861
|
+
}
|
|
920
862
|
}
|
|
921
|
-
|
|
922
|
-
|
|
863
|
+
if (!res.ok) {
|
|
864
|
+
const err = parsed?.error ?? {};
|
|
865
|
+
throw new GoCommerceError(
|
|
866
|
+
err.message ?? `The commerce engine answered ${res.status}.`,
|
|
867
|
+
res.status,
|
|
868
|
+
err.code,
|
|
869
|
+
err.details
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
return parsed ?? { data: null };
|
|
923
873
|
}
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
return this.list({ page, sort });
|
|
874
|
+
/** The bare payload, for the many callers that never look at `meta`. */
|
|
875
|
+
async data(path, req = {}) {
|
|
876
|
+
return (await this.engine(path, req)).data;
|
|
928
877
|
}
|
|
929
|
-
|
|
930
|
-
|
|
878
|
+
/**
|
|
879
|
+
* Prices a cart at the signed-in shopper's customer-group prices. The
|
|
880
|
+
* engine does this only for an account whose email is confirmed (403
|
|
881
|
+
* otherwise), and the cart stays the shopper's either way — so a refusal
|
|
882
|
+
* is not an error, and neither is a build without ext/identity.
|
|
883
|
+
*/
|
|
884
|
+
async claimCartForAccount(cartId) {
|
|
885
|
+
try {
|
|
886
|
+
await this.engine("/x/identity/me/carts", { method: "POST", body: { cart_id: cartId }, auth: true });
|
|
887
|
+
} catch {
|
|
888
|
+
}
|
|
931
889
|
}
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
890
|
+
// ------------------------------------------------------------ Litekart verbs
|
|
891
|
+
/**
|
|
892
|
+
* The family's path-addressed verbs. An absolute URL is fetched as-is; a
|
|
893
|
+
* relative one is a Litekart REST path, answered from the storefront's own
|
|
894
|
+
* data where it has some and refused otherwise (rest-guard.ts).
|
|
895
|
+
*/
|
|
896
|
+
get(path) {
|
|
897
|
+
return this.raw("get", path);
|
|
939
898
|
}
|
|
940
|
-
|
|
941
|
-
return
|
|
899
|
+
post(path, data) {
|
|
900
|
+
return this.raw("post", path, data);
|
|
942
901
|
}
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
const res = await this.api("GET", `/api/variants${query({ product_id: productId, limit: MAX_LIMIT })}`);
|
|
946
|
-
return fromListMeta(res.data || [], res.meta);
|
|
902
|
+
put(path, data) {
|
|
903
|
+
return this.raw("put", path, data);
|
|
947
904
|
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
async addReview(_params) {
|
|
951
|
-
throw new Error("Product reviews are not available on this store.");
|
|
905
|
+
patch(path, data) {
|
|
906
|
+
return this.raw("patch", path, data);
|
|
952
907
|
}
|
|
953
|
-
|
|
954
|
-
return
|
|
908
|
+
delete(path) {
|
|
909
|
+
return this.raw("delete", path);
|
|
955
910
|
}
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
911
|
+
async raw(method, path, data) {
|
|
912
|
+
if (!/^https?:\/\//i.test(path)) return await resolveRestLocally(method, path);
|
|
913
|
+
const res = await this._fetch.call(globalThis, path, {
|
|
914
|
+
method: method.toUpperCase(),
|
|
915
|
+
headers: { Accept: "application/json", ...data === void 0 ? {} : { "Content-Type": "application/json" } },
|
|
916
|
+
body: data === void 0 ? void 0 : JSON.stringify(data)
|
|
917
|
+
});
|
|
918
|
+
const body = await res.json().catch(() => ({}));
|
|
919
|
+
if (!res.ok)
|
|
920
|
+
throw new GoCommerceError(body?.error?.message ?? `Request failed with status ${res.status}`, res.status);
|
|
921
|
+
return body;
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
var unreachable = (base, cause) => new GoCommerceError(`Could not reach the commerce engine at ${base}: ${cause?.message ?? cause}`, 0);
|
|
925
|
+
function qs(query) {
|
|
926
|
+
if (!query) return "";
|
|
927
|
+
const sp = new URLSearchParams();
|
|
928
|
+
for (const [key, value] of Object.entries(query)) {
|
|
929
|
+
if (value === void 0 || value === null || value === "") continue;
|
|
930
|
+
if (Array.isArray(value)) for (const v of value) sp.append(key, String(v));
|
|
931
|
+
else sp.append(key, String(value));
|
|
932
|
+
}
|
|
933
|
+
const s = sp.toString();
|
|
934
|
+
return s ? `?${s}` : "";
|
|
935
|
+
}
|
|
936
|
+
function toPage(env, map) {
|
|
937
|
+
const rows = env.data ?? [];
|
|
938
|
+
const meta = env.meta;
|
|
939
|
+
return {
|
|
940
|
+
data: rows.map(map),
|
|
941
|
+
count: meta?.total ?? rows.length,
|
|
942
|
+
pageSize: meta?.limit ?? rows.length,
|
|
943
|
+
noOfPage: meta?.total_pages ?? 1,
|
|
944
|
+
page: meta?.page ?? 1
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
var emptyPage = () => ({ data: [], count: 0, pageSize: 0, noOfPage: 0, page: 1 });
|
|
948
|
+
|
|
949
|
+
// src/services/facts.ts
|
|
950
|
+
var StoreFacts = class extends BaseService {
|
|
951
|
+
key(name) {
|
|
952
|
+
return `${this.engineBase()}|${this.creds.store ?? ""}|${name}`;
|
|
953
|
+
}
|
|
954
|
+
/** Enabled plugins by key. A module that is not installed has no key here. */
|
|
955
|
+
plugins() {
|
|
956
|
+
return remember(this.key("plugins"), async () => {
|
|
957
|
+
const list = await this.data("/api/plugins") ?? [];
|
|
958
|
+
return new Map(list.map((p) => [p.key, p]));
|
|
959
|
+
});
|
|
963
960
|
}
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
*/
|
|
970
|
-
async get(url) {
|
|
971
|
-
if (typeof url === "string" && url.startsWith("/api/categories/all")) {
|
|
972
|
-
return await this.fetchAllCategories();
|
|
961
|
+
async pluginOn(key) {
|
|
962
|
+
try {
|
|
963
|
+
return (await this.plugins()).has(key);
|
|
964
|
+
} catch {
|
|
965
|
+
return false;
|
|
973
966
|
}
|
|
974
|
-
|
|
967
|
+
}
|
|
968
|
+
/** The store's settlement currency and default language. */
|
|
969
|
+
ready() {
|
|
970
|
+
return remember(this.key("ready"), () => this.data("/health/ready"));
|
|
971
|
+
}
|
|
972
|
+
checkoutOptions() {
|
|
973
|
+
return remember(this.key("checkout"), () => this.data("/api/checkout"));
|
|
975
974
|
}
|
|
976
975
|
/**
|
|
977
|
-
* The
|
|
978
|
-
*
|
|
979
|
-
*
|
|
980
|
-
* every category the storefront can see — roots plus what fits beneath them. A store that has
|
|
981
|
-
* imported a full retail taxonomy has far more nodes than that; the ones a shopper reaches are
|
|
982
|
-
* still reachable, because a listing page looks its category up by slug directly.
|
|
976
|
+
* The whole tree, flat and depth-first. Past 500 categories the engine
|
|
977
|
+
* answers a truncated list; the tree then serves what it has, and a
|
|
978
|
+
* breadcrumb for a category outside it falls back to the category alone.
|
|
983
979
|
*/
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
/** A category and the active products filed under it or anything beneath it. */
|
|
990
|
-
async fetchCategoryPage(slug, { limit = MAX_LIMIT, offset = 0 } = {}) {
|
|
991
|
-
const res = await this.api("GET", `/api/categories/${encodeURIComponent(slug)}${query({ limit, offset })}`);
|
|
992
|
-
const category = res.data;
|
|
993
|
-
if (category) rememberCategories([category]);
|
|
994
|
-
const products = remember(category?.products || []);
|
|
995
|
-
return { category, products, meta: res.meta };
|
|
996
|
-
}
|
|
997
|
-
async fetchCategory(slugOrId) {
|
|
998
|
-
const { category } = await this.fetchCategoryPage(String(slugOrId), { limit: 1 });
|
|
999
|
-
return mapCategory(category);
|
|
1000
|
-
}
|
|
1001
|
-
async fetchFooterCategories({ page = 1, q = "" } = {}) {
|
|
1002
|
-
const all = await this.fetchAllCategories();
|
|
1003
|
-
const filtered = q ? all.data.filter((c) => c?.name?.toLowerCase().includes(q.toLowerCase())) : all.data;
|
|
1004
|
-
const start = (page - 1) * PAGE_SIZE;
|
|
1005
|
-
return paginated(filtered.slice(start, start + PAGE_SIZE), filtered.length, page);
|
|
1006
|
-
}
|
|
1007
|
-
/** The homepage grid: the roots of the taxonomy, which is what a shopper navigates from. */
|
|
1008
|
-
async fetchFeaturedCategories({ limit = 18 } = {}) {
|
|
1009
|
-
const all = await this.fetchAllCategories();
|
|
1010
|
-
const roots = all.data.filter((c) => c && c.parentCategoryId == null);
|
|
1011
|
-
const picked = (roots.length ? roots : all.data).slice(0, limit);
|
|
1012
|
-
return paginated(picked, picked.length, 1, limit);
|
|
1013
|
-
}
|
|
1014
|
-
async fetchAllProductsOfCategories(slugOrId) {
|
|
1015
|
-
const slug = await resolveCategorySlug(slugOrId) || String(slugOrId);
|
|
1016
|
-
const { products, meta } = await this.fetchCategoryPage(slug);
|
|
1017
|
-
return fromListMeta(products.map(mapProduct), meta);
|
|
980
|
+
categories() {
|
|
981
|
+
return remember(
|
|
982
|
+
this.key("categories"),
|
|
983
|
+
async () => await this.data("/api/categories", { query: { flat: 1 } }) ?? []
|
|
984
|
+
);
|
|
1018
985
|
}
|
|
1019
|
-
|
|
1020
|
-
|
|
986
|
+
/** Root → `slug`, by walking parents in the flat tree. */
|
|
987
|
+
async hierarchy(slug) {
|
|
988
|
+
const all = await this.categories().catch(() => []);
|
|
989
|
+
const byId = new Map(all.map((c2) => [c2.id, c2]));
|
|
990
|
+
const out = [];
|
|
991
|
+
let c = all.find((x) => x.slug === slug);
|
|
992
|
+
while (c) {
|
|
993
|
+
out.unshift({ name: c.title, slug: c.slug });
|
|
994
|
+
c = c.parent_id === null ? void 0 : byId.get(c.parent_id);
|
|
995
|
+
}
|
|
996
|
+
return out;
|
|
997
|
+
}
|
|
998
|
+
/** `slug` and every category under it — what a category page sells. */
|
|
999
|
+
async subtree(slug) {
|
|
1000
|
+
const all = await this.categories();
|
|
1001
|
+
const root = all.find((c) => c.slug === slug);
|
|
1002
|
+
if (!root) return [];
|
|
1003
|
+
const ids = /* @__PURE__ */ new Set([root.id]);
|
|
1004
|
+
const out = [root];
|
|
1005
|
+
for (const c of all) {
|
|
1006
|
+
if (c.parent_id !== null && ids.has(c.parent_id) && !ids.has(c.id)) {
|
|
1007
|
+
ids.add(c.id);
|
|
1008
|
+
out.push(c);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
return out;
|
|
1021
1012
|
}
|
|
1022
1013
|
};
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
if (!
|
|
1028
|
-
const
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1014
|
+
|
|
1015
|
+
// src/services/catalog.ts
|
|
1016
|
+
var PAGE_SIZE = 24;
|
|
1017
|
+
function indexSort(sort) {
|
|
1018
|
+
if (!sort) return void 0;
|
|
1019
|
+
const s = sort.trim();
|
|
1020
|
+
const [field, dir] = s.startsWith("-") ? [s.slice(1), "desc"] : s.includes(":") ? s.split(":") : [s, "asc"];
|
|
1021
|
+
const d = dir === "desc" ? "desc" : "asc";
|
|
1022
|
+
switch (field) {
|
|
1023
|
+
case "price":
|
|
1024
|
+
return `price_minor:${d}`;
|
|
1025
|
+
case "updatedAt":
|
|
1026
|
+
case "updated_at":
|
|
1027
|
+
case "createdAt":
|
|
1028
|
+
case "created_at":
|
|
1029
|
+
return `updated_at:${d}`;
|
|
1030
|
+
case "title":
|
|
1031
|
+
case "name":
|
|
1032
|
+
return `title:${d}`;
|
|
1033
|
+
default:
|
|
1034
|
+
return void 0;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
var NATURAL = /* @__PURE__ */ new Set([
|
|
1038
|
+
"-createdAt",
|
|
1039
|
+
"createdAt:desc",
|
|
1040
|
+
"-updatedAt",
|
|
1041
|
+
"updatedAt:desc",
|
|
1042
|
+
"-popularity",
|
|
1043
|
+
"popularity:desc"
|
|
1044
|
+
]);
|
|
1045
|
+
var quote = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
1046
|
+
var CatalogService = class extends BaseService {
|
|
1047
|
+
facts = new StoreFacts(this._fetch);
|
|
1048
|
+
async queryCatalog(q) {
|
|
1049
|
+
const page = Math.max(1, Number(q.page) || 1);
|
|
1050
|
+
const limit = Math.min(100, Math.max(1, Number(q.limit) || PAGE_SIZE));
|
|
1051
|
+
const search = q.search?.trim() || void 0;
|
|
1052
|
+
const wantsRange = Number.isFinite(q.priceFrom) || Number.isFinite(q.priceTo);
|
|
1053
|
+
const sortKey = q.sort && !NATURAL.has(q.sort) ? indexSort(q.sort) : void 0;
|
|
1054
|
+
const hierarchy = q.category ? await this.facts.hierarchy(q.category) : [];
|
|
1055
|
+
if ((sortKey || wantsRange) && await this.facts.pluginOn("meilisearch")) {
|
|
1056
|
+
const filters = [];
|
|
1057
|
+
if (q.category) {
|
|
1058
|
+
const names = (await this.facts.subtree(q.category).catch(() => [])).map(
|
|
1059
|
+
(c) => quote(c.full_name)
|
|
1060
|
+
);
|
|
1061
|
+
if (!names.length) return { ...emptyCatalog(limit), categoryHierarchy: hierarchy, sorted: true };
|
|
1062
|
+
filters.push(`category IN [${names.join(", ")}]`);
|
|
1063
|
+
}
|
|
1064
|
+
if (wantsRange) {
|
|
1065
|
+
const { currency } = await this.facts.ready();
|
|
1066
|
+
if (Number.isFinite(q.priceFrom)) filters.push(`price_minor >= ${toMinor(q.priceFrom, currency)}`);
|
|
1067
|
+
if (Number.isFinite(q.priceTo)) filters.push(`price_minor <= ${toMinor(q.priceTo, currency)}`);
|
|
1068
|
+
}
|
|
1069
|
+
const res = await this.data("/x/meilisearch/search", {
|
|
1070
|
+
query: {
|
|
1071
|
+
q: search ?? "",
|
|
1072
|
+
limit,
|
|
1073
|
+
offset: (page - 1) * limit,
|
|
1074
|
+
filter: filters.join(" AND ") || void 0,
|
|
1075
|
+
sort: sortKey
|
|
1076
|
+
}
|
|
1077
|
+
});
|
|
1078
|
+
const total = res.total ?? 0;
|
|
1079
|
+
return {
|
|
1080
|
+
data: (res.hits ?? []).map(fromSearchDoc),
|
|
1081
|
+
count: total,
|
|
1082
|
+
pageSize: limit,
|
|
1083
|
+
noOfPage: Math.max(1, Math.ceil(total / limit)),
|
|
1084
|
+
page,
|
|
1085
|
+
categoryHierarchy: hierarchy,
|
|
1086
|
+
sorted: true
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
const sorted = !sortKey && !wantsRange;
|
|
1090
|
+
if (q.category && !search) {
|
|
1091
|
+
const env2 = await this.engine(
|
|
1092
|
+
`/api/categories/${encodeURIComponent(q.category)}`,
|
|
1093
|
+
{ query: { page, limit } }
|
|
1094
|
+
);
|
|
1095
|
+
const products = env2.data?.products ?? [];
|
|
1096
|
+
return {
|
|
1097
|
+
...toPage({ data: products, meta: env2.meta }, (p) => toProduct(p)),
|
|
1098
|
+
categoryHierarchy: hierarchy,
|
|
1099
|
+
sorted
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
const env = await this.engine("/api/products", {
|
|
1103
|
+
query: { q: search, page, limit, channel: this.creds.channel }
|
|
1104
|
+
});
|
|
1105
|
+
return { ...toPage(env, (p) => toProduct(p)), categoryHierarchy: hierarchy, sorted };
|
|
1048
1106
|
}
|
|
1107
|
+
};
|
|
1108
|
+
function emptyCatalog(limit = PAGE_SIZE) {
|
|
1109
|
+
return { data: [], count: 0, pageSize: limit, noOfPage: 0, page: 1 };
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// src/services/product-service.ts
|
|
1113
|
+
var ProductService = class extends CatalogService {
|
|
1049
1114
|
/**
|
|
1050
|
-
* A
|
|
1051
|
-
* an
|
|
1115
|
+
* A product page. `slug` may also be a numeric id or a variant SKU, which is
|
|
1116
|
+
* how an old link or a search hit sometimes arrives.
|
|
1117
|
+
*
|
|
1118
|
+
* Reviews ride along when ext/reviews is installed and switched on; a review
|
|
1119
|
+
* failure never costs the shopper the product page.
|
|
1052
1120
|
*/
|
|
1053
|
-
async getOne(
|
|
1054
|
-
const
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1121
|
+
async getOne(slug) {
|
|
1122
|
+
const p = await this.findProduct(String(slug));
|
|
1123
|
+
const product = toProduct(p, {
|
|
1124
|
+
categoryHierarchy: p.category ? await this.facts.hierarchy(p.category.slug) : []
|
|
1125
|
+
});
|
|
1126
|
+
if (await this.facts.pluginOn("product-reviews")) {
|
|
1127
|
+
try {
|
|
1128
|
+
const res = await this.data("/x/reviews", { query: { product_id: p.id, limit: 50 } });
|
|
1129
|
+
product.ratings = (res.reviews ?? []).map(toRating);
|
|
1130
|
+
product.rating = res.summary?.count ? res.summary.average : null;
|
|
1131
|
+
product.ratingCount = res.summary?.count ?? 0;
|
|
1132
|
+
product.reviewCount = res.summary?.count ?? 0;
|
|
1133
|
+
} catch {
|
|
1134
|
+
}
|
|
1061
1135
|
}
|
|
1062
|
-
|
|
1063
|
-
if (!res.data) throw new Error(`Collection ${idOrSlug} not found`);
|
|
1064
|
-
remember(res.data.products || []);
|
|
1065
|
-
return mapCollection(res.data);
|
|
1136
|
+
return product;
|
|
1066
1137
|
}
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
slug:
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
metaTitle: null,
|
|
1080
|
-
metaDescription: null,
|
|
1081
|
-
collectionvalues: [],
|
|
1082
|
-
createdAt: null,
|
|
1083
|
-
updatedAt: null
|
|
1084
|
-
};
|
|
1138
|
+
async findProduct(slug) {
|
|
1139
|
+
try {
|
|
1140
|
+
return await this.data(`/api/products/slug/${encodeURIComponent(slug)}`);
|
|
1141
|
+
} catch (err) {
|
|
1142
|
+
if (!(err instanceof GoCommerceError) || err.status !== 404) throw err;
|
|
1143
|
+
const fallback = /^\d+$/.test(slug) ? `/api/products/${slug}` : `/api/products/sku/${encodeURIComponent(slug)}`;
|
|
1144
|
+
try {
|
|
1145
|
+
return await this.data(fallback);
|
|
1146
|
+
} catch {
|
|
1147
|
+
throw err;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1085
1150
|
}
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1151
|
+
/** The shop's listing: `{ page, search, sort }`, newest first unless the index can reorder it. */
|
|
1152
|
+
async list({
|
|
1153
|
+
page = 1,
|
|
1154
|
+
search,
|
|
1155
|
+
q,
|
|
1156
|
+
sort,
|
|
1157
|
+
limit,
|
|
1158
|
+
categories
|
|
1159
|
+
} = {}) {
|
|
1160
|
+
const res = await this.queryCatalog({ page, search: search ?? q, sort, limit, category: categories || void 0 });
|
|
1161
|
+
return { data: res.data, count: res.count, pageSize: res.pageSize, noOfPage: res.noOfPage, page: res.page };
|
|
1162
|
+
}
|
|
1163
|
+
async listFeaturedProducts({ page = 1, sort, limit } = {}) {
|
|
1164
|
+
return this.shelf("featured", { page, sort, limit });
|
|
1165
|
+
}
|
|
1166
|
+
async listTrendingProducts({ page = 1, limit } = {}) {
|
|
1167
|
+
return this.shelf("trending", { page, limit });
|
|
1168
|
+
}
|
|
1169
|
+
/** More from this product's category. */
|
|
1170
|
+
async listRelatedProducts({
|
|
1171
|
+
page = 1,
|
|
1172
|
+
categoryId,
|
|
1173
|
+
limit
|
|
1174
|
+
} = {}) {
|
|
1175
|
+
if (!categoryId) return this.list({ page, limit });
|
|
1176
|
+
const category = (await this.facts.categories()).find((c) => String(c.id) === String(categoryId));
|
|
1177
|
+
if (!category) return this.list({ page, limit });
|
|
1178
|
+
return this.list({ page, limit, categories: category.slug });
|
|
1179
|
+
}
|
|
1180
|
+
async shelf(slug, { page, sort, limit }) {
|
|
1181
|
+
const size = Math.min(100, Math.max(1, Number(limit) || PAGE_SIZE));
|
|
1182
|
+
try {
|
|
1183
|
+
const env = await this.engine(`/api/collections/${slug}`, {
|
|
1184
|
+
query: { page, limit: size }
|
|
1185
|
+
});
|
|
1186
|
+
const rows = env.data?.products ?? [];
|
|
1187
|
+
return {
|
|
1188
|
+
data: rows.map((p) => toProduct(p)),
|
|
1189
|
+
count: env.meta?.total ?? rows.length,
|
|
1190
|
+
pageSize: env.meta?.limit ?? size,
|
|
1191
|
+
noOfPage: env.meta?.total_pages ?? 1,
|
|
1192
|
+
page: env.meta?.page ?? page
|
|
1193
|
+
};
|
|
1194
|
+
} catch (err) {
|
|
1195
|
+
if (!(err instanceof GoCommerceError) || err.status !== 404) throw err;
|
|
1196
|
+
return this.list({ page, sort, limit: size });
|
|
1102
1197
|
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* A review, through ext/reviews. The engine asks for a name and an email
|
|
1201
|
+
* with every review, so a signed-out shopper is asked to sign in rather
|
|
1202
|
+
* than having one made up for them. Uploaded photos have nowhere to go: the
|
|
1203
|
+
* module stores text.
|
|
1204
|
+
*/
|
|
1205
|
+
async addReview({
|
|
1206
|
+
productId,
|
|
1207
|
+
rating,
|
|
1208
|
+
review,
|
|
1209
|
+
title
|
|
1210
|
+
}) {
|
|
1211
|
+
const me = currentMe();
|
|
1212
|
+
if (!me?.email) throw new GoCommerceError("Please sign in to write a review.", 401, "unauthorized");
|
|
1213
|
+
const name = [me.firstName, me.lastName].filter(Boolean).join(" ") || me.email.split("@")[0];
|
|
1214
|
+
return this.data("/x/reviews", {
|
|
1215
|
+
method: "POST",
|
|
1216
|
+
body: {
|
|
1217
|
+
product_id: Number(productId),
|
|
1218
|
+
name,
|
|
1219
|
+
email: me.email,
|
|
1220
|
+
rating: Math.round(Number(rating)),
|
|
1221
|
+
...title ? { title } : {},
|
|
1222
|
+
body: String(review ?? "")
|
|
1223
|
+
}
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
async fetchReels() {
|
|
1227
|
+
throw new NotSupportedError("ProductService", "fetchReels", "the engine has no reels");
|
|
1228
|
+
}
|
|
1111
1229
|
};
|
|
1112
|
-
var
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
if (existing) existing.count += 1;
|
|
1120
|
-
else counts.set(id, { ...entry, count: 1 });
|
|
1121
|
-
}
|
|
1122
|
-
return [...counts.values()].sort((a, b) => b.count - a.count);
|
|
1230
|
+
var productService = new ProductService();
|
|
1231
|
+
|
|
1232
|
+
// src/services/search-service.ts
|
|
1233
|
+
var num = (v) => {
|
|
1234
|
+
if (v === null || v.trim() === "") return void 0;
|
|
1235
|
+
const n = Number(v);
|
|
1236
|
+
return Number.isFinite(n) ? n : void 0;
|
|
1123
1237
|
};
|
|
1124
|
-
var SearchService = class
|
|
1125
|
-
static instance;
|
|
1126
|
-
static getInstance() {
|
|
1127
|
-
if (!_SearchService.instance) _SearchService.instance = new _SearchService();
|
|
1128
|
-
return _SearchService.instance;
|
|
1129
|
-
}
|
|
1238
|
+
var SearchService = class extends CatalogService {
|
|
1130
1239
|
emptyResult() {
|
|
1131
1240
|
return {
|
|
1132
1241
|
data: [],
|
|
1133
1242
|
count: 0,
|
|
1134
1243
|
totalPages: 0,
|
|
1135
1244
|
categoryHierarchy: [],
|
|
1136
|
-
facets: { priceStat: {
|
|
1245
|
+
facets: { priceStat: {}, categories: [], tags: [], allFilters: {} }
|
|
1137
1246
|
};
|
|
1138
1247
|
}
|
|
1139
|
-
/**
|
|
1140
|
-
* The listing page: `/products`, `/[slug]` for a category, and every filter the sidebar sets.
|
|
1141
|
-
*
|
|
1142
|
-
* Term and category are pushed to the API. Tags, the price range and the sort control are
|
|
1143
|
-
* applied here, because GoCommerce has no parameter for them — so when any of those is active
|
|
1144
|
-
* the request asks for the whole window rather than one page, and the result is paged locally.
|
|
1145
|
-
*/
|
|
1146
1248
|
async searchWithUrl(url, slug) {
|
|
1147
1249
|
try {
|
|
1148
|
-
const
|
|
1149
|
-
const
|
|
1150
|
-
const
|
|
1151
|
-
const
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
let meta;
|
|
1160
|
-
let category;
|
|
1161
|
-
if (categorySlug) {
|
|
1162
|
-
const res = await categoryService.fetchCategoryPage(categorySlug, { limit: MAX_LIMIT });
|
|
1163
|
-
products = res.products;
|
|
1164
|
-
meta = res.meta;
|
|
1165
|
-
category = res.category;
|
|
1166
|
-
if (term) {
|
|
1167
|
-
const needle = term.toLowerCase();
|
|
1168
|
-
products = products.filter((p) => `${p.title} ${p.description || ""}`.toLowerCase().includes(needle));
|
|
1169
|
-
}
|
|
1170
|
-
} else {
|
|
1171
|
-
const res = await productService.fetchProducts({
|
|
1172
|
-
q: term,
|
|
1173
|
-
page: needsWholeWindow ? 1 : page,
|
|
1174
|
-
limit: needsWholeWindow ? MAX_LIMIT : PAGE_SIZE
|
|
1175
|
-
});
|
|
1176
|
-
products = res.products;
|
|
1177
|
-
meta = res.meta;
|
|
1178
|
-
}
|
|
1179
|
-
const facetSource = products;
|
|
1180
|
-
const priceMinor = facetSource.map(productPrice).filter((n) => Number.isFinite(n));
|
|
1181
|
-
const currency = facetSource[0]?.variants?.[0]?.price?.currency || facetSource[0]?.currency;
|
|
1182
|
-
const digits = currency ? 10 ** currencyFractionDigits(currency) : 100;
|
|
1183
|
-
let filtered = products;
|
|
1184
|
-
if (tags.length) {
|
|
1185
|
-
filtered = filtered.filter((p) => (p.tags || []).some((tag) => tags.includes(tag.toLowerCase())));
|
|
1186
|
-
}
|
|
1187
|
-
if (priceFrom !== void 0) filtered = filtered.filter((p) => productPrice(p) / digits >= priceFrom);
|
|
1188
|
-
if (priceTo !== void 0) filtered = filtered.filter((p) => productPrice(p) / digits <= priceTo);
|
|
1189
|
-
filtered = sortProducts(filtered, sort);
|
|
1190
|
-
const paged = needsWholeWindow ? filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE) : filtered;
|
|
1191
|
-
const count = needsWholeWindow ? filtered.length : meta?.total ?? filtered.length;
|
|
1250
|
+
const u = typeof url === "string" ? new URL(url, "http://localhost") : url;
|
|
1251
|
+
const p = u.searchParams;
|
|
1252
|
+
const [from, to] = (p.get("price") ?? "").split(/[,-]/);
|
|
1253
|
+
const res = await this.queryCatalog({
|
|
1254
|
+
page: num(p.get("page")),
|
|
1255
|
+
search: p.get("search") ?? p.get("q") ?? void 0,
|
|
1256
|
+
sort: p.get("sort") ?? void 0,
|
|
1257
|
+
category: slug || p.get("categories")?.split(",")[0] || void 0,
|
|
1258
|
+
priceFrom: num(p.get("priceFrom")) ?? num(from ?? null),
|
|
1259
|
+
priceTo: num(p.get("priceTo")) ?? num(to ?? null)
|
|
1260
|
+
});
|
|
1192
1261
|
return {
|
|
1193
|
-
data:
|
|
1194
|
-
count,
|
|
1195
|
-
totalPages:
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
categoryHierarchy: category ? [{ name: category.title, slug: category.slug }] : [],
|
|
1200
|
-
facets: {
|
|
1201
|
-
priceStat: priceMinor.length ? { min: Math.min(...priceMinor) / digits, max: Math.max(...priceMinor) / digits } : { min: void 0, max: void 0 },
|
|
1202
|
-
categories: countBy(facetSource, (p) => p.category ? { name: p.category.title, slug: p.category.slug } : void 0),
|
|
1203
|
-
tags: countBy(
|
|
1204
|
-
facetSource.flatMap((p) => (p.tags || []).map((tag) => ({ tag }))),
|
|
1205
|
-
({ tag }) => ({ name: tag, slug: tag })
|
|
1206
|
-
),
|
|
1207
|
-
allFilters: {}
|
|
1208
|
-
}
|
|
1262
|
+
data: res.data,
|
|
1263
|
+
count: res.count,
|
|
1264
|
+
totalPages: res.noOfPage,
|
|
1265
|
+
page: res.page,
|
|
1266
|
+
categoryHierarchy: res.categoryHierarchy,
|
|
1267
|
+
facets: { priceStat: {}, categories: [], tags: [], allFilters: {} }
|
|
1209
1268
|
};
|
|
1210
|
-
} catch (
|
|
1211
|
-
if (
|
|
1212
|
-
|
|
1269
|
+
} catch (err) {
|
|
1270
|
+
if (err?.status === 404) return this.emptyResult();
|
|
1271
|
+
throw err;
|
|
1213
1272
|
}
|
|
1214
1273
|
}
|
|
1215
|
-
async searchWithQuery(queryString) {
|
|
1216
|
-
const url = new URL("https://storefront.local/products");
|
|
1217
|
-
if (queryString) url.searchParams.set("search", queryString);
|
|
1218
|
-
return this.searchWithUrl(url);
|
|
1219
|
-
}
|
|
1220
1274
|
};
|
|
1221
|
-
var searchService = SearchService
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
});
|
|
1235
|
-
const hits = products.map(mapProduct);
|
|
1275
|
+
var searchService = new SearchService();
|
|
1276
|
+
|
|
1277
|
+
// src/services/meilisearch-service.ts
|
|
1278
|
+
var MeilisearchService = class extends CatalogService {
|
|
1279
|
+
/** `{ query, categories, page }` → a Meilisearch-shaped answer: `hits`, `totalHits`, `totalPages`. */
|
|
1280
|
+
async search({
|
|
1281
|
+
query,
|
|
1282
|
+
search,
|
|
1283
|
+
categories,
|
|
1284
|
+
page = 1,
|
|
1285
|
+
limit
|
|
1286
|
+
} = {}) {
|
|
1287
|
+
const res = await this.queryCatalog({ page, limit, search: query ?? search, category: categories || void 0 });
|
|
1236
1288
|
return {
|
|
1237
|
-
hits,
|
|
1238
|
-
|
|
1239
|
-
estimatedTotalHits:
|
|
1240
|
-
|
|
1241
|
-
page,
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
facetDistribution: {},
|
|
1245
|
-
processingTimeMs: 0,
|
|
1246
|
-
query: params.q || ""
|
|
1289
|
+
hits: res.data,
|
|
1290
|
+
totalHits: res.count,
|
|
1291
|
+
estimatedTotalHits: res.count,
|
|
1292
|
+
totalPages: res.noOfPage,
|
|
1293
|
+
page: res.page,
|
|
1294
|
+
categories: res.categoryHierarchy,
|
|
1295
|
+
facetDistribution: {}
|
|
1247
1296
|
};
|
|
1248
1297
|
}
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
const
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
})
|
|
1258
|
-
return {
|
|
1259
|
-
}
|
|
1260
|
-
};
|
|
1261
|
-
var meilisearchService = MeilisearchService.getInstance();
|
|
1262
|
-
var AutocompleteService = class _AutocompleteService extends BaseService {
|
|
1263
|
-
static instance;
|
|
1264
|
-
static getInstance() {
|
|
1265
|
-
if (!_AutocompleteService.instance) _AutocompleteService.instance = new _AutocompleteService();
|
|
1266
|
-
return _AutocompleteService.instance;
|
|
1267
|
-
}
|
|
1268
|
-
async list({ page = 1, q = "" } = {}) {
|
|
1269
|
-
const { products, meta } = await productService.fetchProducts({ q, page, limit: 10 });
|
|
1270
|
-
return fromListMeta(
|
|
1271
|
-
products.map((product) => ({ id: product.id, name: product.title, slug: product.slug })),
|
|
1272
|
-
meta,
|
|
1273
|
-
page
|
|
1274
|
-
);
|
|
1298
|
+
/** The header's type-ahead: `{ data: [{ slug, title, thumbnail, price }] }`. */
|
|
1299
|
+
async searchAutoComplete({ query, search, limit = 8 } = {}) {
|
|
1300
|
+
const q = (query ?? search ?? "").trim();
|
|
1301
|
+
if (!q) return { data: [] };
|
|
1302
|
+
if (await this.facts.pluginOn("meilisearch")) {
|
|
1303
|
+
const res = await this.data("/x/meilisearch/search", { query: { q, limit } });
|
|
1304
|
+
return { data: (res.hits ?? []).map(fromSearchDoc) };
|
|
1305
|
+
}
|
|
1306
|
+
const rows = await this.data("/api/products", { query: { q, limit, channel: this.creds.channel } }) ?? [];
|
|
1307
|
+
return { data: rows.map((p) => toProduct(p)) };
|
|
1275
1308
|
}
|
|
1276
1309
|
};
|
|
1277
|
-
var
|
|
1278
|
-
var resolveProductIndex = async (ids, fetchFn) => {
|
|
1279
|
-
const wanted = [...new Set(ids.filter((id) => typeof id === "number"))];
|
|
1280
|
-
const service = fetchFn ? new ProductService(fetchFn) : productService;
|
|
1281
|
-
await Promise.all(wanted.filter((id) => !productCache.has(id)).map((id) => service.fetchProductById(id)));
|
|
1282
|
-
const index = /* @__PURE__ */ new Map();
|
|
1283
|
-
for (const id of wanted) {
|
|
1284
|
-
const product = productCache.get(id);
|
|
1285
|
-
if (product) index.set(id, product);
|
|
1286
|
-
}
|
|
1287
|
-
return index;
|
|
1288
|
-
};
|
|
1310
|
+
var meilisearchService = new MeilisearchService();
|
|
1289
1311
|
|
|
1290
|
-
// src/services/
|
|
1291
|
-
var
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1312
|
+
// src/services/category-service.ts
|
|
1313
|
+
var asList = (rows) => ({
|
|
1314
|
+
data: rows,
|
|
1315
|
+
count: rows.length,
|
|
1316
|
+
pageSize: rows.length,
|
|
1317
|
+
noOfPage: 1,
|
|
1318
|
+
page: 1
|
|
1319
|
+
});
|
|
1320
|
+
var CategoryService = class extends BaseService {
|
|
1321
|
+
facts = new StoreFacts(this._fetch);
|
|
1322
|
+
/** Every category, flat, each naming its parent. The storefront builds the tree. */
|
|
1323
|
+
async fetchAllCategories(_args = {}) {
|
|
1324
|
+
const all = await this.facts.categories();
|
|
1325
|
+
return asList(all.map(flat));
|
|
1298
1326
|
}
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
try {
|
|
1303
|
-
window?.localStorage?.setItem(CART_ID_KEY, cartId);
|
|
1304
|
-
} catch {
|
|
1327
|
+
async fetchFeaturedCategories({ limit = 18 } = {}) {
|
|
1328
|
+
const roots = (await this.facts.categories()).filter((c) => c.parent_id === null);
|
|
1329
|
+
return asList(roots.slice(0, Math.max(0, Number(limit) || 18)).map(flat));
|
|
1305
1330
|
}
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
try {
|
|
1309
|
-
window?.localStorage?.removeItem(CART_ID_KEY);
|
|
1310
|
-
} catch {
|
|
1331
|
+
async fetchFooterCategories(args = {}) {
|
|
1332
|
+
return this.fetchFeaturedCategories(args);
|
|
1311
1333
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
static getInstance() {
|
|
1317
|
-
if (!_CartService.instance) _CartService.instance = new _CartService();
|
|
1318
|
-
return _CartService.instance;
|
|
1319
|
-
}
|
|
1320
|
-
/** Turns a GoCommerce cart into the shape the storefront's cart, drawer and checkout read. */
|
|
1321
|
-
async present(cart) {
|
|
1322
|
-
if (!cart) return {};
|
|
1323
|
-
storeCartId(cart.id);
|
|
1324
|
-
const [index, store] = await Promise.all([
|
|
1325
|
-
// Cart lines name a product by id only, so slug and image are filled in from the catalogue.
|
|
1326
|
-
resolveProductIndex(
|
|
1327
|
-
(cart.line_items || []).map((line) => line.product_id),
|
|
1328
|
-
this.getFetch()
|
|
1329
|
-
),
|
|
1330
|
-
readStaticStore()
|
|
1331
|
-
]);
|
|
1332
|
-
return mapCart(cart, readCheckoutDetails(cart.id), index, store);
|
|
1333
|
-
}
|
|
1334
|
-
/** Opens a cart. The email is optional here and is the only field `POST /api/carts` accepts. */
|
|
1335
|
-
async openCart(email) {
|
|
1336
|
-
const res = await this.api("POST", "/api/carts", email ? { email } : {});
|
|
1337
|
-
storeCartId(res.data?.id);
|
|
1338
|
-
return res.data;
|
|
1334
|
+
/** The nested tree, roots first. */
|
|
1335
|
+
async getMegamenu(_args = {}) {
|
|
1336
|
+
const tree = await this.data("/api/categories") ?? [];
|
|
1337
|
+
return tree.map(toCategory);
|
|
1339
1338
|
}
|
|
1340
|
-
async
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
const res = await this.api("GET", `/api/carts/${encodeURIComponent(cartId)}`);
|
|
1344
|
-
cart = res.data ?? null;
|
|
1345
|
-
} catch {
|
|
1346
|
-
return null;
|
|
1347
|
-
}
|
|
1348
|
-
if (cart && cart.status && cart.status !== "open") {
|
|
1349
|
-
forgetCartId();
|
|
1350
|
-
return null;
|
|
1351
|
-
}
|
|
1352
|
-
return cart;
|
|
1339
|
+
async fetchCategory(slug) {
|
|
1340
|
+
const c = await this.data(`/api/categories/${encodeURIComponent(slug)}`, { query: { limit: 1 } });
|
|
1341
|
+
return toCategory(c);
|
|
1353
1342
|
}
|
|
1354
1343
|
/**
|
|
1355
|
-
*
|
|
1356
|
-
*
|
|
1357
|
-
*
|
|
1358
|
-
* reports several unrelated things the same way: a dead cart and an unknown payment code are both
|
|
1359
|
-
* `404 not_found`. The status is the fact; the message is prose.
|
|
1344
|
+
* kitcommerce-core's category filter asks for `/api/categories/all` by path,
|
|
1345
|
+
* through the family's raw `get`. That is a Litekart path the rest-guard
|
|
1346
|
+
* would swallow, so it is answered here, from the engine's tree.
|
|
1360
1347
|
*/
|
|
1361
|
-
async
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
return !res.data || Boolean(res.data.status) && res.data.status !== "open";
|
|
1365
|
-
} catch {
|
|
1366
|
-
return true;
|
|
1367
|
-
}
|
|
1348
|
+
async get(path) {
|
|
1349
|
+
if (path.split("?")[0] === "/api/categories/all") return await this.fetchAllCategories();
|
|
1350
|
+
return super.get(path);
|
|
1368
1351
|
}
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1352
|
+
};
|
|
1353
|
+
var flat = (c) => ({ ...toCategory(c), children: [] });
|
|
1354
|
+
var categoryService = new CategoryService();
|
|
1355
|
+
|
|
1356
|
+
// src/services/collection-service.ts
|
|
1357
|
+
var CollectionService = class extends BaseService {
|
|
1358
|
+
async list({ page = 1, limit = 24 } = {}) {
|
|
1359
|
+
const env = await this.engine("/api/collections", { query: { page, limit } });
|
|
1360
|
+
return toPage(env, toCollection);
|
|
1361
|
+
}
|
|
1362
|
+
/** A collection page: the collection, and its products in the merchant's order. */
|
|
1363
|
+
async getOne(slug, { page = 1, limit = 48 } = {}) {
|
|
1364
|
+
const env = await this.engine(
|
|
1365
|
+
`/api/collections/${encodeURIComponent(slug)}`,
|
|
1366
|
+
{ query: { page, limit } }
|
|
1367
|
+
);
|
|
1368
|
+
const products = (env.data.products ?? []).map((p) => toProduct(p));
|
|
1369
|
+
return {
|
|
1370
|
+
...toCollection(env.data),
|
|
1371
|
+
products,
|
|
1372
|
+
// The family's shape for a collection's members.
|
|
1373
|
+
collectionvalues: products.map((p) => ({ products: p })),
|
|
1374
|
+
count: env.meta?.total ?? products.length,
|
|
1375
|
+
noOfPage: env.meta?.total_pages ?? 1,
|
|
1376
|
+
page: env.meta?.page ?? page
|
|
1377
|
+
};
|
|
1383
1378
|
}
|
|
1379
|
+
/** Collection-level ratings are a Litekart aggregate; the engine has none to report. */
|
|
1380
|
+
async getAllRatings() {
|
|
1381
|
+
return { data: [] };
|
|
1382
|
+
}
|
|
1383
|
+
};
|
|
1384
|
+
var collectionService = new CollectionService();
|
|
1385
|
+
|
|
1386
|
+
// src/services/cart-service.ts
|
|
1387
|
+
var REMOVE = -9999999;
|
|
1388
|
+
var CartService = class extends BaseService {
|
|
1389
|
+
facts = new StoreFacts(this._fetch);
|
|
1384
1390
|
async getCartByCartId(cartId) {
|
|
1385
|
-
const id =
|
|
1386
|
-
if (!id) return
|
|
1387
|
-
|
|
1391
|
+
const id = clean(cartId);
|
|
1392
|
+
if (!id) return emptyCart(await this.currency());
|
|
1393
|
+
try {
|
|
1394
|
+
return await this.present(await this.data(`/api/carts/${encodeURIComponent(id)}`));
|
|
1395
|
+
} catch (err) {
|
|
1396
|
+
if (err instanceof GoCommerceError && err.status === 404) return emptyCart(await this.currency());
|
|
1397
|
+
throw err;
|
|
1398
|
+
}
|
|
1388
1399
|
}
|
|
1389
|
-
async fetchCartData() {
|
|
1390
|
-
|
|
1391
|
-
if (!id) return {};
|
|
1392
|
-
return this.present(await this.readCart(id));
|
|
1400
|
+
async fetchCartData(cartId) {
|
|
1401
|
+
return this.getCartByCartId(cartId ?? storedCartId());
|
|
1393
1402
|
}
|
|
1403
|
+
/**
|
|
1404
|
+
* The cart after an order: the engine marks it `converted` and keeps its
|
|
1405
|
+
* lines, and this answers it with none — which is how the storefront knows
|
|
1406
|
+
* to clear the bag on the success page.
|
|
1407
|
+
*/
|
|
1394
1408
|
async refereshCart() {
|
|
1395
|
-
|
|
1409
|
+
const id = storedCartId();
|
|
1410
|
+
const cart = await this.getCartByCartId(id);
|
|
1411
|
+
if (id && cart.status === "converted") forgetCartExtras(id);
|
|
1412
|
+
return cart;
|
|
1396
1413
|
}
|
|
1397
1414
|
/**
|
|
1398
|
-
*
|
|
1399
|
-
*
|
|
1400
|
-
* `qty` is the line's new quantity, not a delta — which is how the shared cart store calls it
|
|
1401
|
-
* (its stepper sends `qty ± 1`), and what `PATCH .../line-items/{lineId}` takes. A quantity of
|
|
1402
|
-
* zero removes the line, and so does the store's `REMOVE_LINE` sentinel.
|
|
1403
|
-
*
|
|
1404
|
-
* A cart token in this browser can be dead — expired, wiped server-side, or already checked out —
|
|
1405
|
-
* and the API then answers "cart not found", which is what the shopper was shown when they
|
|
1406
|
-
* clicked Add to bag. That is a token this storefront is holding, not anything they did, so a
|
|
1407
|
-
* dead one is dropped and the item is added to a fresh cart instead of reporting it.
|
|
1415
|
+
* `qty` is a change, not a total: +1 from a product page, −1 from a card's
|
|
1416
|
+
* minus button. With no cart yet, one is opened and remembered.
|
|
1408
1417
|
*/
|
|
1409
1418
|
async addToCart({
|
|
1410
|
-
variantId,
|
|
1411
|
-
qty,
|
|
1412
1419
|
cartId,
|
|
1413
|
-
lineId
|
|
1420
|
+
lineId,
|
|
1421
|
+
qty = 1,
|
|
1422
|
+
variantId
|
|
1414
1423
|
}) {
|
|
1415
|
-
|
|
1416
|
-
if (
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1424
|
+
let id = clean(cartId) ?? storedCartId();
|
|
1425
|
+
if (lineId && id) {
|
|
1426
|
+
const cart = await this.data(`/api/carts/${encodeURIComponent(id)}`);
|
|
1427
|
+
const line = (cart.line_items ?? []).find((l) => String(l.id) === String(lineId));
|
|
1428
|
+
if (line) {
|
|
1429
|
+
const next = qty === REMOVE ? 0 : line.quantity + Number(qty);
|
|
1430
|
+
return this.setQuantity(id, line.id, next);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
if (qty === REMOVE || Number(qty) <= 0) return this.getCartByCartId(id);
|
|
1434
|
+
if (!variantId) throw new GoCommerceError("Please choose an option before adding this to your bag.", 400);
|
|
1435
|
+
if (!id) id = await this.open();
|
|
1436
|
+
try {
|
|
1437
|
+
return await this.present(
|
|
1438
|
+
await this.data(`/api/carts/${encodeURIComponent(id)}/line-items`, {
|
|
1439
|
+
method: "POST",
|
|
1440
|
+
body: { variant_id: Number(variantId), quantity: Number(qty) }
|
|
1441
|
+
})
|
|
1442
|
+
);
|
|
1443
|
+
} catch (err) {
|
|
1444
|
+
if (err instanceof GoCommerceError && (err.status === 404 || /checked out/.test(err.message))) {
|
|
1445
|
+
const fresh = await this.open();
|
|
1446
|
+
return this.present(
|
|
1447
|
+
await this.data(`/api/carts/${encodeURIComponent(fresh)}/line-items`, {
|
|
1448
|
+
method: "POST",
|
|
1449
|
+
body: { variant_id: Number(variantId), quantity: Number(qty) }
|
|
1450
|
+
})
|
|
1451
|
+
);
|
|
1426
1452
|
}
|
|
1427
|
-
|
|
1428
|
-
forgetCartId();
|
|
1429
|
-
id = null;
|
|
1453
|
+
throw err;
|
|
1430
1454
|
}
|
|
1431
|
-
if (removing) return {};
|
|
1432
|
-
const opened = await this.openCart(readCheckoutDetails(null).email);
|
|
1433
|
-
if (!opened?.id) throw new Error("Could not open a cart on this store.");
|
|
1434
|
-
return this.present(await this.writeLine(opened.id, { variantId, qty, lineId: null, removing: false }));
|
|
1435
1455
|
}
|
|
1456
|
+
/** `qty` is the new total; 0 takes the line out. */
|
|
1436
1457
|
async updateCart({
|
|
1437
|
-
qty,
|
|
1438
1458
|
cartId,
|
|
1439
|
-
lineId
|
|
1440
|
-
|
|
1441
|
-
variantId
|
|
1459
|
+
lineId,
|
|
1460
|
+
qty
|
|
1442
1461
|
}) {
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1462
|
+
const id = clean(cartId) ?? storedCartId();
|
|
1463
|
+
if (!id) return emptyCart(await this.currency());
|
|
1464
|
+
return this.setQuantity(id, lineId, Number(qty));
|
|
1465
|
+
}
|
|
1466
|
+
async removeCart({ cartId, lineId }) {
|
|
1467
|
+
const id = clean(cartId) ?? storedCartId();
|
|
1468
|
+
if (!id) return emptyCart(await this.currency());
|
|
1469
|
+
return this.present(
|
|
1470
|
+
await this.data(`/api/carts/${encodeURIComponent(id)}/line-items/${encodeURIComponent(String(lineId))}`, {
|
|
1471
|
+
method: "DELETE"
|
|
1472
|
+
})
|
|
1473
|
+
);
|
|
1447
1474
|
}
|
|
1448
1475
|
/**
|
|
1449
|
-
*
|
|
1450
|
-
*
|
|
1451
|
-
*
|
|
1452
|
-
* fields are arguments to checkout. They are kept against this cart id in the browser and sent
|
|
1453
|
-
* when the order is placed — see checkout-details.ts. The email additionally seeds a cart that
|
|
1454
|
-
* has not been opened yet, which is the one moment GoCommerce does accept it.
|
|
1476
|
+
* Contact details and addresses, from the checkout's address step. The
|
|
1477
|
+
* email goes to the engine — it is how an abandoned cart is followed up —
|
|
1478
|
+
* and the rest is kept for the checkout request.
|
|
1455
1479
|
*/
|
|
1456
|
-
async updateCart2({
|
|
1457
|
-
cartId
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
shippingAddress
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
let id = usableId(cartId) || readStoredCartId();
|
|
1465
|
-
let cart = id ? await this.readCart(id) : null;
|
|
1466
|
-
if (!cart) {
|
|
1467
|
-
cart = await this.openCart(email);
|
|
1468
|
-
id = cart?.id ?? null;
|
|
1480
|
+
async updateCart2(args) {
|
|
1481
|
+
const id = clean(args.cartId) ?? storedCartId();
|
|
1482
|
+
if (!id) return emptyCart(await this.currency());
|
|
1483
|
+
const patch = {};
|
|
1484
|
+
if (args.phone !== void 0) patch.phone = args.phone || void 0;
|
|
1485
|
+
if (args.shippingAddress !== void 0) {
|
|
1486
|
+
patch.shippingAddress = args.shippingAddress;
|
|
1487
|
+
if (args.shippingAddress?.phone && !patch.phone) patch.phone = String(args.shippingAddress.phone);
|
|
1469
1488
|
}
|
|
1470
|
-
if (
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1489
|
+
if (args.billingAddress !== void 0 || args.isBillingAddressSameAsShipping) {
|
|
1490
|
+
patch.billingAddress = args.isBillingAddressSameAsShipping ? null : args.billingAddress ?? null;
|
|
1491
|
+
}
|
|
1492
|
+
const before = cartExtras(id);
|
|
1493
|
+
const country = (a) => String(a?.countryCode || a?.country || "").toUpperCase();
|
|
1494
|
+
if (args.shippingAddress !== void 0 && country(args.shippingAddress) !== country(before.shippingAddress)) {
|
|
1495
|
+
patch.shippingRateId = null;
|
|
1496
|
+
patch.shippingRate = null;
|
|
1497
|
+
}
|
|
1498
|
+
saveCartExtras(id, patch);
|
|
1499
|
+
const email = args.email ?? (args.shippingAddress?.email ? String(args.shippingAddress.email) : void 0);
|
|
1500
|
+
if (email !== void 0) {
|
|
1501
|
+
return this.present(
|
|
1502
|
+
await this.data(`/api/carts/${encodeURIComponent(id)}/email`, { method: "PUT", body: { email } })
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
return this.getCartByCartId(id);
|
|
1506
|
+
}
|
|
1507
|
+
/**
|
|
1508
|
+
* A discount code. The engine prices it against the basket now and decides
|
|
1509
|
+
* it again at checkout; what it answers is kept so the bag can show it,
|
|
1510
|
+
* because the cart itself does not carry the code.
|
|
1511
|
+
*/
|
|
1512
|
+
async applyCoupon({ cartId, couponCode }) {
|
|
1513
|
+
const id = clean(cartId) ?? storedCartId();
|
|
1514
|
+
if (!id) throw new GoCommerceError("Add something to your bag before applying a code.", 400);
|
|
1515
|
+
const cart = await this.data(`/api/carts/${encodeURIComponent(id)}`);
|
|
1516
|
+
const discount = await this.data(`/api/carts/${encodeURIComponent(id)}/discount`, {
|
|
1517
|
+
method: "PUT",
|
|
1518
|
+
body: { code: String(couponCode ?? "").trim(), ...cart.email ? { email: cart.email } : {} }
|
|
1478
1519
|
});
|
|
1520
|
+
saveCartExtras(id, { discount: { ...discount, code: discount.code || String(couponCode).trim() } });
|
|
1479
1521
|
return this.present(cart);
|
|
1480
1522
|
}
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
return this.
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
const
|
|
1491
|
-
if (
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
throw new
|
|
1523
|
+
async removeCoupon(cartId) {
|
|
1524
|
+
const id = clean(cartId) ?? storedCartId();
|
|
1525
|
+
if (!id) return emptyCart(await this.currency());
|
|
1526
|
+
await this.engine(`/api/carts/${encodeURIComponent(id)}/discount`, { method: "DELETE" });
|
|
1527
|
+
saveCartExtras(id, { discount: null });
|
|
1528
|
+
return this.getCartByCartId(id);
|
|
1529
|
+
}
|
|
1530
|
+
/** The delivery option the shopper picked, priced from the engine's own quote. */
|
|
1531
|
+
async updateShippingRate({ cartId, shippingRateId }) {
|
|
1532
|
+
const id = clean(cartId) ?? storedCartId();
|
|
1533
|
+
if (!id) return emptyCart(await this.currency());
|
|
1534
|
+
const ship = cartExtras(id).shippingAddress;
|
|
1535
|
+
const country = String(ship?.countryCode || ship?.country || "").toUpperCase();
|
|
1536
|
+
if (!country) throw new GoCommerceError("Add a delivery address first.", 400);
|
|
1537
|
+
const quote2 = await this.data("/api/checkout/rates", { query: { cart: id, country, state: ship?.state ? String(ship.state) : void 0 } });
|
|
1538
|
+
const rate = (quote2.rates ?? []).find((r) => String(r.rate_id) === String(shippingRateId));
|
|
1539
|
+
if (!rate) throw new GoCommerceError("That delivery option is not available for this address any more.", 409);
|
|
1540
|
+
saveCartExtras(id, {
|
|
1541
|
+
shippingRateId: String(rate.rate_id),
|
|
1542
|
+
shippingRate: {
|
|
1543
|
+
id: String(rate.rate_id),
|
|
1544
|
+
name: rate.name,
|
|
1545
|
+
minor: rate.price.amount_minor,
|
|
1546
|
+
currency: rate.price.currency
|
|
1547
|
+
}
|
|
1548
|
+
});
|
|
1549
|
+
return this.getCartByCartId(id);
|
|
1550
|
+
}
|
|
1551
|
+
/** The payment method is chosen at checkout, by which checkout route is called. */
|
|
1552
|
+
async updateCartPaymentMethod({ cartId }) {
|
|
1553
|
+
return this.getCartByCartId(clean(cartId) ?? storedCartId());
|
|
1554
|
+
}
|
|
1555
|
+
async completeCart() {
|
|
1556
|
+
throw new NotSupportedError("CartService", "completeCart", "an order is placed through checkoutService");
|
|
1557
|
+
}
|
|
1558
|
+
// ------------------------------------------------------------ internals
|
|
1559
|
+
async setQuantity(cartId, lineId, qty) {
|
|
1560
|
+
const path = `/api/carts/${encodeURIComponent(cartId)}/line-items/${encodeURIComponent(String(lineId))}`;
|
|
1561
|
+
if (qty <= 0) return this.present(await this.data(path, { method: "DELETE" }));
|
|
1562
|
+
return this.present(await this.data(path, { method: "PATCH", body: { quantity: qty } }));
|
|
1563
|
+
}
|
|
1564
|
+
/** A new cart, remembered — and priced for the signed-in shopper's group when the engine allows it. */
|
|
1565
|
+
async open() {
|
|
1566
|
+
const channel = this.creds.channel;
|
|
1567
|
+
const cart = await this.data("/api/carts", { method: "POST", ...channel ? { body: { channel } } : {} });
|
|
1568
|
+
rememberCartId(cart.id);
|
|
1569
|
+
if (sessionToken()) await this.claimCartForAccount(cart.id);
|
|
1570
|
+
return cart.id;
|
|
1571
|
+
}
|
|
1572
|
+
async currency() {
|
|
1573
|
+
try {
|
|
1574
|
+
return (await this.facts.ready()).currency;
|
|
1575
|
+
} catch {
|
|
1576
|
+
return "USD";
|
|
1577
|
+
}
|
|
1498
1578
|
}
|
|
1499
|
-
|
|
1500
|
-
|
|
1579
|
+
/** The engine's cart with the browser-held extras and each line's picture and link. */
|
|
1580
|
+
async present(cart) {
|
|
1581
|
+
const ids = [...new Set((cart.line_items ?? []).map((l) => l.product_id))];
|
|
1582
|
+
const products = /* @__PURE__ */ new Map();
|
|
1583
|
+
await Promise.all(
|
|
1584
|
+
ids.map(async (pid) => {
|
|
1585
|
+
products.set(pid, await this.productFor(pid));
|
|
1586
|
+
})
|
|
1587
|
+
);
|
|
1588
|
+
return toCart(cart, cartExtras(cart.id), (l) => {
|
|
1589
|
+
const p = products.get(l.product_id);
|
|
1590
|
+
if (!p) return {};
|
|
1591
|
+
const v = (p.variants ?? []).find((x) => x.id === l.variant_id);
|
|
1592
|
+
return { slug: p.slug, thumbnail: v?.image?.url ?? p.image_url ?? null };
|
|
1593
|
+
});
|
|
1501
1594
|
}
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1595
|
+
productFor(id) {
|
|
1596
|
+
return remember(
|
|
1597
|
+
`${this.engineBase()}|product|${id}`,
|
|
1598
|
+
() => this.data(`/api/products/${id}`).catch(() => null)
|
|
1599
|
+
);
|
|
1505
1600
|
}
|
|
1506
1601
|
};
|
|
1507
|
-
var
|
|
1602
|
+
var clean = (id) => id && id !== "undefined" && id !== "null" ? id : null;
|
|
1603
|
+
var cartService = new CartService();
|
|
1508
1604
|
|
|
1509
|
-
// src/services/checkout.ts
|
|
1510
|
-
var
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
};
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
};
|
|
1532
|
-
var titleCase = (code) => code.charAt(0).toUpperCase() + code.slice(1);
|
|
1533
|
-
var PaymentMethodService = class _PaymentMethodService extends BaseService {
|
|
1534
|
-
static instance;
|
|
1535
|
-
static getInstance() {
|
|
1536
|
-
if (!_PaymentMethodService.instance) _PaymentMethodService.instance = new _PaymentMethodService();
|
|
1537
|
-
return _PaymentMethodService.instance;
|
|
1538
|
-
}
|
|
1539
|
-
/**
|
|
1540
|
-
* What this store accepts, from `GET /api/checkout`.
|
|
1541
|
-
*
|
|
1542
|
-
* The storefront branches on an uppercase code (`COD`, `STRIPE`, …) while GoCommerce installs
|
|
1543
|
-
* them lowercase, so the code is uppercased for the UI and the original kept for the API call.
|
|
1544
|
-
*/
|
|
1545
|
-
async list(_params = {}) {
|
|
1546
|
-
const res = await this.api("GET", "/api/checkout");
|
|
1547
|
-
const codes = res.data?.payment_methods || [];
|
|
1548
|
-
installedCodes = codes;
|
|
1549
|
-
const data = codes.map((code, index) => ({
|
|
1550
|
-
id: code,
|
|
1551
|
-
code: code.toUpperCase(),
|
|
1552
|
-
name: METHOD_NAMES[code.toLowerCase()] || titleCase(code),
|
|
1553
|
-
description: "",
|
|
1554
|
-
active: true,
|
|
1555
|
-
isTest: false,
|
|
1556
|
-
img: null,
|
|
1557
|
-
rank: index,
|
|
1558
|
-
currency: res.data?.currency || null
|
|
1559
|
-
}));
|
|
1560
|
-
return { data, count: data.length, pageSize: data.length, noOfPage: data.length ? 1 : 0, page: 1 };
|
|
1605
|
+
// src/services/checkout-service.ts
|
|
1606
|
+
var CheckoutService = class extends BaseService {
|
|
1607
|
+
/** Delivery options for the address on the cart: `{ data: [{ id, name, base_rate }] }`. */
|
|
1608
|
+
async getShippingRates({ cartId } = {}) {
|
|
1609
|
+
const id = cartId || storedCartId();
|
|
1610
|
+
const ship = id ? cartExtras(id).shippingAddress : null;
|
|
1611
|
+
const country = String(ship?.countryCode || ship?.country || "").toUpperCase();
|
|
1612
|
+
if (!id || !country) return { data: [] };
|
|
1613
|
+
const quote2 = await this.data("/api/checkout/rates", { query: { cart: id, country, state: ship?.state ? String(ship.state) : void 0 } });
|
|
1614
|
+
return {
|
|
1615
|
+
data: (quote2.rates ?? []).map((r) => ({
|
|
1616
|
+
id: String(r.rate_id),
|
|
1617
|
+
name: r.name,
|
|
1618
|
+
description: null,
|
|
1619
|
+
base_rate: minorToMajor(r.price.amount_minor, r.price.currency),
|
|
1620
|
+
price: minorToMajor(r.price.amount_minor, r.price.currency),
|
|
1621
|
+
currencyCode: r.price.currency,
|
|
1622
|
+
// The engine's rates carry no delivery estimate; none is made up.
|
|
1623
|
+
estimated_min_days: null,
|
|
1624
|
+
estimated_max_days: null
|
|
1625
|
+
}))
|
|
1626
|
+
};
|
|
1561
1627
|
}
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1628
|
+
/** Cash on delivery: the order is confirmed at once and paid on the doorstep. */
|
|
1629
|
+
async checkoutCOD({ cartId } = {}) {
|
|
1630
|
+
const result = await this.place("cod", cartId);
|
|
1631
|
+
return {
|
|
1632
|
+
order_no: result.order.number,
|
|
1633
|
+
orderNo: result.order.number,
|
|
1634
|
+
id: String(result.order.id),
|
|
1635
|
+
status: result.order.status
|
|
1636
|
+
};
|
|
1569
1637
|
}
|
|
1570
1638
|
/**
|
|
1571
|
-
*
|
|
1572
|
-
*
|
|
1573
|
-
*
|
|
1574
|
-
*
|
|
1575
|
-
* error rather than sent as blanks, because GoCommerce's own validation message ("address.line1
|
|
1576
|
-
* is required") does not tell a shopper which screen to go back to.
|
|
1639
|
+
* Razorpay's modal. The engine has already created the Razorpay order; this
|
|
1640
|
+
* hands the storefront what the modal needs. A store running Razorpay's
|
|
1641
|
+
* hosted (redirect) mode cannot be driven from this storefront's modal flow,
|
|
1642
|
+
* and says so rather than opening a modal on an order it cannot pay.
|
|
1577
1643
|
*/
|
|
1578
|
-
async
|
|
1579
|
-
const
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
const code = goCommerceCode(storefrontCode);
|
|
1586
|
-
let res;
|
|
1587
|
-
try {
|
|
1588
|
-
res = await this.api(
|
|
1589
|
-
"POST",
|
|
1590
|
-
`/api/checkout/${encodeURIComponent(code)}`,
|
|
1591
|
-
{
|
|
1592
|
-
cart_id: id,
|
|
1593
|
-
email: details.email,
|
|
1594
|
-
...details.name ? { name: details.name } : {},
|
|
1595
|
-
...details.phone ? { phone: details.phone } : {},
|
|
1596
|
-
address,
|
|
1597
|
-
...returnUrl || origin ? { return_url: returnUrl || `${origin}/checkout/process` } : {}
|
|
1598
|
-
},
|
|
1599
|
-
// A double-submitted Place Order must not become two orders and two inventory holds.
|
|
1600
|
-
{ "Idempotency-Key": `${id}:${code}` }
|
|
1644
|
+
async checkoutRazorpay({ cartId } = {}) {
|
|
1645
|
+
const result = await this.place("razorpay", cartId);
|
|
1646
|
+
const data = result.payment.client_data ?? {};
|
|
1647
|
+
if (result.payment.kind !== "client_action" || !data.razorpay_order_id) {
|
|
1648
|
+
throw new GoCommerceError(
|
|
1649
|
+
"This store takes Razorpay payments on Razorpay\u2019s own page, which this storefront does not support yet.",
|
|
1650
|
+
0
|
|
1601
1651
|
);
|
|
1602
|
-
} catch (error) {
|
|
1603
|
-
if (await cartService.cartIsSpent(id)) {
|
|
1604
|
-
throw new Error("This cart is no longer available \u2014 it has expired or was already checked out. Add the items to a new bag and try again.");
|
|
1605
|
-
}
|
|
1606
|
-
throw error;
|
|
1607
1652
|
}
|
|
1608
|
-
const order = res.data?.order;
|
|
1609
|
-
const payment = res.data?.payment;
|
|
1610
|
-
rememberOrderToken(order?.number, order?.access_token);
|
|
1611
|
-
clearCheckoutDetails(id);
|
|
1612
|
-
const index = await resolveProductIndex(
|
|
1613
|
-
(order?.line_items || []).map((line) => line.product_id),
|
|
1614
|
-
this.getFetch()
|
|
1615
|
-
);
|
|
1616
1653
|
return {
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
payment: payment ?? null,
|
|
1623
|
-
paymentKind: payment?.kind ?? "none",
|
|
1624
|
-
// A gateway's own fields (a Razorpay order id, a Stripe client secret, a redirect url)
|
|
1625
|
-
// ride on `client_data`; spreading them is what lets the storefront's per-gateway
|
|
1626
|
-
// branches find what they look for without this connector naming each gateway.
|
|
1627
|
-
...payment?.client_data ?? {}
|
|
1654
|
+
order_no: result.order.number,
|
|
1655
|
+
id: data.razorpay_order_id,
|
|
1656
|
+
key_id: data.key_id,
|
|
1657
|
+
amount: Number(data.amount),
|
|
1658
|
+
currency: data.currency
|
|
1628
1659
|
};
|
|
1629
1660
|
}
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
return
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1661
|
+
/** The modal closed with a payment. The engine learns of it from Razorpay itself. */
|
|
1662
|
+
async captureRazorpayPayment({ razorpay_order_id }) {
|
|
1663
|
+
const order = placedOrderByReference(razorpay_order_id);
|
|
1664
|
+
if (!order) throw new GoCommerceError("That payment does not belong to an order placed from this browser.", 404);
|
|
1665
|
+
return { order_no: order.number, status: "processing" };
|
|
1666
|
+
}
|
|
1667
|
+
// The family's other gateways. GoCommerce's Stripe is a Payment Element
|
|
1668
|
+
// (a client secret, not a hosted page), and PayPal, Cashfree, PhonePe and
|
|
1669
|
+
// Affirm are not engine gateways at all; paymentMethodService never lists
|
|
1670
|
+
// them, so these are reached only by a storefront that hard-codes one.
|
|
1671
|
+
async checkoutStripe() {
|
|
1672
|
+
throw unsupported(
|
|
1673
|
+
"checkoutStripe",
|
|
1674
|
+
"GoCommerce\u2019s Stripe gateway uses the Payment Element, not a hosted checkout page"
|
|
1675
|
+
);
|
|
1644
1676
|
}
|
|
1645
|
-
async
|
|
1646
|
-
|
|
1677
|
+
async checkoutStripeCapture() {
|
|
1678
|
+
throw unsupported("checkoutStripeCapture", "Stripe confirms payments to the engine by webhook");
|
|
1647
1679
|
}
|
|
1648
|
-
async
|
|
1649
|
-
|
|
1680
|
+
async checkoutPaypal() {
|
|
1681
|
+
throw unsupported("checkoutPaypal", "the engine has no PayPal gateway");
|
|
1650
1682
|
}
|
|
1651
|
-
async
|
|
1652
|
-
|
|
1653
|
-
origin
|
|
1654
|
-
}) {
|
|
1655
|
-
return this.place("affirm", { cartId, origin });
|
|
1683
|
+
async capturePaypalPayment() {
|
|
1684
|
+
throw unsupported("capturePaypalPayment", "the engine has no PayPal gateway");
|
|
1656
1685
|
}
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
// the storefront's redirect moving with the order number it already has rather than pretending
|
|
1660
|
-
// to perform a capture that this API does not offer.
|
|
1661
|
-
settled(orderNo) {
|
|
1662
|
-
return { order_no: orderNo || "", orderNo: orderNo || "", captured: false };
|
|
1686
|
+
async checkoutCashfree() {
|
|
1687
|
+
throw unsupported("checkoutCashfree", "the engine has no Cashfree gateway");
|
|
1663
1688
|
}
|
|
1664
|
-
async
|
|
1665
|
-
|
|
1689
|
+
async captureCashfreePayment() {
|
|
1690
|
+
throw unsupported("captureCashfreePayment", "the engine has no Cashfree gateway");
|
|
1666
1691
|
}
|
|
1667
|
-
async
|
|
1668
|
-
|
|
1692
|
+
async checkoutPhonepe() {
|
|
1693
|
+
throw unsupported("checkoutPhonepe", "the engine has no PhonePe gateway");
|
|
1669
1694
|
}
|
|
1670
|
-
async
|
|
1671
|
-
|
|
1695
|
+
async createAffirmPayOrder() {
|
|
1696
|
+
throw unsupported("createAffirmPayOrder", "the engine has no Affirm gateway");
|
|
1672
1697
|
}
|
|
1673
|
-
async
|
|
1674
|
-
|
|
1698
|
+
async confirmAffirmOrder() {
|
|
1699
|
+
throw unsupported("confirmAffirmOrder", "the engine has no Affirm gateway");
|
|
1675
1700
|
}
|
|
1676
|
-
async
|
|
1677
|
-
|
|
1701
|
+
async cancelAffirmOrder() {
|
|
1702
|
+
throw unsupported("cancelAffirmOrder", "the engine has no Affirm gateway");
|
|
1678
1703
|
}
|
|
1679
|
-
async
|
|
1680
|
-
throw
|
|
1704
|
+
async checkoutPOS() {
|
|
1705
|
+
throw unsupported("checkoutPOS", "point-of-sale orders are placed from the admin panel");
|
|
1681
1706
|
}
|
|
1682
1707
|
/**
|
|
1683
|
-
*
|
|
1684
|
-
*
|
|
1685
|
-
*
|
|
1708
|
+
* One checkout attempt.
|
|
1709
|
+
*
|
|
1710
|
+
* The Idempotency-Key is kept with the cart and reused while the request is
|
|
1711
|
+
* the same, because a payment gateway that fails after the engine has
|
|
1712
|
+
* created the order leaves the cart converted: only a retry under the same
|
|
1713
|
+
* key resumes that order, and without one the shopper is stuck. A changed
|
|
1714
|
+
* request — a different address, a different method — gets a new key, since
|
|
1715
|
+
* the engine refuses a key replayed with a different body.
|
|
1686
1716
|
*/
|
|
1687
|
-
async
|
|
1688
|
-
|
|
1717
|
+
async place(code, cartId) {
|
|
1718
|
+
const id = cartId || storedCartId();
|
|
1719
|
+
if (!id) throw new GoCommerceError("Your bag is empty.", 400);
|
|
1720
|
+
const extras = cartExtras(id);
|
|
1721
|
+
const ship = extras.shippingAddress;
|
|
1722
|
+
if (!ship) throw new GoCommerceError("Add a delivery address first.", 400);
|
|
1723
|
+
const cart = await this.data(`/api/carts/${encodeURIComponent(id)}`);
|
|
1724
|
+
const email = cart.email || (ship.email ? String(ship.email) : "");
|
|
1725
|
+
if (!email) throw new GoCommerceError("Add an email address so we can send your receipt.", 400);
|
|
1726
|
+
const address = toEngineAddress(ship);
|
|
1727
|
+
const body = {
|
|
1728
|
+
cart_id: id,
|
|
1729
|
+
email,
|
|
1730
|
+
...extras.phone || address.phone ? { phone: String(extras.phone || address.phone) } : {},
|
|
1731
|
+
...address.name ? { name: address.name } : {},
|
|
1732
|
+
address,
|
|
1733
|
+
...extras.shippingRateId ? { shipping_rate_id: Number(extras.shippingRateId) } : {}
|
|
1734
|
+
};
|
|
1735
|
+
const serialised = JSON.stringify(body);
|
|
1736
|
+
const key = extras.attempt?.body === `${code}:${serialised}` ? extras.attempt.key : newKey();
|
|
1737
|
+
saveCartExtras(id, { attempt: { key, body: `${code}:${serialised}` } });
|
|
1738
|
+
const result = await this.data(`/api/checkout/${encodeURIComponent(code)}`, {
|
|
1739
|
+
method: "POST",
|
|
1740
|
+
body,
|
|
1741
|
+
headers: { "Idempotency-Key": key }
|
|
1742
|
+
});
|
|
1743
|
+
if (result.order.access_token) {
|
|
1744
|
+
rememberOrder({
|
|
1745
|
+
number: result.order.number,
|
|
1746
|
+
token: result.order.access_token,
|
|
1747
|
+
cartId: id,
|
|
1748
|
+
reference: result.payment.client_data?.razorpay_order_id ?? result.payment.reference
|
|
1749
|
+
});
|
|
1750
|
+
if (sessionToken()) {
|
|
1751
|
+
await this.engine("/x/identity/me/orders", {
|
|
1752
|
+
method: "POST",
|
|
1753
|
+
body: { number: result.order.number, token: result.order.access_token },
|
|
1754
|
+
auth: true
|
|
1755
|
+
}).catch(() => void 0);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
saveCartExtras(id, { attempt: null });
|
|
1759
|
+
return result;
|
|
1689
1760
|
}
|
|
1690
1761
|
};
|
|
1691
|
-
var
|
|
1762
|
+
var unsupported = (method, why) => new NotSupportedError("CheckoutService", method, why);
|
|
1763
|
+
function newKey() {
|
|
1764
|
+
const c = globalThis.crypto;
|
|
1765
|
+
if (c?.randomUUID) return c.randomUUID();
|
|
1766
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}-${Math.random().toString(36).slice(2)}`;
|
|
1767
|
+
}
|
|
1768
|
+
var checkoutService = new CheckoutService();
|
|
1692
1769
|
|
|
1693
|
-
// src/services/
|
|
1694
|
-
var
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
async
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1770
|
+
// src/services/payment-method-service.ts
|
|
1771
|
+
var DRIVABLE = {
|
|
1772
|
+
cod: { code: "COD", img: "/payment/cod.svg" },
|
|
1773
|
+
razorpay: { code: "RAZORPAY" }
|
|
1774
|
+
};
|
|
1775
|
+
var reported2 = /* @__PURE__ */ new Set();
|
|
1776
|
+
var PaymentMethodService = class extends BaseService {
|
|
1777
|
+
facts = new StoreFacts(this._fetch);
|
|
1778
|
+
async list(_args = {}) {
|
|
1779
|
+
const options = await this.facts.checkoutOptions();
|
|
1780
|
+
const methods = options.methods ?? [];
|
|
1781
|
+
const data = methods.filter((m) => {
|
|
1782
|
+
if (DRIVABLE[m.code]) return true;
|
|
1783
|
+
if (!reported2.has(m.code)) {
|
|
1784
|
+
reported2.add(m.code);
|
|
1785
|
+
console.warn(
|
|
1786
|
+
`[gocommerce] payment method "${m.code}" is switched on but this storefront cannot drive its flow, so it is not offered.`
|
|
1787
|
+
);
|
|
1788
|
+
}
|
|
1789
|
+
return false;
|
|
1790
|
+
}).map((m, i) => ({
|
|
1791
|
+
id: m.code,
|
|
1792
|
+
code: DRIVABLE[m.code].code,
|
|
1793
|
+
engineCode: m.code,
|
|
1794
|
+
name: m.name,
|
|
1795
|
+
title: m.name,
|
|
1796
|
+
description: "",
|
|
1797
|
+
img: DRIVABLE[m.code].img ?? null,
|
|
1798
|
+
rank: i,
|
|
1799
|
+
active: true,
|
|
1800
|
+
isActive: true
|
|
1801
|
+
}));
|
|
1802
|
+
return { data, count: data.length, pageSize: data.length, noOfPage: 1, page: 1 };
|
|
1708
1803
|
}
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
return
|
|
1804
|
+
};
|
|
1805
|
+
var paymentMethodService = new PaymentMethodService();
|
|
1806
|
+
|
|
1807
|
+
// src/services/coupon-service.ts
|
|
1808
|
+
var CouponService = class extends BaseService {
|
|
1809
|
+
async listCoupons(_args = {}) {
|
|
1810
|
+
return emptyPage();
|
|
1716
1811
|
}
|
|
1812
|
+
};
|
|
1813
|
+
var couponService = new CouponService();
|
|
1814
|
+
|
|
1815
|
+
// src/services/order-service.ts
|
|
1816
|
+
var OrderService = class extends BaseService {
|
|
1717
1817
|
/**
|
|
1718
|
-
* The
|
|
1818
|
+
* The order-success page's lookup, by the number and cart in its URL.
|
|
1719
1819
|
*
|
|
1720
|
-
*
|
|
1721
|
-
*
|
|
1820
|
+
* It runs on the server, where neither the token nor the session exists —
|
|
1821
|
+
* they live in the shopper's browser — so there it answers nothing and the
|
|
1822
|
+
* page says the details are not available yet. In the browser it answers
|
|
1823
|
+
* the order. Inventing a server-side credential from the cart token in the
|
|
1824
|
+
* URL would make that URL, which lands in history and referrers, a key to
|
|
1825
|
+
* the shopper's address.
|
|
1722
1826
|
*/
|
|
1723
|
-
async
|
|
1724
|
-
|
|
1827
|
+
async listOrdersByParent({ orderNo, cartId } = {}) {
|
|
1828
|
+
const number = clean2(orderNo) ?? (clean2(cartId) ? placedOrderForCart(clean2(cartId))?.number : void 0);
|
|
1829
|
+
if (!number) return { data: [] };
|
|
1725
1830
|
try {
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1831
|
+
return { data: [await this.getOrder(number)] };
|
|
1832
|
+
} catch (err) {
|
|
1833
|
+
if (err instanceof GoCommerceError && (err.status === 404 || err.status === 401)) return { data: [] };
|
|
1834
|
+
throw err;
|
|
1730
1835
|
}
|
|
1731
|
-
if (!cart) return null;
|
|
1732
|
-
const lines = cart.line_items || [];
|
|
1733
|
-
const index = await resolveProductIndex(
|
|
1734
|
-
lines.map((line) => line.product_id),
|
|
1735
|
-
this.getFetch()
|
|
1736
|
-
);
|
|
1737
|
-
const subtotal = fromMoney(cart.subtotal);
|
|
1738
|
-
return {
|
|
1739
|
-
id: null,
|
|
1740
|
-
orderNo: orderNumber,
|
|
1741
|
-
order_no: orderNumber,
|
|
1742
|
-
parentOrderNo: orderNumber,
|
|
1743
|
-
status: cart.status === "converted" ? "confirmed" : "pending",
|
|
1744
|
-
paymentStatus: "pending",
|
|
1745
|
-
paymentMethod: null,
|
|
1746
|
-
createdAt: cart.updated_at || cart.created_at || null,
|
|
1747
|
-
updatedAt: cart.updated_at || null,
|
|
1748
|
-
userEmail: cart.email || "",
|
|
1749
|
-
phone: "",
|
|
1750
|
-
subtotal,
|
|
1751
|
-
shippingCharges: 0,
|
|
1752
|
-
discount: 0,
|
|
1753
|
-
total: subtotal,
|
|
1754
|
-
currencyCode: cart.currency,
|
|
1755
|
-
shippingRate: null,
|
|
1756
|
-
fulfillments: [],
|
|
1757
|
-
shippingAddress: null,
|
|
1758
|
-
billingAddress: null,
|
|
1759
|
-
lineItems: lines.map((line) => {
|
|
1760
|
-
const product = line.product_id == null ? void 0 : index.get(line.product_id);
|
|
1761
|
-
const mapped = product ? mapProduct(product) : null;
|
|
1762
|
-
const variant = product?.variants?.find((v) => v.id === line.variant_id);
|
|
1763
|
-
return {
|
|
1764
|
-
id: line.id,
|
|
1765
|
-
productId: line.product_id,
|
|
1766
|
-
variantId: line.variant_id,
|
|
1767
|
-
title: line.title || mapped?.title || "",
|
|
1768
|
-
variantTitle: line.variant_label || line.sku || "",
|
|
1769
|
-
slug: mapped?.slug || "",
|
|
1770
|
-
sku: line.sku || "",
|
|
1771
|
-
thumbnail: absoluteMedia(variant?.image?.url) || mapped?.thumbnail || null,
|
|
1772
|
-
qty: line.quantity,
|
|
1773
|
-
price: fromMoney(line.unit_price),
|
|
1774
|
-
subtotal: fromMoney(line.total),
|
|
1775
|
-
total: fromMoney(line.total)
|
|
1776
|
-
};
|
|
1777
|
-
})
|
|
1778
|
-
};
|
|
1779
1836
|
}
|
|
1780
|
-
/**
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
const
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
return token ? this.present(await this.readOrder(number, token)) : null;
|
|
1793
|
-
})
|
|
1794
|
-
);
|
|
1795
|
-
return paginated(orders.filter(Boolean), numbers.length, page);
|
|
1796
|
-
}
|
|
1797
|
-
/** What the confirmation page loads with, server-side, straight after checkout. */
|
|
1798
|
-
async listOrdersByParent({ orderNo, cartId }) {
|
|
1799
|
-
const number = usable(orderNo);
|
|
1800
|
-
if (!number) return emptyPage();
|
|
1801
|
-
const token = readOrderToken(number);
|
|
1802
|
-
if (token) {
|
|
1803
|
-
const order = await this.present(await this.readOrder(number, token));
|
|
1804
|
-
if (order) return paginated([order], 1, 1, 1);
|
|
1837
|
+
/** The signed-in shopper's orders, newest claim first. */
|
|
1838
|
+
async list({ page = 1, limit = 20 } = {}) {
|
|
1839
|
+
const env = await this.engine("/x/identity/me/orders", { query: { page, limit }, auth: true });
|
|
1840
|
+
return toPage(env, toOrder);
|
|
1841
|
+
}
|
|
1842
|
+
/** One order by number: the account's when signed in, otherwise one this browser placed. */
|
|
1843
|
+
async getOrder(number) {
|
|
1844
|
+
const kept = placedOrder(number);
|
|
1845
|
+
if (kept) {
|
|
1846
|
+
return toOrder(
|
|
1847
|
+
await this.data(`/api/orders/${encodeURIComponent(number)}`, { query: { token: kept.token } })
|
|
1848
|
+
);
|
|
1805
1849
|
}
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
const reconstructed = await this.orderFromCart(cart, number);
|
|
1809
|
-
if (reconstructed) return paginated([reconstructed], 1, 1, 1);
|
|
1850
|
+
if (sessionToken()) {
|
|
1851
|
+
return toOrder(await this.data(`/x/identity/me/orders/${encodeURIComponent(number)}`, { auth: true }));
|
|
1810
1852
|
}
|
|
1811
|
-
|
|
1853
|
+
throw new GoCommerceError(
|
|
1854
|
+
inBrowser() ? "Sign in to see this order, or open it from the browser that placed it." : "order not found",
|
|
1855
|
+
404,
|
|
1856
|
+
"not_found"
|
|
1857
|
+
);
|
|
1812
1858
|
}
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
const
|
|
1817
|
-
|
|
1818
|
-
|
|
1859
|
+
/** Everything the signed-in shopper has bought, once each, most recent first. */
|
|
1860
|
+
async buyAgain() {
|
|
1861
|
+
const orders = await this.data("/x/identity/me/orders", { query: { limit: 20 }, auth: true }) ?? [];
|
|
1862
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1863
|
+
const lines = [];
|
|
1864
|
+
for (const o of orders) {
|
|
1865
|
+
for (const l of o.line_items ?? []) {
|
|
1866
|
+
if (!l.product_id || !l.variant_id || seen.has(l.variant_id)) continue;
|
|
1867
|
+
seen.add(l.variant_id);
|
|
1868
|
+
lines.push({
|
|
1869
|
+
productId: l.product_id,
|
|
1870
|
+
variantId: l.variant_id,
|
|
1871
|
+
title: l.title,
|
|
1872
|
+
img: l.image_url ?? null,
|
|
1873
|
+
price: toMajor(l.unit_price),
|
|
1874
|
+
qty: l.quantity
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
const live = await Promise.all(
|
|
1879
|
+
lines.map((l) => this.data(`/api/products/${l.productId}`).catch(() => null))
|
|
1880
|
+
);
|
|
1881
|
+
return lines.flatMap((l, i) => {
|
|
1882
|
+
const p = live[i];
|
|
1883
|
+
if (!p) return [];
|
|
1884
|
+
return [{ ...l, productId: String(l.productId), variantId: String(l.variantId), slug: p.slug }];
|
|
1885
|
+
});
|
|
1819
1886
|
}
|
|
1820
|
-
|
|
1821
|
-
|
|
1887
|
+
/** The "someone just bought" popup would publish other shoppers' purchases; the engine offers none. */
|
|
1888
|
+
async listPublic(_args = {}) {
|
|
1889
|
+
return emptyPage();
|
|
1822
1890
|
}
|
|
1823
|
-
async
|
|
1824
|
-
|
|
1891
|
+
async getOrderByEmailAndOTP() {
|
|
1892
|
+
throw new NotSupportedError(
|
|
1893
|
+
"OrderService",
|
|
1894
|
+
"getOrderByEmailAndOTP",
|
|
1895
|
+
"the engine sends no one-time codes; a guest reads an order with its access token"
|
|
1896
|
+
);
|
|
1825
1897
|
}
|
|
1826
|
-
|
|
1827
|
-
|
|
1898
|
+
};
|
|
1899
|
+
var clean2 = (v) => v && v !== "undefined" && v !== "null" ? v : void 0;
|
|
1900
|
+
var orderService = new OrderService();
|
|
1901
|
+
|
|
1902
|
+
// src/services/user-service.ts
|
|
1903
|
+
var UserService = class extends BaseService {
|
|
1904
|
+
/** Email and password. The answer is the user object the storefront keeps in its `me` cookie. */
|
|
1905
|
+
async login({ email, password }) {
|
|
1906
|
+
const auth = await this.account("/x/identity/login", {
|
|
1907
|
+
method: "POST",
|
|
1908
|
+
body: { email: String(email ?? "").trim(), password: String(password ?? "") }
|
|
1909
|
+
});
|
|
1910
|
+
return this.signedIn(auth);
|
|
1828
1911
|
}
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1912
|
+
async signup({
|
|
1913
|
+
email,
|
|
1914
|
+
password,
|
|
1915
|
+
firstName,
|
|
1916
|
+
lastName,
|
|
1917
|
+
phone
|
|
1918
|
+
}) {
|
|
1919
|
+
const name = [firstName, lastName].filter(Boolean).join(" ").trim();
|
|
1920
|
+
const auth = await this.account("/x/identity/register", {
|
|
1921
|
+
method: "POST",
|
|
1922
|
+
body: {
|
|
1923
|
+
email: String(email ?? "").trim(),
|
|
1924
|
+
password: String(password ?? ""),
|
|
1925
|
+
...name ? { name } : {},
|
|
1926
|
+
...phone ? { phone: String(phone) } : {}
|
|
1927
|
+
}
|
|
1928
|
+
});
|
|
1929
|
+
return this.signedIn(auth);
|
|
1832
1930
|
}
|
|
1833
|
-
|
|
1834
|
-
async
|
|
1835
|
-
|
|
1931
|
+
/** Signs out here and on the engine. The storefront assigns the answer to its user: `null`. */
|
|
1932
|
+
async logout() {
|
|
1933
|
+
await this.engine("/x/identity/logout", { method: "POST", auth: true }).catch(() => void 0);
|
|
1934
|
+
endSession();
|
|
1935
|
+
return null;
|
|
1836
1936
|
}
|
|
1837
|
-
async
|
|
1838
|
-
return
|
|
1937
|
+
async getMe() {
|
|
1938
|
+
return toUser(await this.account("/x/identity/me", { auth: true }));
|
|
1839
1939
|
}
|
|
1840
|
-
async
|
|
1841
|
-
|
|
1940
|
+
async updateProfile(user) {
|
|
1941
|
+
const name = user.name ?? [user.firstName, user.lastName].filter((v) => v !== void 0).join(" ").trim();
|
|
1942
|
+
const c = await this.account("/x/identity/me", {
|
|
1943
|
+
method: "PATCH",
|
|
1944
|
+
auth: true,
|
|
1945
|
+
body: {
|
|
1946
|
+
...name !== void 0 ? { name } : {},
|
|
1947
|
+
...user.email ? { email: String(user.email).trim() } : {},
|
|
1948
|
+
...user.phone !== void 0 && user.phone !== null ? { phone: String(user.phone) } : {}
|
|
1949
|
+
}
|
|
1950
|
+
});
|
|
1951
|
+
updateMe(c);
|
|
1952
|
+
return toUser(c);
|
|
1842
1953
|
}
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1954
|
+
/**
|
|
1955
|
+
* A password change ends every session the account had, this one included,
|
|
1956
|
+
* and the engine answers a fresh one — which is kept, so the shopper stays
|
|
1957
|
+
* signed in here and nowhere else.
|
|
1958
|
+
*/
|
|
1959
|
+
async changePassword({ old, oldPassword, password }) {
|
|
1960
|
+
const auth = await this.account("/x/identity/me/password", {
|
|
1961
|
+
method: "PUT",
|
|
1962
|
+
auth: true,
|
|
1963
|
+
body: { current_password: String(old ?? oldPassword ?? ""), password: String(password ?? "") }
|
|
1964
|
+
});
|
|
1965
|
+
startSession(auth);
|
|
1966
|
+
return toUser(auth.record);
|
|
1847
1967
|
}
|
|
1848
|
-
async
|
|
1849
|
-
throw new
|
|
1968
|
+
async deleteUser() {
|
|
1969
|
+
throw new NotSupportedError("UserService", "deleteUser", "an account is closed by the store, on request");
|
|
1850
1970
|
}
|
|
1851
|
-
async
|
|
1852
|
-
throw new
|
|
1971
|
+
async checkEmail() {
|
|
1972
|
+
throw new NotSupportedError("UserService", "checkEmail", "the engine does not say which addresses have accounts");
|
|
1853
1973
|
}
|
|
1854
|
-
async
|
|
1855
|
-
throw new
|
|
1974
|
+
async joinAsVendor() {
|
|
1975
|
+
throw new NotSupportedError("UserService", "joinAsVendor", "sellers are invited by the store");
|
|
1856
1976
|
}
|
|
1857
|
-
async
|
|
1858
|
-
|
|
1977
|
+
async signedIn(auth) {
|
|
1978
|
+
const me = startSession(auth);
|
|
1979
|
+
const cartId = storedCartId();
|
|
1980
|
+
if (cartId) await this.claimCartForAccount(cartId);
|
|
1981
|
+
return me;
|
|
1859
1982
|
}
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
}
|
|
1871
|
-
async getStoreByIdOrDomain(_params = {}) {
|
|
1872
|
-
const store = await readStaticStoreDetails();
|
|
1873
|
-
if (!store) {
|
|
1874
|
-
throw new Error("No store configuration is registered \u2014 check kitcommerce.config.ts (see docs/GOCOMMERCE.md).");
|
|
1983
|
+
/** An identity call, with a 404 from a build without the module said in words. */
|
|
1984
|
+
async account(path, req = {}) {
|
|
1985
|
+
try {
|
|
1986
|
+
return await this.data(path, req);
|
|
1987
|
+
} catch (err) {
|
|
1988
|
+
if (err instanceof GoCommerceError && err.status === 404 && /no route/i.test(err.message)) {
|
|
1989
|
+
throw new GoCommerceError("Accounts are not available on this store.", 404, "not_found");
|
|
1990
|
+
}
|
|
1991
|
+
if (err instanceof GoCommerceError && err.status === 401 && req.auth) endSession();
|
|
1992
|
+
throw err;
|
|
1875
1993
|
}
|
|
1876
|
-
return store;
|
|
1877
|
-
}
|
|
1878
|
-
/** The settings store asks for the record by id; there is only ever the configured one. */
|
|
1879
|
-
async getStore(_storeId) {
|
|
1880
|
-
return this.getStoreByIdOrDomain({});
|
|
1881
1994
|
}
|
|
1882
1995
|
};
|
|
1883
|
-
var
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
}
|
|
1890
|
-
/** The nav reads `response.data` and filters it on `menuId`, the way the Litekart API served it. */
|
|
1891
|
-
async list() {
|
|
1892
|
-
const store = await readStaticStore();
|
|
1893
|
-
return { data: store?.menu ?? [] };
|
|
1996
|
+
var AuthService = class extends UserService {
|
|
1997
|
+
async forgotPassword({ email }) {
|
|
1998
|
+
return this.account("/x/identity/password-reset", {
|
|
1999
|
+
method: "POST",
|
|
2000
|
+
body: { email: String(email ?? "").trim() }
|
|
2001
|
+
});
|
|
1894
2002
|
}
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
return
|
|
2003
|
+
/** The token arrives in the link the engine emailed (`GOCOMMERCE_IDENTITY_RESET_URL`). */
|
|
2004
|
+
async resetPassword({ token, password }) {
|
|
2005
|
+
const auth = await this.account("/x/identity/password-reset/confirm", {
|
|
2006
|
+
method: "POST",
|
|
2007
|
+
body: { token: String(token ?? ""), password: String(password ?? "") }
|
|
2008
|
+
});
|
|
2009
|
+
return startSession(auth);
|
|
1902
2010
|
}
|
|
1903
|
-
|
|
1904
|
-
async
|
|
1905
|
-
|
|
2011
|
+
/** Answers `{ errorCode, message }` on failure rather than throwing: that is how the page reads it. */
|
|
2012
|
+
async verifyEmail(_email, token) {
|
|
2013
|
+
try {
|
|
2014
|
+
const c = await this.account("/x/identity/email-verification/confirm", {
|
|
2015
|
+
method: "POST",
|
|
2016
|
+
body: { token: String(token ?? "") }
|
|
2017
|
+
});
|
|
2018
|
+
return { ...toUser(c), message: "Your email address is confirmed." };
|
|
2019
|
+
} catch (err) {
|
|
2020
|
+
return { errorCode: err.code ?? "invalid_token", message: err.message };
|
|
2021
|
+
}
|
|
1906
2022
|
}
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
var CurrencyService = class _CurrencyService extends BaseService {
|
|
1910
|
-
static instance;
|
|
1911
|
-
static getInstance() {
|
|
1912
|
-
if (!_CurrencyService.instance) _CurrencyService.instance = new _CurrencyService();
|
|
1913
|
-
return _CurrencyService.instance;
|
|
1914
|
-
}
|
|
1915
|
-
/** One currency: GoCommerce prices a store in the currency its catalogue is recorded in. */
|
|
1916
|
-
async listCurrencies() {
|
|
1917
|
-
const store = await readStaticStore();
|
|
1918
|
-
return listOf(store?.currency ? [store.currency] : []);
|
|
2023
|
+
async getOtp() {
|
|
2024
|
+
throw new NotSupportedError("AuthService", "getOtp", "sign in with an email and password");
|
|
1919
2025
|
}
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
var RegionService = class _RegionService extends BaseService {
|
|
1923
|
-
static instance;
|
|
1924
|
-
static getInstance() {
|
|
1925
|
-
if (!_RegionService.instance) _RegionService.instance = new _RegionService();
|
|
1926
|
-
return _RegionService.instance;
|
|
2026
|
+
async verifyOtp() {
|
|
2027
|
+
throw new NotSupportedError("AuthService", "verifyOtp", "sign in with an email and password");
|
|
1927
2028
|
}
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
return {};
|
|
2029
|
+
async joinAsAdmin() {
|
|
2030
|
+
throw new NotSupportedError("AuthService", "joinAsAdmin", "operators are invited from the admin panel");
|
|
1931
2031
|
}
|
|
1932
2032
|
};
|
|
1933
|
-
var
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
static getInstance() {
|
|
1937
|
-
if (!_PluginService.instance) _PluginService.instance = new _PluginService();
|
|
1938
|
-
return _PluginService.instance;
|
|
2033
|
+
var ProfileService = class extends UserService {
|
|
2034
|
+
async getOne() {
|
|
2035
|
+
return this.getMe();
|
|
1939
2036
|
}
|
|
1940
|
-
async
|
|
1941
|
-
|
|
1942
|
-
return listOf(store?.plugins ?? []);
|
|
2037
|
+
async save(profile) {
|
|
2038
|
+
return this.updateProfile(profile);
|
|
1943
2039
|
}
|
|
1944
2040
|
};
|
|
1945
|
-
var
|
|
1946
|
-
var
|
|
1947
|
-
var
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
2041
|
+
var userService = new UserService();
|
|
2042
|
+
var authService = new AuthService();
|
|
2043
|
+
var profileService = new ProfileService();
|
|
2044
|
+
|
|
2045
|
+
// src/services/address-service.ts
|
|
2046
|
+
var AddressService = class extends BaseService {
|
|
2047
|
+
async list({
|
|
2048
|
+
page = 1,
|
|
2049
|
+
limit = 20
|
|
2050
|
+
} = {}) {
|
|
2051
|
+
const env = await this.engine("/x/identity/me/addresses", {
|
|
2052
|
+
query: { page, limit },
|
|
2053
|
+
auth: true
|
|
2054
|
+
});
|
|
2055
|
+
const email = currentMe()?.email;
|
|
2056
|
+
const rows = env.data ?? [];
|
|
2057
|
+
return {
|
|
2058
|
+
data: rows.map((a) => fromIdentityAddress(a, email)),
|
|
2059
|
+
count: env.meta?.total ?? rows.length,
|
|
2060
|
+
pageSize: env.meta?.limit ?? limit,
|
|
2061
|
+
noOfPage: env.meta?.total_pages ?? 1,
|
|
2062
|
+
page: env.meta?.page ?? page
|
|
2063
|
+
};
|
|
1956
2064
|
}
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
2065
|
+
async fetchAddress(id) {
|
|
2066
|
+
return fromIdentityAddress(
|
|
2067
|
+
await this.data(`/x/identity/me/addresses/${enc(id)}`, { auth: true }),
|
|
2068
|
+
currentMe()?.email
|
|
2069
|
+
);
|
|
1960
2070
|
}
|
|
1961
|
-
|
|
1962
|
-
|
|
2071
|
+
/** New when it has no id (or the form's `'new'`), an edit when it has one — the storefront saves both here. */
|
|
2072
|
+
async saveAddress(address) {
|
|
2073
|
+
const id = address?.id && address.id !== "new" ? String(address.id) : null;
|
|
2074
|
+
if (id) return this.editAddress(id, address);
|
|
2075
|
+
const a = await this.data("/x/identity/me/addresses", {
|
|
2076
|
+
method: "POST",
|
|
2077
|
+
auth: true,
|
|
2078
|
+
body: toIdentityAddress(address)
|
|
2079
|
+
});
|
|
2080
|
+
return fromIdentityAddress(a, currentMe()?.email);
|
|
1963
2081
|
}
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
return _InitService.instance;
|
|
2082
|
+
async editAddress(id, address) {
|
|
2083
|
+
const a = await this.data(`/x/identity/me/addresses/${enc(id)}`, {
|
|
2084
|
+
method: "PATCH",
|
|
2085
|
+
auth: true,
|
|
2086
|
+
body: toIdentityAddress(address)
|
|
2087
|
+
});
|
|
2088
|
+
return fromIdentityAddress(a, currentMe()?.email);
|
|
1972
2089
|
}
|
|
1973
|
-
async
|
|
1974
|
-
|
|
1975
|
-
return
|
|
2090
|
+
async deleteAddress(id) {
|
|
2091
|
+
await this.engine(`/x/identity/me/addresses/${enc(id)}`, { method: "DELETE", auth: true });
|
|
2092
|
+
return { id: String(id), deleted: true };
|
|
1976
2093
|
}
|
|
1977
2094
|
};
|
|
1978
|
-
var
|
|
1979
|
-
var
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
2095
|
+
var enc = (id) => encodeURIComponent(String(id));
|
|
2096
|
+
var addressService = new AddressService();
|
|
2097
|
+
|
|
2098
|
+
// src/services/wishlist-service.ts
|
|
2099
|
+
var WishlistService = class extends BaseService {
|
|
2100
|
+
async fetchWishlist(_args = {}) {
|
|
2101
|
+
const list = await this.current();
|
|
2102
|
+
if (!list) return { data: [], count: 0 };
|
|
2103
|
+
const items = (list.items ?? []).filter((i) => !i.deleted);
|
|
2104
|
+
const products = await Promise.all(
|
|
2105
|
+
items.map((i) => this.data(`/api/products/${i.product_id}`).catch(() => null))
|
|
2106
|
+
);
|
|
2107
|
+
const data = items.flatMap((item, n) => {
|
|
2108
|
+
const p = products[n];
|
|
2109
|
+
if (!p) return [];
|
|
2110
|
+
return [
|
|
2111
|
+
{
|
|
2112
|
+
id: String(item.id),
|
|
2113
|
+
productId: String(item.product_id),
|
|
2114
|
+
variantId: item.variant_id ? String(item.variant_id) : null,
|
|
2115
|
+
product: toProduct(p),
|
|
2116
|
+
variant: { id: item.variant_id ? String(item.variant_id) : null },
|
|
2117
|
+
createdAt: item.created_at
|
|
2118
|
+
}
|
|
2119
|
+
];
|
|
2120
|
+
});
|
|
2121
|
+
return { data, count: data.length };
|
|
2122
|
+
}
|
|
2123
|
+
/** Saved → removed, removed → saved. `{ active }` is the state after the click. */
|
|
2124
|
+
async toggleWishlist({ productId, variantId }) {
|
|
2125
|
+
const list = await this.current() ?? await this.create();
|
|
2126
|
+
const item = (list.items ?? []).find((i) => String(i.product_id) === String(productId));
|
|
2127
|
+
if (item) {
|
|
2128
|
+
await this.data(`/x/wishlist/${enc2(list.token)}/items/${item.id}`, { method: "DELETE" });
|
|
2129
|
+
return { active: false, id: String(item.id) };
|
|
2130
|
+
}
|
|
2131
|
+
const next = await this.data(`/x/wishlist/${enc2(list.token)}/items`, {
|
|
2132
|
+
method: "POST",
|
|
2133
|
+
body: { product_id: Number(productId), ...variantId ? { variant_id: Number(variantId) } : {} }
|
|
2134
|
+
});
|
|
2135
|
+
const added = (next.items ?? []).find((i) => String(i.product_id) === String(productId));
|
|
2136
|
+
return { active: true, id: added ? String(added.id) : null };
|
|
1988
2137
|
}
|
|
1989
|
-
}
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
static instance;
|
|
1993
|
-
static getInstance() {
|
|
1994
|
-
if (!_PopularSearchService.instance) _PopularSearchService.instance = new _PopularSearchService();
|
|
1995
|
-
return _PopularSearchService.instance;
|
|
2138
|
+
async checkWishlist({ productId }) {
|
|
2139
|
+
const list = await this.current();
|
|
2140
|
+
return !!list && (list.items ?? []).some((i) => String(i.product_id) === String(productId));
|
|
1996
2141
|
}
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2142
|
+
async checkWishlistInBulk(pairs) {
|
|
2143
|
+
const list = await this.current();
|
|
2144
|
+
const saved = new Set((list?.items ?? []).map((i) => String(i.product_id)));
|
|
2145
|
+
return (pairs ?? []).map((p) => ({ ...p, exists: saved.has(String(p.productId)) }));
|
|
2000
2146
|
}
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2147
|
+
async current() {
|
|
2148
|
+
const token = wishlistToken();
|
|
2149
|
+
if (!token) return null;
|
|
2150
|
+
try {
|
|
2151
|
+
return { ...await this.data(`/x/wishlist/${enc2(token)}`), token };
|
|
2152
|
+
} catch (err) {
|
|
2153
|
+
if (err instanceof GoCommerceError && err.status === 404 && !/no wishlist|no route/i.test(err.message)) {
|
|
2154
|
+
forgetWishlistToken();
|
|
2155
|
+
return null;
|
|
2156
|
+
}
|
|
2157
|
+
throw err;
|
|
2158
|
+
}
|
|
2008
2159
|
}
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2160
|
+
async create() {
|
|
2161
|
+
const email = currentMe()?.email;
|
|
2162
|
+
const list = await this.data("/x/wishlist", { method: "POST", ...email ? { body: { email } } : {} });
|
|
2163
|
+
if (!list.token) throw new GoCommerceError("The store did not return a wishlist token.", 500);
|
|
2164
|
+
rememberWishlistToken(list.token);
|
|
2165
|
+
return { ...list, token: list.token };
|
|
2012
2166
|
}
|
|
2013
2167
|
};
|
|
2014
|
-
var
|
|
2168
|
+
var enc2 = (s) => encodeURIComponent(s);
|
|
2169
|
+
var wishlistService = new WishlistService();
|
|
2015
2170
|
|
|
2016
|
-
// src/services/
|
|
2017
|
-
var
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2171
|
+
// src/services/review-service.ts
|
|
2172
|
+
var ReviewService = class extends BaseService {
|
|
2173
|
+
async fetchReviews({
|
|
2174
|
+
productId,
|
|
2175
|
+
currentPage = 1,
|
|
2176
|
+
page,
|
|
2177
|
+
limit = 20
|
|
2178
|
+
} = {}) {
|
|
2179
|
+
if (!productId) return { data: [], count: 0, pageSize: limit, noOfPage: 0, page: 1 };
|
|
2180
|
+
const p = Math.max(1, Number(page ?? currentPage) || 1);
|
|
2181
|
+
const res = await this.data("/x/reviews", { query: { product_id: productId, page: p, limit } });
|
|
2182
|
+
const total = res.meta?.total ?? res.summary?.count ?? 0;
|
|
2183
|
+
return {
|
|
2184
|
+
data: (res.reviews ?? []).map(toRating),
|
|
2185
|
+
count: total,
|
|
2186
|
+
pageSize: limit,
|
|
2187
|
+
noOfPage: Math.max(1, Math.ceil(total / limit)),
|
|
2188
|
+
page: p,
|
|
2189
|
+
summary: res.summary ?? null
|
|
2190
|
+
};
|
|
2034
2191
|
}
|
|
2035
|
-
async
|
|
2036
|
-
|
|
2192
|
+
async allReviews(args = {}) {
|
|
2193
|
+
return this.fetchReviews(args);
|
|
2037
2194
|
}
|
|
2038
|
-
|
|
2039
|
-
|
|
2195
|
+
};
|
|
2196
|
+
var reviewService = new ReviewService();
|
|
2197
|
+
|
|
2198
|
+
// src/services/menu-service.ts
|
|
2199
|
+
function linkFor(item) {
|
|
2200
|
+
const t = item.target ?? "";
|
|
2201
|
+
switch (item.kind) {
|
|
2202
|
+
case "home":
|
|
2203
|
+
return "/";
|
|
2204
|
+
case "product":
|
|
2205
|
+
return t ? `/products/${t}` : item.url;
|
|
2206
|
+
case "collection":
|
|
2207
|
+
return t ? `/collections/${t}` : item.url;
|
|
2208
|
+
case "category":
|
|
2209
|
+
return t ? `/${t}` : item.url;
|
|
2210
|
+
case "page":
|
|
2211
|
+
return t ? `/p/${t}` : item.url;
|
|
2212
|
+
default:
|
|
2213
|
+
return item.url;
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
var toItem = (item, i) => ({
|
|
2217
|
+
id: String(item.id ?? i),
|
|
2218
|
+
name: item.title,
|
|
2219
|
+
link: linkFor(item),
|
|
2220
|
+
items: (item.children ?? []).map(toItem)
|
|
2221
|
+
});
|
|
2222
|
+
var MenuService = class extends BaseService {
|
|
2223
|
+
facts = new StoreFacts(this._fetch);
|
|
2224
|
+
async list(_args = {}) {
|
|
2225
|
+
const menus = await this.engineMenus();
|
|
2226
|
+
if (menus) return { data: menus };
|
|
2227
|
+
return resolveRestLocally("get", "/api/menu");
|
|
2228
|
+
}
|
|
2229
|
+
/** The header and footer menus, or `null` when the module is not there. Cached: a menu is store-level. */
|
|
2230
|
+
async engineMenus() {
|
|
2231
|
+
if (!await this.facts.pluginOn("navigation")) return null;
|
|
2232
|
+
return remember(`${this.engineBase()}|${this.creds.store ?? ""}|menus`, async () => {
|
|
2233
|
+
const out = [];
|
|
2234
|
+
for (const handle of ["header", "footer"]) {
|
|
2235
|
+
try {
|
|
2236
|
+
const m = await this.data(`/x/navigation/menus/${handle}`);
|
|
2237
|
+
out.push({ menuId: handle, name: m.title, items: (m.items ?? []).map(toItem) });
|
|
2238
|
+
} catch {
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
return out;
|
|
2242
|
+
}).catch(() => null);
|
|
2040
2243
|
}
|
|
2041
|
-
|
|
2042
|
-
|
|
2244
|
+
};
|
|
2245
|
+
var menuService = new MenuService();
|
|
2246
|
+
|
|
2247
|
+
// src/services/store-service.ts
|
|
2248
|
+
function symbolFor(code) {
|
|
2249
|
+
try {
|
|
2250
|
+
const part = new Intl.NumberFormat("en", { style: "currency", currency: code }).formatToParts(0).find((p) => p.type === "currency");
|
|
2251
|
+
return part?.value ?? code;
|
|
2252
|
+
} catch {
|
|
2253
|
+
return code;
|
|
2043
2254
|
}
|
|
2044
|
-
|
|
2045
|
-
|
|
2255
|
+
}
|
|
2256
|
+
var StoreService = class extends BaseService {
|
|
2257
|
+
facts = new StoreFacts(this._fetch);
|
|
2258
|
+
async getStoreByIdOrDomain(_args = {}) {
|
|
2259
|
+
const base = await readStaticStore();
|
|
2260
|
+
const live = await this.live().catch(() => ({}));
|
|
2261
|
+
return { ...base, ...live, id: base.id || "gocommerce" };
|
|
2046
2262
|
}
|
|
2047
|
-
async
|
|
2048
|
-
|
|
2263
|
+
async getStore(storeId) {
|
|
2264
|
+
return this.getStoreByIdOrDomain({ storeId });
|
|
2049
2265
|
}
|
|
2050
|
-
|
|
2051
|
-
|
|
2266
|
+
/**
|
|
2267
|
+
* The storefront's sitemap route asks for `/api/stores/<id>` and serves the
|
|
2268
|
+
* `sitemap` path it gets back from `PUBLIC_SITEMAP_URL`. With ext/sitemaps
|
|
2269
|
+
* installed, `/sitemaps/sitemap.xml` under `PUBLIC_SITEMAP_URL=<engine>/x`
|
|
2270
|
+
* is the engine's own sitemap; without it, an empty path is the route's
|
|
2271
|
+
* clean 404 rather than a crash.
|
|
2272
|
+
*/
|
|
2273
|
+
async get(path) {
|
|
2274
|
+
if (path.split("?")[0].startsWith("/api/stores/")) {
|
|
2275
|
+
const on = await this.facts.pluginOn("sitemap");
|
|
2276
|
+
return { sitemap: on ? "/sitemaps/sitemap.xml" : "" };
|
|
2277
|
+
}
|
|
2278
|
+
return super.get(path);
|
|
2052
2279
|
}
|
|
2053
|
-
async
|
|
2054
|
-
|
|
2280
|
+
async live() {
|
|
2281
|
+
const [ready, plugins, menus, base] = await Promise.all([
|
|
2282
|
+
this.facts.ready(),
|
|
2283
|
+
this.facts.plugins().catch(() => /* @__PURE__ */ new Map()),
|
|
2284
|
+
new MenuService(this._fetch).engineMenus(),
|
|
2285
|
+
readStaticStore()
|
|
2286
|
+
]);
|
|
2287
|
+
const code = ready.currency;
|
|
2288
|
+
const sameCurrency = (base.currency?.code ?? base.currencyCode) === code;
|
|
2289
|
+
const flag = (on) => ({ ...on ? { active: true } : { active: false } });
|
|
2290
|
+
const basePlugins = base.plugins ?? {};
|
|
2291
|
+
return {
|
|
2292
|
+
currencyCode: code,
|
|
2293
|
+
currencySymbol: sameCurrency ? base.currencySymbol ?? symbolFor(code) : symbolFor(code),
|
|
2294
|
+
currency: {
|
|
2295
|
+
...base.currency ?? {},
|
|
2296
|
+
code,
|
|
2297
|
+
symbol: sameCurrency ? base.currency?.symbol ?? symbolFor(code) : symbolFor(code)
|
|
2298
|
+
},
|
|
2299
|
+
language: ready.language,
|
|
2300
|
+
plugins: {
|
|
2301
|
+
...basePlugins,
|
|
2302
|
+
// Features this connector serves from a module: on exactly when the
|
|
2303
|
+
// module is installed and switched on, whatever the static record says.
|
|
2304
|
+
isWishlist: { ...basePlugins.isWishlist, ...flag(plugins.has("wishlist")) },
|
|
2305
|
+
isProductReviewsAndRatings: {
|
|
2306
|
+
...basePlugins.isProductReviewsAndRatings,
|
|
2307
|
+
...flag(plugins.has("product-reviews"))
|
|
2308
|
+
}
|
|
2309
|
+
},
|
|
2310
|
+
...menus?.length ? { menu: menus } : {}
|
|
2311
|
+
};
|
|
2055
2312
|
}
|
|
2056
|
-
|
|
2057
|
-
|
|
2313
|
+
};
|
|
2314
|
+
var storeService = new StoreService();
|
|
2315
|
+
|
|
2316
|
+
// src/services/page-service.ts
|
|
2317
|
+
var PageService = class extends BaseService {
|
|
2318
|
+
async getOne(slug) {
|
|
2319
|
+
return toCmsPage(await this.data(`/x/cms/pages/${encodeURIComponent(String(slug))}`));
|
|
2320
|
+
}
|
|
2321
|
+
async list({ page = 1, limit = 50 } = {}) {
|
|
2322
|
+
const env = await this.engine("/x/cms/pages", { query: { page, limit } });
|
|
2323
|
+
return toPage(env, toCmsPage);
|
|
2324
|
+
}
|
|
2325
|
+
};
|
|
2326
|
+
var FaqService = class extends BaseService {
|
|
2327
|
+
async listFaqs(_args = {}) {
|
|
2328
|
+
const faq = await this.data("/x/faq");
|
|
2329
|
+
const data = (faq.sections ?? []).flatMap(
|
|
2330
|
+
(s) => (s.entries ?? []).map((e) => ({
|
|
2331
|
+
id: String(e.id),
|
|
2332
|
+
question: e.question,
|
|
2333
|
+
answer: e.answer,
|
|
2334
|
+
category: s.section || null
|
|
2335
|
+
}))
|
|
2336
|
+
);
|
|
2337
|
+
return { data, count: data.length, heading: faq.heading, blurb: faq.blurb };
|
|
2058
2338
|
}
|
|
2059
|
-
async
|
|
2060
|
-
return
|
|
2339
|
+
async list(args = {}) {
|
|
2340
|
+
return this.listFaqs(args);
|
|
2061
2341
|
}
|
|
2062
|
-
|
|
2063
|
-
|
|
2342
|
+
};
|
|
2343
|
+
var pageService = new PageService();
|
|
2344
|
+
var faqService = new FaqService();
|
|
2345
|
+
|
|
2346
|
+
// src/services/vendor-service.ts
|
|
2347
|
+
var VendorService = class extends BaseService {
|
|
2348
|
+
async list({
|
|
2349
|
+
page = 1,
|
|
2350
|
+
limit = 24,
|
|
2351
|
+
q,
|
|
2352
|
+
search
|
|
2353
|
+
} = {}) {
|
|
2354
|
+
const env = await this.engine("/api/vendors", { query: { page, limit, q: q ?? search } });
|
|
2355
|
+
return toPage(env, toVendor);
|
|
2356
|
+
}
|
|
2357
|
+
async getVendor(slug) {
|
|
2358
|
+
return toVendor(await this.data(`/api/vendors/${encodeURIComponent(String(slug))}`));
|
|
2064
2359
|
}
|
|
2065
2360
|
/**
|
|
2066
|
-
*
|
|
2067
|
-
*
|
|
2068
|
-
*
|
|
2069
|
-
* is a trap. The shared user store decides who is signed in by reading the `connect.sid` and `me`
|
|
2070
|
-
* cookies straight from `document.cookie`, not by asking a service. A pair left behind by running
|
|
2071
|
-
* this storefront against a backend that does have accounts therefore makes GoCommerce look
|
|
2072
|
-
* signed-in, which sends the checkout down the saved-address path instead of the guest one. The
|
|
2073
|
-
* checkout page already knows to call `logout()` and retry when that happens — so this has to
|
|
2074
|
-
* actually clear them, or that recovery does nothing.
|
|
2361
|
+
* A seller's shelf. The engine's vendors sell through offers on the shop's
|
|
2362
|
+
* own variants and the public API lists no products by seller, so the shelf
|
|
2363
|
+
* is empty — the seller's page still renders, with their details.
|
|
2075
2364
|
*/
|
|
2076
|
-
async
|
|
2077
|
-
|
|
2078
|
-
return { success: true };
|
|
2365
|
+
async fetchProductsOfVendor(_vendorId) {
|
|
2366
|
+
return emptyPage();
|
|
2079
2367
|
}
|
|
2080
2368
|
};
|
|
2081
|
-
var
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2369
|
+
var vendorService = new VendorService();
|
|
2370
|
+
|
|
2371
|
+
// src/services/misc-services.ts
|
|
2372
|
+
var refuse = (service, method, why) => new NotSupportedError(service, method, why);
|
|
2373
|
+
var ContactService = class extends BaseService {
|
|
2374
|
+
async submitContactUsForm({
|
|
2375
|
+
name,
|
|
2376
|
+
email,
|
|
2377
|
+
phone,
|
|
2378
|
+
message,
|
|
2379
|
+
subject
|
|
2380
|
+
}) {
|
|
2381
|
+
return this.data("/x/contact/messages", {
|
|
2382
|
+
method: "POST",
|
|
2383
|
+
body: {
|
|
2384
|
+
name: String(name ?? ""),
|
|
2385
|
+
email: String(email ?? ""),
|
|
2386
|
+
...phone ? { phone: String(phone) } : {},
|
|
2387
|
+
...subject ? { subject: String(subject) } : {},
|
|
2388
|
+
body: String(message ?? "")
|
|
2389
|
+
}
|
|
2390
|
+
});
|
|
2089
2391
|
}
|
|
2090
2392
|
};
|
|
2091
|
-
var
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2393
|
+
var EnquiryService = class extends ContactService {
|
|
2394
|
+
async create({
|
|
2395
|
+
name,
|
|
2396
|
+
email,
|
|
2397
|
+
phone,
|
|
2398
|
+
message,
|
|
2399
|
+
productId
|
|
2400
|
+
}) {
|
|
2401
|
+
return this.submitContactUsForm({
|
|
2402
|
+
name,
|
|
2403
|
+
email,
|
|
2404
|
+
phone,
|
|
2405
|
+
subject: productId ? `Enquiry about product ${productId}` : "Enquiry",
|
|
2406
|
+
message
|
|
2407
|
+
});
|
|
2099
2408
|
}
|
|
2100
2409
|
};
|
|
2101
|
-
var
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
const page = Math.max(1, Number(params.page || 1) || 1);
|
|
2107
|
-
const start = (page - 1) * PAGE_SIZE;
|
|
2108
|
-
return paginated(addresses.slice(start, start + PAGE_SIZE), addresses.length, page);
|
|
2109
|
-
}
|
|
2110
|
-
async fetchAddress(id) {
|
|
2111
|
-
const address = findAddress(id);
|
|
2112
|
-
if (!address) throw new Error("That address is no longer saved on this device.");
|
|
2113
|
-
return address;
|
|
2114
|
-
}
|
|
2115
|
-
async saveAddress(address) {
|
|
2116
|
-
return putAddress(address);
|
|
2117
|
-
}
|
|
2118
|
-
async editAddress(id, address) {
|
|
2119
|
-
const saved = patchAddress(id, address);
|
|
2120
|
-
if (!saved) throw new Error("That address is no longer saved on this device.");
|
|
2121
|
-
return saved;
|
|
2122
|
-
}
|
|
2123
|
-
async deleteAddress(id) {
|
|
2124
|
-
removeAddress(id);
|
|
2410
|
+
var PluginService = class extends BaseService {
|
|
2411
|
+
facts = new StoreFacts(this._fetch);
|
|
2412
|
+
async list(_args = {}) {
|
|
2413
|
+
const data = [...(await this.facts.plugins()).values()];
|
|
2414
|
+
return { data, count: data.length, pageSize: data.length, noOfPage: 1, page: 1 };
|
|
2125
2415
|
}
|
|
2126
2416
|
};
|
|
2127
|
-
var
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
async fetchWishlist(_params = {}) {
|
|
2131
|
-
return emptyPage();
|
|
2132
|
-
}
|
|
2133
|
-
async checkWishlist(_params = {}) {
|
|
2134
|
-
return { data: false };
|
|
2135
|
-
}
|
|
2136
|
-
/** The product grid asks about many ids at once; nothing is on a list that cannot exist. */
|
|
2137
|
-
async checkWishlistInBulk(_ids) {
|
|
2138
|
-
return { data: [] };
|
|
2417
|
+
var SettingService = class extends BaseService {
|
|
2418
|
+
async fetchSettings(_args = {}) {
|
|
2419
|
+
return new StoreService(this._fetch).getStoreByIdOrDomain();
|
|
2139
2420
|
}
|
|
2140
|
-
async
|
|
2141
|
-
|
|
2421
|
+
async list(args = {}) {
|
|
2422
|
+
return this.fetchSettings(args);
|
|
2142
2423
|
}
|
|
2143
2424
|
};
|
|
2144
|
-
var
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
async listCoupons(_params = {}) {
|
|
2149
|
-
return emptyPage();
|
|
2150
|
-
}
|
|
2151
|
-
async searchCoupons(_params = {}) {
|
|
2152
|
-
return emptyPage();
|
|
2425
|
+
var InitService = class extends SettingService {
|
|
2426
|
+
async fetchInit(_args = {}) {
|
|
2427
|
+
const store = await this.fetchSettings();
|
|
2428
|
+
return { storeOne: store, store, settings: store };
|
|
2153
2429
|
}
|
|
2154
|
-
|
|
2155
|
-
|
|
2430
|
+
};
|
|
2431
|
+
var HomeService = class extends BaseService {
|
|
2432
|
+
/** A homepage is assembled by the storefront from shelves; the engine has no page of its own for it. */
|
|
2433
|
+
async getHome(_args = {}) {
|
|
2434
|
+
return { banners: [], groupByBanner: [], categories: [], products: [] };
|
|
2156
2435
|
}
|
|
2157
|
-
|
|
2158
|
-
|
|
2436
|
+
};
|
|
2437
|
+
var CountryService = class extends BaseService {
|
|
2438
|
+
async list(_args = {}) {
|
|
2439
|
+
return resolveRestLocally("get", "/api/countries");
|
|
2159
2440
|
}
|
|
2160
|
-
|
|
2161
|
-
|
|
2441
|
+
};
|
|
2442
|
+
var CurrencyService = class extends BaseService {
|
|
2443
|
+
facts = new StoreFacts(this._fetch);
|
|
2444
|
+
async listCurrencies(_args = {}) {
|
|
2445
|
+
const { currency } = await this.facts.ready();
|
|
2446
|
+
const data = [{ code: currency, name: currency, isDefault: true }];
|
|
2447
|
+
return { data, count: 1, pageSize: 1, noOfPage: 1, page: 1 };
|
|
2162
2448
|
}
|
|
2163
|
-
async
|
|
2164
|
-
|
|
2449
|
+
async list(args = {}) {
|
|
2450
|
+
return this.listCurrencies(args);
|
|
2165
2451
|
}
|
|
2166
2452
|
};
|
|
2167
|
-
var
|
|
2168
|
-
|
|
2169
|
-
static getInstance = singleton(_DealService);
|
|
2170
|
-
async fetchDeals() {
|
|
2453
|
+
var StateService = class extends BaseService {
|
|
2454
|
+
async list(_args = {}) {
|
|
2171
2455
|
return emptyPage();
|
|
2172
2456
|
}
|
|
2173
2457
|
};
|
|
2174
|
-
var
|
|
2175
|
-
|
|
2176
|
-
static getInstance = singleton(_ReviewService);
|
|
2177
|
-
async fetchReviews(_params = {}) {
|
|
2178
|
-
return emptyPage();
|
|
2179
|
-
}
|
|
2180
|
-
async allReviews(_params = {}) {
|
|
2181
|
-
return emptyPage();
|
|
2182
|
-
}
|
|
2183
|
-
async fetchProducrReviews(_productId) {
|
|
2458
|
+
var RegionService = class extends BaseService {
|
|
2459
|
+
async list(_args = {}) {
|
|
2184
2460
|
return emptyPage();
|
|
2185
2461
|
}
|
|
2186
|
-
async saveReview(_review) {
|
|
2187
|
-
throw unavailable("Product reviews");
|
|
2188
|
-
}
|
|
2189
2462
|
};
|
|
2190
|
-
var
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
async list(_params = {}) {
|
|
2194
|
-
return emptyPage();
|
|
2463
|
+
var AutocompleteService = class extends MeilisearchService {
|
|
2464
|
+
async list({ q, search } = {}) {
|
|
2465
|
+
return this.searchAutoComplete({ query: q ?? search });
|
|
2195
2466
|
}
|
|
2196
|
-
|
|
2467
|
+
};
|
|
2468
|
+
var PopularSearchService = class extends BaseService {
|
|
2469
|
+
async list(_args = {}) {
|
|
2197
2470
|
return emptyPage();
|
|
2198
2471
|
}
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
* request for something that was never required.
|
|
2204
|
-
*/
|
|
2205
|
-
async getOne(_id) {
|
|
2206
|
-
return {};
|
|
2472
|
+
};
|
|
2473
|
+
var PopularityService = class extends BaseService {
|
|
2474
|
+
async updatePopularity(_args = {}) {
|
|
2475
|
+
return void 0;
|
|
2207
2476
|
}
|
|
2208
2477
|
};
|
|
2209
|
-
var
|
|
2210
|
-
|
|
2211
|
-
static getInstance = singleton(_BlogService);
|
|
2212
|
-
async list(_params = {}) {
|
|
2478
|
+
var BlogService = class extends BaseService {
|
|
2479
|
+
async list(_args = {}) {
|
|
2213
2480
|
return emptyPage();
|
|
2214
2481
|
}
|
|
2215
|
-
async getOne(
|
|
2216
|
-
throw
|
|
2482
|
+
async getOne(_slug) {
|
|
2483
|
+
throw refuse("BlogService", "getOne", "the engine has no blog; publish articles as pages");
|
|
2217
2484
|
}
|
|
2218
2485
|
};
|
|
2219
|
-
var
|
|
2220
|
-
|
|
2221
|
-
static getInstance = singleton(_BannerService);
|
|
2222
|
-
async list() {
|
|
2486
|
+
var BannerService = class extends BaseService {
|
|
2487
|
+
async list(_args = {}) {
|
|
2223
2488
|
return emptyPage();
|
|
2224
2489
|
}
|
|
2225
|
-
async
|
|
2226
|
-
return
|
|
2490
|
+
async listBanners(args = {}) {
|
|
2491
|
+
return this.list(args);
|
|
2227
2492
|
}
|
|
2228
|
-
}
|
|
2229
|
-
|
|
2230
|
-
var GalleryService = class _GalleryService extends BaseService {
|
|
2231
|
-
static getInstance = singleton(_GalleryService);
|
|
2232
|
-
async fetchGallery() {
|
|
2233
|
-
return [];
|
|
2493
|
+
async fetchBanners(args = {}) {
|
|
2494
|
+
return this.list(args);
|
|
2234
2495
|
}
|
|
2235
2496
|
};
|
|
2236
|
-
var
|
|
2237
|
-
|
|
2238
|
-
static getInstance = singleton(_ReelsService);
|
|
2239
|
-
async list() {
|
|
2497
|
+
var ReelsService = class extends BaseService {
|
|
2498
|
+
async list(_args = {}) {
|
|
2240
2499
|
return emptyPage();
|
|
2241
2500
|
}
|
|
2242
2501
|
};
|
|
2243
|
-
var
|
|
2244
|
-
|
|
2245
|
-
static getInstance = singleton(_FaqService);
|
|
2246
|
-
async listFaqs(_params = {}) {
|
|
2502
|
+
var DealService = class extends BaseService {
|
|
2503
|
+
async fetchDeals(_args = {}) {
|
|
2247
2504
|
return emptyPage();
|
|
2248
2505
|
}
|
|
2249
2506
|
};
|
|
2250
|
-
var
|
|
2251
|
-
|
|
2252
|
-
static getInstance = singleton(_FeedbackService);
|
|
2253
|
-
async listFeedbacks(_params = {}) {
|
|
2507
|
+
var GalleryService = class extends BaseService {
|
|
2508
|
+
async list(_args = {}) {
|
|
2254
2509
|
return emptyPage();
|
|
2255
2510
|
}
|
|
2256
2511
|
};
|
|
2257
|
-
var
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
async submitContactUsForm(_params) {
|
|
2261
|
-
throw unavailable("The contact form");
|
|
2262
|
-
}
|
|
2263
|
-
};
|
|
2264
|
-
var contactService = ContactService.getInstance();
|
|
2265
|
-
var EnquiryService = class _EnquiryService extends BaseService {
|
|
2266
|
-
static getInstance = singleton(_EnquiryService);
|
|
2267
|
-
async create(_params) {
|
|
2268
|
-
throw unavailable("Product enquiries");
|
|
2269
|
-
}
|
|
2270
|
-
};
|
|
2271
|
-
var enquiryService = EnquiryService.getInstance();
|
|
2272
|
-
var ChatService = class _ChatService extends BaseService {
|
|
2273
|
-
static getInstance = singleton(_ChatService);
|
|
2274
|
-
async list() {
|
|
2275
|
-
return [];
|
|
2512
|
+
var ChatService = class extends BaseService {
|
|
2513
|
+
async list(_args = {}) {
|
|
2514
|
+
return emptyPage();
|
|
2276
2515
|
}
|
|
2277
2516
|
async chats() {
|
|
2278
|
-
|
|
2517
|
+
throw refuse("ChatService", "chats", "the engine has no shopper chat");
|
|
2279
2518
|
}
|
|
2280
|
-
async save(
|
|
2281
|
-
throw
|
|
2519
|
+
async save() {
|
|
2520
|
+
throw refuse("ChatService", "save", "the engine has no shopper chat");
|
|
2282
2521
|
}
|
|
2283
2522
|
};
|
|
2284
|
-
var
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
// Media in GoCommerce is an operator's, uploaded through the admin API. A storefront holds no
|
|
2288
|
-
// admin credential and must not act as though it does.
|
|
2289
|
-
async uploadToS3(_params) {
|
|
2290
|
-
throw unavailable("File uploads");
|
|
2291
|
-
}
|
|
2292
|
-
async uploadMultipleToS3(_params) {
|
|
2293
|
-
throw unavailable("File uploads");
|
|
2294
|
-
}
|
|
2295
|
-
async deleteFromS3(_params) {
|
|
2296
|
-
throw unavailable("File uploads");
|
|
2523
|
+
var FeedbackService = class extends BaseService {
|
|
2524
|
+
async save() {
|
|
2525
|
+
throw refuse("FeedbackService", "save", "use the contact form");
|
|
2297
2526
|
}
|
|
2298
2527
|
};
|
|
2299
|
-
var
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
async list(_params = {}) {
|
|
2303
|
-
return emptyPage();
|
|
2304
|
-
}
|
|
2305
|
-
async getVendor(_id) {
|
|
2306
|
-
throw unavailable("Vendor pages");
|
|
2307
|
-
}
|
|
2308
|
-
async fetchMyVendorDetails() {
|
|
2309
|
-
throw unavailable("Vendor accounts");
|
|
2310
|
-
}
|
|
2311
|
-
async fetchDashboard() {
|
|
2312
|
-
throw unavailable("Vendor accounts");
|
|
2313
|
-
}
|
|
2314
|
-
async fetchProductsOfVendor(_vendorId) {
|
|
2315
|
-
return [];
|
|
2316
|
-
}
|
|
2317
|
-
async getAllVendorRatings(_vendorId) {
|
|
2318
|
-
return [];
|
|
2528
|
+
var DemoRequestService = class extends BaseService {
|
|
2529
|
+
async save() {
|
|
2530
|
+
throw refuse("DemoRequestService", "save", "use the contact form");
|
|
2319
2531
|
}
|
|
2320
|
-
|
|
2321
|
-
|
|
2532
|
+
};
|
|
2533
|
+
var UploadService = class extends BaseService {
|
|
2534
|
+
async uploadMultipleToS3() {
|
|
2535
|
+
throw refuse("UploadService", "uploadMultipleToS3", "shoppers cannot upload files to the engine");
|
|
2322
2536
|
}
|
|
2323
|
-
async
|
|
2324
|
-
throw
|
|
2537
|
+
async upload() {
|
|
2538
|
+
throw refuse("UploadService", "upload", "shoppers cannot upload files to the engine");
|
|
2325
2539
|
}
|
|
2326
2540
|
};
|
|
2327
|
-
var
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
async submitForm(_formDetails) {
|
|
2331
|
-
throw unavailable("Custom design requests");
|
|
2541
|
+
var WarrantyService = class extends BaseService {
|
|
2542
|
+
async submitWarrantyRegistration() {
|
|
2543
|
+
throw refuse("WarrantyService", "submitWarrantyRegistration", "the engine has no warranty registrations");
|
|
2332
2544
|
}
|
|
2333
2545
|
};
|
|
2334
|
-
var
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
async checkForCombinationWith(_args) {
|
|
2338
|
-
return { data: [], count: 0, pageSize: PAGE_SIZE, noOfPage: 0, page: 1 };
|
|
2339
|
-
}
|
|
2340
|
-
async createCustomOrderChat(_productId) {
|
|
2341
|
-
throw unavailable("Custom product chat");
|
|
2546
|
+
var VarniCustomDesignService = class extends BaseService {
|
|
2547
|
+
async submitForm() {
|
|
2548
|
+
throw refuse("VarniCustomDesignService", "submitForm", "custom design requests are a Litekart feature");
|
|
2342
2549
|
}
|
|
2343
|
-
|
|
2344
|
-
|
|
2550
|
+
};
|
|
2551
|
+
var VarniCustomProductService = class extends BaseService {
|
|
2552
|
+
async list(_args = {}) {
|
|
2553
|
+
return emptyPage();
|
|
2345
2554
|
}
|
|
2346
2555
|
};
|
|
2347
|
-
var
|
|
2556
|
+
var staticStore = () => readStaticStore();
|
|
2557
|
+
var contactService = new ContactService();
|
|
2558
|
+
var enquiryService = new EnquiryService();
|
|
2559
|
+
var pluginService = new PluginService();
|
|
2560
|
+
var settingService = new SettingService();
|
|
2561
|
+
var initService = new InitService();
|
|
2562
|
+
var homeService = new HomeService();
|
|
2563
|
+
var countryService = new CountryService();
|
|
2564
|
+
var currencyService = new CurrencyService();
|
|
2565
|
+
var stateService = new StateService();
|
|
2566
|
+
var regionService = new RegionService();
|
|
2567
|
+
var autocompleteService = new AutocompleteService();
|
|
2568
|
+
var popularSearchService = new PopularSearchService();
|
|
2569
|
+
var popularityService = new PopularityService();
|
|
2570
|
+
var blogService = new BlogService();
|
|
2571
|
+
var bannerService = new BannerService();
|
|
2572
|
+
var reelsService = new ReelsService();
|
|
2573
|
+
var dealService = new DealService();
|
|
2574
|
+
var galleryService = new GalleryService();
|
|
2575
|
+
var chatService = new ChatService();
|
|
2576
|
+
var feedbackService = new FeedbackService();
|
|
2577
|
+
var demoRequestService = new DemoRequestService();
|
|
2578
|
+
var uploadService = new UploadService();
|
|
2579
|
+
var warrantyService = new WarrantyService();
|
|
2580
|
+
var varniCustomDesignService = new VarniCustomDesignService();
|
|
2581
|
+
var varniCustomProductService = new VarniCustomProductService();
|
|
2582
|
+
var pluginsService = pluginService;
|
|
2583
|
+
var settingsService = settingService;
|
|
2348
2584
|
|
|
2349
|
-
exports.API_PROXY_PREFIX = API_PROXY_PREFIX;
|
|
2350
2585
|
exports.AddressService = AddressService;
|
|
2351
2586
|
exports.AuthService = AuthService;
|
|
2352
2587
|
exports.AutocompleteService = AutocompleteService;
|
|
@@ -2354,14 +2589,17 @@ exports.BannerService = BannerService;
|
|
|
2354
2589
|
exports.BaseService = BaseService;
|
|
2355
2590
|
exports.BlogService = BlogService;
|
|
2356
2591
|
exports.CartService = CartService;
|
|
2592
|
+
exports.CatalogService = CatalogService;
|
|
2357
2593
|
exports.CategoryService = CategoryService;
|
|
2358
2594
|
exports.ChatService = ChatService;
|
|
2359
2595
|
exports.CheckoutService = CheckoutService;
|
|
2360
2596
|
exports.CollectionService = CollectionService;
|
|
2361
2597
|
exports.ContactService = ContactService;
|
|
2598
|
+
exports.CountryService = CountryService;
|
|
2362
2599
|
exports.CouponService = CouponService;
|
|
2363
2600
|
exports.CurrencyService = CurrencyService;
|
|
2364
2601
|
exports.DealService = DealService;
|
|
2602
|
+
exports.DemoRequestService = DemoRequestService;
|
|
2365
2603
|
exports.EnquiryService = EnquiryService;
|
|
2366
2604
|
exports.FaqService = FaqService;
|
|
2367
2605
|
exports.FeedbackService = FeedbackService;
|
|
@@ -2369,11 +2607,10 @@ exports.GalleryService = GalleryService;
|
|
|
2369
2607
|
exports.GoCommerceError = GoCommerceError;
|
|
2370
2608
|
exports.HomeService = HomeService;
|
|
2371
2609
|
exports.InitService = InitService;
|
|
2372
|
-
exports.MAX_LIMIT = MAX_LIMIT;
|
|
2373
2610
|
exports.MeilisearchService = MeilisearchService;
|
|
2374
2611
|
exports.MenuService = MenuService;
|
|
2612
|
+
exports.NotSupportedError = NotSupportedError;
|
|
2375
2613
|
exports.OrderService = OrderService;
|
|
2376
|
-
exports.PAGE_SIZE = PAGE_SIZE;
|
|
2377
2614
|
exports.PageService = PageService;
|
|
2378
2615
|
exports.PaymentMethodService = PaymentMethodService;
|
|
2379
2616
|
exports.PluginService = PluginService;
|
|
@@ -2395,8 +2632,8 @@ exports.UserService = UserService;
|
|
|
2395
2632
|
exports.VarniCustomDesignService = VarniCustomDesignService;
|
|
2396
2633
|
exports.VarniCustomProductService = VarniCustomProductService;
|
|
2397
2634
|
exports.VendorService = VendorService;
|
|
2635
|
+
exports.WarrantyService = WarrantyService;
|
|
2398
2636
|
exports.WishlistService = WishlistService;
|
|
2399
|
-
exports.absoluteMedia = absoluteMedia;
|
|
2400
2637
|
exports.addressService = addressService;
|
|
2401
2638
|
exports.authService = authService;
|
|
2402
2639
|
exports.autocompleteService = autocompleteService;
|
|
@@ -2406,72 +2643,77 @@ exports.cartService = cartService;
|
|
|
2406
2643
|
exports.categoryService = categoryService;
|
|
2407
2644
|
exports.chatService = chatService;
|
|
2408
2645
|
exports.checkoutService = checkoutService;
|
|
2409
|
-
exports.
|
|
2410
|
-
exports.clearPhantomSession = clearPhantomSession;
|
|
2646
|
+
exports.clearStoreCache = forgetAll;
|
|
2411
2647
|
exports.collectionService = collectionService;
|
|
2648
|
+
exports.connectorName = connectorName;
|
|
2412
2649
|
exports.contactService = contactService;
|
|
2650
|
+
exports.countryService = countryService;
|
|
2413
2651
|
exports.couponService = couponService;
|
|
2414
|
-
exports.
|
|
2652
|
+
exports.currencyExponent = currencyExponent;
|
|
2415
2653
|
exports.currencyService = currencyService;
|
|
2416
2654
|
exports.dealService = dealService;
|
|
2655
|
+
exports.demoRequestService = demoRequestService;
|
|
2656
|
+
exports.emptyCart = emptyCart;
|
|
2417
2657
|
exports.emptyPage = emptyPage;
|
|
2418
2658
|
exports.enquiryService = enquiryService;
|
|
2419
2659
|
exports.faqService = faqService;
|
|
2420
2660
|
exports.feedbackService = feedbackService;
|
|
2421
|
-
exports.
|
|
2422
|
-
exports.
|
|
2423
|
-
exports.
|
|
2661
|
+
exports.fromEngineAddress = fromEngineAddress;
|
|
2662
|
+
exports.fromIdentityAddress = fromIdentityAddress;
|
|
2663
|
+
exports.fromSearchDoc = fromSearchDoc;
|
|
2424
2664
|
exports.galleryService = galleryService;
|
|
2425
|
-
exports.hasPhantomSession = hasPhantomSession;
|
|
2426
2665
|
exports.hasStaticStore = hasStaticStore;
|
|
2427
2666
|
exports.homeService = homeService;
|
|
2428
2667
|
exports.initService = initService;
|
|
2429
|
-
exports.listAddresses = listAddresses;
|
|
2430
|
-
exports.mapCart = mapCart;
|
|
2431
|
-
exports.mapCategory = mapCategory;
|
|
2432
|
-
exports.mapCollection = mapCollection;
|
|
2433
|
-
exports.mapOrder = mapOrder;
|
|
2434
|
-
exports.mapProduct = mapProduct;
|
|
2435
2668
|
exports.meilisearchService = meilisearchService;
|
|
2436
2669
|
exports.menuService = menuService;
|
|
2670
|
+
exports.minorToMajor = minorToMajor;
|
|
2437
2671
|
exports.orderService = orderService;
|
|
2438
2672
|
exports.pageService = pageService;
|
|
2439
|
-
exports.paginated = paginated;
|
|
2440
|
-
exports.patchAddress = patchAddress;
|
|
2441
2673
|
exports.paymentMethodService = paymentMethodService;
|
|
2442
2674
|
exports.pluginService = pluginService;
|
|
2443
2675
|
exports.pluginsService = pluginsService;
|
|
2444
2676
|
exports.popularSearchService = popularSearchService;
|
|
2445
2677
|
exports.popularityService = popularityService;
|
|
2678
|
+
exports.priceOf = priceOf;
|
|
2446
2679
|
exports.productImages = productImages;
|
|
2447
2680
|
exports.productService = productService;
|
|
2448
2681
|
exports.profileService = profileService;
|
|
2449
|
-
exports.
|
|
2450
|
-
exports.readCheckoutDetails = readCheckoutDetails;
|
|
2451
|
-
exports.readOrderNumbers = readOrderNumbers;
|
|
2452
|
-
exports.readOrderToken = readOrderToken;
|
|
2682
|
+
exports.qs = qs;
|
|
2453
2683
|
exports.readStaticStore = readStaticStore;
|
|
2454
|
-
exports.readStaticStoreDetails = readStaticStoreDetails;
|
|
2455
2684
|
exports.reelsService = reelsService;
|
|
2456
2685
|
exports.regionService = regionService;
|
|
2457
|
-
exports.rememberOrderToken = rememberOrderToken;
|
|
2458
|
-
exports.removeAddress = removeAddress;
|
|
2459
|
-
exports.resolveProductIndex = resolveProductIndex;
|
|
2460
2686
|
exports.reviewService = reviewService;
|
|
2461
2687
|
exports.searchService = searchService;
|
|
2688
|
+
exports.serveRestLocally = serveRestLocally;
|
|
2462
2689
|
exports.setStaticStore = setStaticStore;
|
|
2463
2690
|
exports.settingService = settingService;
|
|
2464
2691
|
exports.settingsService = settingsService;
|
|
2465
2692
|
exports.stateService = stateService;
|
|
2693
|
+
exports.staticStore = staticStore;
|
|
2466
2694
|
exports.storeService = storeService;
|
|
2467
|
-
exports.
|
|
2468
|
-
exports.
|
|
2695
|
+
exports.toCart = toCart;
|
|
2696
|
+
exports.toCartLine = toCartLine;
|
|
2697
|
+
exports.toCategory = toCategory;
|
|
2698
|
+
exports.toCmsPage = toCmsPage;
|
|
2699
|
+
exports.toCollection = toCollection;
|
|
2700
|
+
exports.toEngineAddress = toEngineAddress;
|
|
2701
|
+
exports.toIdentityAddress = toIdentityAddress;
|
|
2702
|
+
exports.toMajor = toMajor;
|
|
2703
|
+
exports.toMinor = toMinor;
|
|
2704
|
+
exports.toOrder = toOrder;
|
|
2705
|
+
exports.toPage = toPage;
|
|
2706
|
+
exports.toProduct = toProduct;
|
|
2707
|
+
exports.toRating = toRating;
|
|
2708
|
+
exports.toUser = toUser;
|
|
2709
|
+
exports.toVariant = toVariant;
|
|
2710
|
+
exports.toVendor = toVendor;
|
|
2469
2711
|
exports.uploadService = uploadService;
|
|
2470
2712
|
exports.userService = userService;
|
|
2471
2713
|
exports.varniCustomDesignService = varniCustomDesignService;
|
|
2472
2714
|
exports.varniCustomProductService = varniCustomProductService;
|
|
2473
2715
|
exports.vendorService = vendorService;
|
|
2716
|
+
exports.warrantyService = warrantyService;
|
|
2474
2717
|
exports.wishlistService = wishlistService;
|
|
2475
|
-
exports.writeCheckoutDetails = writeCheckoutDetails;
|
|
2476
2718
|
//# sourceMappingURL=index.cjs.map
|
|
2477
2719
|
//# sourceMappingURL=index.cjs.map
|