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