@misiki/gocommerce-connector 0.2.0 → 0.3.1

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.cjs CHANGED
@@ -138,630 +138,122 @@ function forgetAll() {
138
138
  entries.clear();
139
139
  }
140
140
 
141
- // src/map.ts
142
- var str = (v) => v === null || v === void 0 ? "" : String(v);
143
- var arr = (v) => v ?? [];
144
- var liveVariants = (p) => arr(p.variants).filter((v) => v.active);
145
- function variantImages(v) {
146
- const out = [];
147
- for (const img of [v.image, ...arr(v.images)]) {
148
- if (img?.url && (img.kind ?? "image") === "image" && !out.includes(img.url)) out.push(img.url);
141
+ // src/browser.ts
142
+ var inBrowser = () => typeof window !== "undefined" && typeof document !== "undefined";
143
+ function readLocal(key) {
144
+ if (!inBrowser()) return null;
145
+ try {
146
+ return window.localStorage.getItem(key);
147
+ } catch {
148
+ return null;
149
149
  }
150
- return out;
151
150
  }
152
- function productImages(p) {
153
- const out = [];
154
- const push = (url) => {
155
- if (url && !out.includes(url)) out.push(url);
156
- };
157
- push(p.image_url);
158
- for (const v of arr(p.variants)) for (const url of variantImages(v)) push(url);
159
- return out;
151
+ function writeLocal(key, value) {
152
+ if (!inBrowser()) return;
153
+ try {
154
+ window.localStorage.setItem(key, value);
155
+ } catch {
156
+ }
160
157
  }
161
- function toVariant(v, p, optionless) {
162
- const images = variantImages(v);
163
- const options = arr(p.options);
164
- return {
165
- id: str(v.id),
166
- productId: str(v.product_id),
167
- // The storefront picks the default variant of an optionless product by
168
- // this exact title, and hides the variant name when it sees it.
169
- title: optionless ? "default" : v.label || v.sku,
170
- sku: v.sku,
171
- barcode: v.barcode ?? null,
172
- price: toMajor(v.price),
173
- mrp: toMajor(v.compare_at_price ?? v.price),
174
- // The engine's own vocabulary, unchanged: `available` is on hand minus
175
- // reserved; an untracked variant is never managed, so its count is moot.
176
- stock: v.active ? v.available : 0,
177
- manageInventory: v.active ? v.track_inventory : true,
178
- allowBackorder: v.active && v.continue_selling,
179
- active: v.active,
180
- weight: v.weight_grams ?? null,
181
- length: v.dimensions?.length_mm ?? null,
182
- width: v.dimensions?.width_mm ?? null,
183
- height: v.dimensions?.height_mm ?? null,
184
- images: images.join(","),
185
- img: images[0] ?? null,
186
- image: images[0] ?? null,
187
- thumbnail: images[0] ?? null,
188
- // Values in option order — `options[i]` answers `p.options[i]`.
189
- options: arr(v.options).map((value, i) => ({ optionId: str(options[i]?.id), value })),
190
- metadata: v.metadata ?? null
191
- };
158
+ function removeLocal(key) {
159
+ if (!inBrowser()) return;
160
+ try {
161
+ window.localStorage.removeItem(key);
162
+ } catch {
163
+ }
192
164
  }
193
- function toProduct(p, extra = {}) {
194
- const live = liveVariants(p);
195
- const lead = live[0] ?? arr(p.variants)[0];
196
- const optionless = arr(p.options).length === 0;
197
- const images = productImages(p);
198
- const prices = live.map((v) => toMajor(v.price));
199
- const tracked = live.filter((v) => v.track_inventory);
200
- const category = p.category;
201
- const tags = arr(p.tags);
202
- return {
203
- id: str(p.id),
204
- slug: p.slug,
205
- title: p.title,
206
- name: p.title,
207
- subtitle: null,
208
- description: p.description ?? "",
209
- status: p.status === "active" ? "published" : p.status,
210
- active: p.status === "active",
211
- type: p.product_type ?? "",
212
- currency: p.currency,
213
- currencyCode: p.currency,
214
- price: lead ? toMajor(lead.price) : 0,
215
- mrp: lead ? toMajor(lead.compare_at_price ?? lead.price) : 0,
216
- minPrice: prices.length ? Math.min(...prices) : 0,
217
- maxPrice: prices.length ? Math.max(...prices) : 0,
218
- sku: lead?.sku ?? null,
219
- barcode: lead?.barcode ?? null,
220
- weight: lead?.weight_grams ?? null,
221
- originCountry: lead?.origin_country ?? null,
222
- thumbnail: images[0] ?? null,
223
- image_url: images[0] ?? null,
224
- images: images.join(","),
225
- imageList: images,
226
- // Product-level stock is what a card shows before a variant is picked:
227
- // buyable when any live variant is, counted only where counting means
228
- // something.
229
- manageInventory: live.length > 0 && tracked.length === live.length,
230
- allowBackorder: live.some((v) => v.continue_selling),
231
- stock: tracked.reduce((n, v) => n + Math.max(0, v.available), 0),
232
- hasStock: live.some((v) => !v.track_inventory || v.continue_selling || v.available > 0),
233
- categoryId: category ? str(category.id) : null,
234
- category: category ? { id: str(category.id), name: category.title, slug: category.slug } : null,
235
- categories: category ? [{ category: { id: str(category.id), name: category.title, slug: category.slug } }] : [],
236
- categoryHierarchy: extra.categoryHierarchy ?? (category ? [{ name: category.title, slug: category.slug }] : []),
237
- collections: arr(p.collections).map((c) => ({ id: str(c.id), name: c.title, slug: c.slug })),
238
- // Free text on the engine's product, not a vendor record.
239
- vendor: p.vendor ? { businessName: p.vendor } : null,
240
- productTags: tags.join(","),
241
- keywords: tags.join(","),
242
- metaTitle: p.seo_title || p.title,
243
- metaDescription: p.seo_description || null,
244
- link: `/products/${p.slug}`,
245
- options: arr(p.options).map((o) => ({
246
- id: str(o.id),
247
- title: o.name,
248
- name: o.name,
249
- type: "text",
250
- values: arr(o.values).map((val) => ({ id: str(val.id), value: val.value }))
251
- })),
252
- variants: live.map((v) => toVariant(v, p, optionless)),
253
- // Filled by the product page from ext/reviews when it is installed.
254
- ratings: [],
255
- rating: null,
256
- ratingCount: 0,
257
- reviewCount: 0,
258
- attributes: [],
259
- metadata: p.metadata ?? null,
260
- createdAt: p.created_at,
261
- updatedAt: p.updated_at
262
- };
165
+ function readJSON(key) {
166
+ const raw = readLocal(key);
167
+ if (!raw) return null;
168
+ try {
169
+ return JSON.parse(raw);
170
+ } catch {
171
+ return null;
172
+ }
263
173
  }
264
- function fromSearchDoc(d) {
265
- const price = minorToMajor(d.price_minor, d.currency);
266
- return {
267
- id: str(d.id),
268
- slug: d.slug,
269
- title: d.title,
270
- name: d.title,
271
- description: d.description,
272
- currency: d.currency,
273
- price,
274
- mrp: price,
275
- minPrice: price,
276
- maxPrice: minorToMajor(d.price_max_minor, d.currency),
277
- thumbnail: d.image ?? null,
278
- image_url: d.image ?? null,
279
- images: d.image ?? "",
280
- stock: d.in_stock ? 1 : 0,
281
- hasStock: d.in_stock,
282
- // The index cannot say how much is left, only whether any is; a card
283
- // built from it must not claim to count.
284
- manageInventory: false,
285
- allowBackorder: false,
286
- productTags: arr(d.tags).join(","),
287
- category: d.category ? { name: d.category.split(" / ").pop() ?? d.category, slug: "" } : null,
288
- vendor: d.vendor ? { businessName: d.vendor } : null,
289
- variants: [],
290
- options: [],
291
- ratings: [],
292
- link: `/products/${d.slug}`,
293
- updatedAt: new Date(d.updated_at * 1e3).toISOString()
294
- };
174
+ var YEAR = 60 * 60 * 24 * 365;
175
+ function setCookie(name, value, maxAge = YEAR) {
176
+ if (!inBrowser()) return;
177
+ document.cookie = `${name}=${value}; path=/; max-age=${maxAge}; samesite=lax`;
295
178
  }
296
- function toCategory(c) {
297
- return {
298
- id: str(c.id),
299
- name: c.title,
300
- title: c.title,
301
- slug: c.slug,
302
- link: `/${c.slug}`,
303
- parentCategoryId: c.parent_id === null || c.parent_id === void 0 ? null : str(c.parent_id),
304
- path: c.full_name,
305
- level: c.depth,
306
- rank: c.position,
307
- childCount: c.child_count,
308
- thumbnail: c.metadata?.image ?? null,
309
- description: c.metadata?.description ?? null,
310
- isActive: true,
311
- isFeatured: false,
312
- isMegamenu: false,
313
- metaTitle: c.title,
314
- children: arr(c.children).map(toCategory),
315
- createdAt: c.created_at,
316
- updatedAt: c.updated_at
317
- };
179
+ function clearCookie(name) {
180
+ if (!inBrowser()) return;
181
+ document.cookie = `${name}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`;
318
182
  }
319
- function toCollection(c) {
320
- return {
321
- id: str(c.id),
322
- name: c.title,
323
- title: c.title,
324
- slug: c.slug,
325
- description: c.description || null,
326
- subTitle: null,
327
- rank: str(c.position),
328
- thumbnail: c.metadata?.image ?? null,
329
- isActive: true,
330
- metaTitle: c.title,
331
- metaDescription: c.description || null,
332
- createdAt: c.created_at,
333
- updatedAt: c.updated_at
334
- };
183
+ function readCookie(name) {
184
+ if (!inBrowser()) return null;
185
+ for (const pair of document.cookie.split(";")) {
186
+ const at = pair.indexOf("=");
187
+ if (at < 0) continue;
188
+ if (pair.slice(0, at).trim() === name) return pair.slice(at + 1).trim();
189
+ }
190
+ return null;
335
191
  }
336
- function fromEngineAddress(a, email) {
337
- if (!a) return null;
338
- const [firstName, ...rest] = (a.name ?? "").trim().split(/\s+/);
192
+ var TOKEN = "gocommerce_token";
193
+ var sessionToken = () => readLocal(TOKEN);
194
+ function toMe(c) {
195
+ const [firstName, ...rest] = (c.name ?? "").trim().split(/\s+/);
339
196
  return {
197
+ id: String(c.id),
198
+ userId: String(c.id),
199
+ email: c.email,
200
+ phone: c.phone || null,
340
201
  firstName: firstName || "",
341
202
  lastName: rest.join(" "),
342
- email: email ?? null,
343
- phone: a.phone ?? null,
344
- address_1: a.line1,
345
- address_2: a.line2 ?? "",
346
- city: a.city,
347
- state: a.state ?? "",
348
- zip: a.postal_code,
349
- country: a.country,
350
- countryCode: a.country
203
+ name: c.name ?? "",
204
+ fullName: c.name ?? "",
205
+ avatar: null,
206
+ role: "USER",
207
+ emailVerified: c.email_verified,
208
+ storeId: null
351
209
  };
352
210
  }
353
- function toEngineAddress(a) {
354
- const name = [a.firstName, a.lastName].filter(Boolean).join(" ").trim();
355
- return {
356
- ...name ? { name } : {},
357
- ...a.phone ? { phone: String(a.phone) } : {},
358
- line1: String(a.address_1 ?? "").trim(),
359
- ...a.address_2 || a.locality ? { line2: [a.address_2, a.locality].filter(Boolean).join(", ") } : {},
360
- city: String(a.city ?? "").trim(),
361
- ...a.state ? { state: String(a.state) } : {},
362
- postal_code: String(a.zip ?? "").trim(),
363
- country: String(a.countryCode || a.country || "").trim().toUpperCase()
364
- };
365
- }
366
- function fromIdentityAddress(a, email) {
367
- const [firstName, ...rest] = (a.name ?? "").trim().split(/\s+/);
368
- return {
369
- id: str(a.id),
370
- firstName: firstName || "",
371
- lastName: rest.join(" "),
372
- email: email ?? null,
373
- phone: a.phone || null,
374
- address_1: a.line1,
375
- address_2: a.line2,
376
- city: a.city,
377
- state: a.state,
378
- zip: a.postal_code,
379
- country: a.country,
380
- countryCode: a.country,
381
- label: a.label,
382
- isPrimary: a.is_default,
383
- createdAt: a.created_at,
384
- updatedAt: a.updated_at
385
- };
211
+ function startSession(auth) {
212
+ writeLocal(TOKEN, auth.token);
213
+ const me = toMe(auth.record);
214
+ const expires = Math.max(60, Math.floor((Date.parse(auth.expires_at) - Date.now()) / 1e3)) || YEAR;
215
+ setCookie("me", encodeURIComponent(JSON.stringify(me)), expires);
216
+ setCookie("connect.sid", "gocommerce", expires);
217
+ return me;
386
218
  }
387
- function toIdentityAddress(a) {
388
- const e = toEngineAddress(a);
389
- return {
390
- name: e.name ?? "",
391
- phone: e.phone ?? "",
392
- line1: e.line1,
393
- line2: e.line2 ?? "",
394
- city: e.city,
395
- state: e.state ?? "",
396
- postal_code: e.postal_code,
397
- country: e.country,
398
- ...a.label ? { label: String(a.label) } : {},
399
- ...a.isPrimary ? { is_default: true } : {}
400
- };
219
+ function updateMe(c) {
220
+ const me = toMe(c);
221
+ setCookie("me", encodeURIComponent(JSON.stringify(me)));
222
+ return me;
401
223
  }
402
- function toCartLine(l, extra = {}) {
403
- return {
404
- id: str(l.id),
405
- productId: str(l.product_id),
406
- variantId: str(l.variant_id),
407
- sku: l.sku,
408
- title: l.title,
409
- name: l.title,
410
- slug: extra.slug ?? "",
411
- thumbnail: extra.thumbnail ?? null,
412
- variantTitle: l.variant_label ?? "",
413
- qty: l.quantity,
414
- price: toMajor(l.unit_price),
415
- mrp: toMajor(l.unit_price),
416
- subtotal: toMajor(l.total),
417
- total: toMajor(l.total),
418
- inStock: l.in_stock,
419
- priceChanged: l.price_changed,
420
- currentPrice: toMajor(l.current_price),
421
- // The storefront's review step lists only lines marked for checkout. The
422
- // engine checks a cart out whole, so every line is.
423
- isSelectedForCheckout: true,
424
- variant: {
425
- id: str(l.variant_id),
426
- options: l.variant_label ? l.variant_label.split(" / ").map((value) => ({ option: { title: "" }, value })) : []
427
- },
428
- product: { id: str(l.product_id), categories: [] }
429
- };
224
+ function endSession() {
225
+ removeLocal(TOKEN);
226
+ clearCookie("me");
227
+ clearCookie("connect.sid");
430
228
  }
431
- function toCart(c, extras = {}, lineExtra = () => ({})) {
432
- const lines = c.status === "converted" ? [] : arr(c.line_items);
433
- const currency = c.currency;
434
- const subtotalMinor = lines.reduce((n, l) => n + l.total.amount_minor, 0);
435
- const discountMinor = extras.discount && !extras.discount.free_shipping ? extras.discount.amount_minor : 0;
436
- const shippingMinor = extras.discount?.free_shipping ? 0 : extras.shippingRate?.minor ?? 0;
437
- const totalMinor = Math.max(0, subtotalMinor - discountMinor) + shippingMinor;
438
- const money = (minor) => minorToMajor(minor, currency);
439
- const ship = extras.shippingAddress ?? null;
440
- return {
441
- id: c.id,
442
- status: c.status,
443
- email: c.email ?? ship?.email ?? null,
444
- phone: extras.phone ?? ship?.phone ?? null,
445
- name: ship ? [ship.firstName, ship.lastName].filter(Boolean).join(" ") : null,
446
- lineItems: lines.map((l) => toCartLine(l, lineExtra(l))),
447
- qty: lines.reduce((n, l) => n + l.quantity, 0),
448
- currencyCode: currency,
449
- currency,
450
- subtotal: money(subtotalMinor),
451
- discountAmount: money(discountMinor),
452
- savingAmount: money(discountMinor),
453
- couponCode: extras.discount?.code ?? null,
454
- discount: extras.discount ?? null,
455
- shippingCharges: money(shippingMinor),
456
- shippingRateId: extras.shippingRateId ?? null,
457
- shippingMethod: extras.shippingRate?.name ?? null,
458
- tax: 0,
459
- taxIncluded: false,
460
- codCharges: 0,
461
- total: money(totalMinor),
462
- shippingAddress: ship,
463
- shippingAddressId: ship ? ship.id && ship.id !== "new" ? String(ship.id) : "cart" : null,
464
- billingAddress: extras.billingAddress ?? ship,
465
- billingAddressId: extras.billingAddress ? extras.billingAddress.id && extras.billingAddress.id !== "new" ? String(extras.billingAddress.id) : "cart-billing" : ship ? ship.id && ship.id !== "new" ? String(ship.id) : "cart" : null,
466
- paymentMethod: null,
467
- needAddress: lines.length > 0,
468
- completedAt: c.status === "converted" ? c.updated_at : null,
469
- createdAt: c.created_at,
470
- updatedAt: c.updated_at,
471
- expiresAt: c.expires_at
472
- };
229
+ function currentMe() {
230
+ const raw = readCookie("me");
231
+ if (!raw) return null;
232
+ try {
233
+ return JSON.parse(decodeURIComponent(raw));
234
+ } catch {
235
+ return null;
236
+ }
473
237
  }
474
- function emptyCart(currency = "USD") {
475
- return {
476
- id: "",
477
- status: "open",
478
- email: null,
479
- phone: null,
480
- lineItems: [],
481
- qty: 0,
482
- currencyCode: currency,
483
- subtotal: 0,
484
- discountAmount: 0,
485
- savingAmount: 0,
486
- couponCode: null,
487
- shippingCharges: 0,
488
- tax: 0,
489
- total: 0,
490
- shippingAddress: null,
491
- shippingAddressId: null,
492
- billingAddress: null,
493
- billingAddressId: null
494
- };
238
+ function storedCartId() {
239
+ const v = readLocal("cart_id");
240
+ if (!v || v === "undefined" || v === "null") return null;
241
+ return v;
495
242
  }
496
- var STATUS = {
497
- pending: "Pending",
498
- confirmed: "Confirmed",
499
- partial: "Partially shipped",
500
- shipped: "Shipped",
501
- delivered: "Delivered",
502
- cancelled: "Cancelled"
503
- };
504
- var PAYMENT_STATUS = {
505
- pending: "Pending",
506
- paid: "Paid",
507
- failed: "Failed",
508
- refunded: "Refunded"
509
- };
510
- function toOrderLine(l, currency) {
511
- return {
512
- id: str(l.id),
513
- productId: str(l.product_id),
514
- variantId: str(l.variant_id),
515
- sku: l.sku,
516
- title: l.title,
517
- name: l.title,
518
- slug: "",
519
- thumbnail: l.image_url ?? null,
520
- img: l.image_url ?? null,
521
- variantTitle: l.variant_label ?? "",
522
- qty: l.quantity,
523
- shippedQty: l.shipped_quantity,
524
- price: toMajor(l.unit_price),
525
- mrp: toMajor(l.unit_price),
526
- subtotal: toMajor(l.total),
527
- total: toMajor(l.total),
528
- currencyCode: currency,
529
- status: l.shipped_quantity >= l.quantity ? "Shipped" : "Pending",
530
- files: [],
531
- usedOptions: []
532
- };
243
+ var rememberCartId = (id2) => writeLocal("cart_id", id2);
244
+ var extrasKey = (cartId) => `gocommerce_cart_${cartId}`;
245
+ var cartExtras = (cartId) => readJSON(extrasKey(cartId)) ?? {};
246
+ function saveCartExtras(cartId, patch) {
247
+ const next = { ...cartExtras(cartId), ...patch };
248
+ writeLocal(extrasKey(cartId), JSON.stringify(next));
249
+ return next;
533
250
  }
534
- function toOrder(o) {
535
- const address = fromEngineAddress(o.address, o.email);
536
- const code = (o.discounts ?? []).find((d) => d.code)?.code;
537
- return {
538
- id: str(o.id),
539
- orderNo: o.number,
540
- orderNumber: o.number,
541
- number: o.number,
542
- // The storefront links an order by this; the engine has no sub-orders,
543
- // so an order is its own parent.
544
- parentOrderNo: o.number,
545
- status: STATUS[o.status] ?? o.status,
546
- engineStatus: o.status,
547
- paymentStatus: PAYMENT_STATUS[o.payment_status] ?? o.payment_status,
548
- paymentMethod: o.payment_provider.toUpperCase(),
549
- currencyCode: o.currency,
550
- subtotal: toMajor(o.subtotal),
551
- shippingCharges: toMajor(o.shipping),
552
- discount: toMajor(o.discount),
553
- tax: toMajor(o.tax),
554
- taxIncluded: o.tax_inclusive,
555
- total: toMajor(o.total),
556
- refunded: toMajor(o.refunded),
557
- codCharges: 0,
558
- coupon: code ? { code } : null,
559
- userEmail: o.email,
560
- email: o.email,
561
- phone: o.phone ?? null,
562
- shippingAddress: address,
563
- billingAddress: address,
564
- shippingMethod: o.shipping_method ?? null,
565
- shippingRate: o.shipping_method ? { name: o.shipping_method } : null,
566
- lineItems: arr(o.line_items).map((l) => toOrderLine(l, o.currency)),
567
- fulfillments: arr(o.fulfillments).map((f) => ({
568
- id: str(f.id),
569
- status: f.status,
570
- carrier: f.carrier_name || f.carrier || f.provider,
571
- trackingNumber: f.tracking ?? null,
572
- trackingUrl: f.tracking_url ?? null,
573
- lineItems: arr(f.lines).map((l) => ({ id: str(l.order_line_id), title: l.title, qty: l.quantity })),
574
- createdAt: f.created_at
575
- })),
576
- tracking: [],
577
- orderHistory: [],
578
- isReplaceOrReturn: false,
579
- createdAt: o.created_at,
580
- updatedAt: o.updated_at
581
- };
582
- }
583
- function toRating(r) {
584
- return {
585
- id: str(r.id),
586
- rating: r.rating,
587
- name: r.name,
588
- title: r.title ?? "",
589
- review: r.body,
590
- message: r.body,
591
- reply: r.reply ?? null,
592
- verified: r.verified,
593
- img: "",
594
- createdAt: r.created_at
595
- };
596
- }
597
- function toCmsPage(p) {
598
- return {
599
- id: str(p.id),
600
- slug: p.slug,
601
- name: p.title,
602
- title: p.title,
603
- content: p.body,
604
- excerpt: p.excerpt ?? null,
605
- layouts: [],
606
- language: p.language,
607
- metaTitle: p.seo?.title || p.title,
608
- metaDescription: p.seo?.description || p.excerpt || null,
609
- metaKeywords: p.seo?.keywords || null,
610
- publishedAt: p.published_at ?? null,
611
- createdAt: p.created_at,
612
- updatedAt: p.updated_at
613
- };
614
- }
615
- function toVendor(v) {
616
- return {
617
- id: str(v.id),
618
- slug: v.slug,
619
- name: v.name,
620
- businessName: v.name,
621
- about: v.about,
622
- description: v.about,
623
- website: v.website,
624
- address: v.address,
625
- banners: [],
626
- rating: null
627
- };
628
- }
629
- function toUser(c) {
630
- const [firstName, ...rest] = (c.name ?? "").trim().split(/\s+/);
631
- return {
632
- id: str(c.id),
633
- userId: str(c.id),
634
- email: c.email,
635
- emailVerified: c.email_verified,
636
- phone: c.phone || null,
637
- firstName: firstName || "",
638
- lastName: rest.join(" "),
639
- name: c.name,
640
- fullName: c.name,
641
- avatar: null,
642
- role: "USER",
643
- createdAt: c.created_at,
644
- updatedAt: c.updated_at
645
- };
646
- }
647
- var priceOf = (m) => m ? toMajor(m) : 0;
648
-
649
- // src/browser.ts
650
- var inBrowser = () => typeof window !== "undefined" && typeof document !== "undefined";
651
- function readLocal(key) {
652
- if (!inBrowser()) return null;
653
- try {
654
- return window.localStorage.getItem(key);
655
- } catch {
656
- return null;
657
- }
658
- }
659
- function writeLocal(key, value) {
660
- if (!inBrowser()) return;
661
- try {
662
- window.localStorage.setItem(key, value);
663
- } catch {
664
- }
665
- }
666
- function removeLocal(key) {
667
- if (!inBrowser()) return;
668
- try {
669
- window.localStorage.removeItem(key);
670
- } catch {
671
- }
672
- }
673
- function readJSON(key) {
674
- const raw = readLocal(key);
675
- if (!raw) return null;
676
- try {
677
- return JSON.parse(raw);
678
- } catch {
679
- return null;
680
- }
681
- }
682
- var YEAR = 60 * 60 * 24 * 365;
683
- function setCookie(name, value, maxAge = YEAR) {
684
- if (!inBrowser()) return;
685
- document.cookie = `${name}=${value}; path=/; max-age=${maxAge}; samesite=lax`;
686
- }
687
- function clearCookie(name) {
688
- if (!inBrowser()) return;
689
- document.cookie = `${name}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`;
690
- }
691
- function readCookie(name) {
692
- if (!inBrowser()) return null;
693
- for (const pair of document.cookie.split(";")) {
694
- const at = pair.indexOf("=");
695
- if (at < 0) continue;
696
- if (pair.slice(0, at).trim() === name) return pair.slice(at + 1).trim();
697
- }
698
- return null;
699
- }
700
- var TOKEN = "gocommerce_token";
701
- var sessionToken = () => readLocal(TOKEN);
702
- function toMe(c) {
703
- const [firstName, ...rest] = (c.name ?? "").trim().split(/\s+/);
704
- return {
705
- id: String(c.id),
706
- userId: String(c.id),
707
- email: c.email,
708
- phone: c.phone || null,
709
- firstName: firstName || "",
710
- lastName: rest.join(" "),
711
- name: c.name ?? "",
712
- fullName: c.name ?? "",
713
- avatar: null,
714
- role: "USER",
715
- emailVerified: c.email_verified,
716
- storeId: null
717
- };
718
- }
719
- function startSession(auth) {
720
- writeLocal(TOKEN, auth.token);
721
- const me = toMe(auth.record);
722
- const expires = Math.max(60, Math.floor((Date.parse(auth.expires_at) - Date.now()) / 1e3)) || YEAR;
723
- setCookie("me", encodeURIComponent(JSON.stringify(me)), expires);
724
- setCookie("connect.sid", "gocommerce", expires);
725
- return me;
726
- }
727
- function updateMe(c) {
728
- const me = toMe(c);
729
- setCookie("me", encodeURIComponent(JSON.stringify(me)));
730
- return me;
731
- }
732
- function endSession() {
733
- removeLocal(TOKEN);
734
- clearCookie("me");
735
- clearCookie("connect.sid");
736
- }
737
- function currentMe() {
738
- const raw = readCookie("me");
739
- if (!raw) return null;
740
- try {
741
- return JSON.parse(decodeURIComponent(raw));
742
- } catch {
743
- return null;
744
- }
745
- }
746
- function storedCartId() {
747
- const v = readLocal("cart_id");
748
- if (!v || v === "undefined" || v === "null") return null;
749
- return v;
750
- }
751
- var rememberCartId = (id) => writeLocal("cart_id", id);
752
- var extrasKey = (cartId) => `gocommerce_cart_${cartId}`;
753
- var cartExtras = (cartId) => readJSON(extrasKey(cartId)) ?? {};
754
- function saveCartExtras(cartId, patch) {
755
- const next = { ...cartExtras(cartId), ...patch };
756
- writeLocal(extrasKey(cartId), JSON.stringify(next));
757
- return next;
758
- }
759
- var forgetCartExtras = (cartId) => removeLocal(extrasKey(cartId));
760
- var ORDERS = "gocommerce_orders";
761
- var placedOrders = () => readJSON(ORDERS) ?? [];
762
- function rememberOrder(o) {
763
- const kept = placedOrders().filter((p) => p.number !== o.number);
764
- writeLocal(ORDERS, JSON.stringify([o, ...kept].slice(0, 50)));
251
+ var forgetCartExtras = (cartId) => removeLocal(extrasKey(cartId));
252
+ var ORDERS = "gocommerce_orders";
253
+ var placedOrders = () => readJSON(ORDERS) ?? [];
254
+ function rememberOrder(o) {
255
+ const kept = placedOrders().filter((p) => p.number !== o.number);
256
+ writeLocal(ORDERS, JSON.stringify([o, ...kept].slice(0, 50)));
765
257
  }
766
258
  var placedOrder = (number) => placedOrders().find((p) => p.number === number);
767
259
  var placedOrderForCart = (cartId) => placedOrders().find((p) => p.cartId === cartId);
@@ -772,6 +264,12 @@ var rememberWishlistToken = (token) => writeLocal(WISHLIST, token);
772
264
  var forgetWishlistToken = () => removeLocal(WISHLIST);
773
265
 
774
266
  // src/services/base.service.ts
267
+ function mediaUrl(url) {
268
+ if (!url) return null;
269
+ if (!url.startsWith("/") || url.startsWith("//")) return url;
270
+ const { apiUrl, proxyPath } = BaseService.getCredentials();
271
+ return (proxyPath || apiUrl || "").trim().replace(/\/+$/, "") + url;
272
+ }
775
273
  var BaseService = class _BaseService {
776
274
  static _credentials = { apiUrl: "" };
777
275
  _fetch;
@@ -816,7 +314,8 @@ var BaseService = class _BaseService {
816
314
  * as what it is rather than as a JSON parse error three frames away.
817
315
  */
818
316
  async engine(path, req = {}) {
819
- const url = this.engineURL(path, req.query);
317
+ const lang = (req.method ?? "GET") === "GET" ? this.creds.language : void 0;
318
+ const url = this.engineURL(path, lang ? { lang, ...req.query } : req.query);
820
319
  const headers = { Accept: "application/json", ...req.headers };
821
320
  if (req.body !== void 0) headers["Content-Type"] = "application/json";
822
321
  if (this.creds.store) headers["X-Store"] = this.creds.store;
@@ -933,18 +432,537 @@ function qs(query) {
933
432
  const s = sp.toString();
934
433
  return s ? `?${s}` : "";
935
434
  }
936
- function toPage(env, map) {
937
- const rows = env.data ?? [];
938
- const meta = env.meta;
435
+ function toPage(env, map) {
436
+ const rows = env.data ?? [];
437
+ const meta = env.meta;
438
+ return {
439
+ data: rows.map(map),
440
+ count: meta?.total ?? rows.length,
441
+ pageSize: meta?.limit ?? rows.length,
442
+ noOfPage: meta?.total_pages ?? 1,
443
+ page: meta?.page ?? 1
444
+ };
445
+ }
446
+ var emptyPage = () => ({ data: [], count: 0, pageSize: 0, noOfPage: 0, page: 1 });
447
+
448
+ // src/map.ts
449
+ var str = (v) => v === null || v === void 0 ? "" : String(v);
450
+ var arr = (v) => v ?? [];
451
+ var liveVariants = (p) => arr(p.variants).filter((v) => v.active);
452
+ function variantImages(v) {
453
+ const out = [];
454
+ for (const img of [v.image, ...arr(v.images)]) {
455
+ const url = mediaUrl(img?.url);
456
+ if (url && (img?.kind ?? "image") === "image" && !out.includes(url)) out.push(url);
457
+ }
458
+ return out;
459
+ }
460
+ function productImages(p) {
461
+ const out = [];
462
+ const push = (url) => {
463
+ if (url && !out.includes(url)) out.push(url);
464
+ };
465
+ push(mediaUrl(p.image_url));
466
+ for (const v of arr(p.variants)) for (const url of variantImages(v)) push(url);
467
+ return out;
468
+ }
469
+ function toVariant(v, p, optionless) {
470
+ const images = variantImages(v);
471
+ const options = arr(p.options);
472
+ return {
473
+ id: str(v.id),
474
+ productId: str(v.product_id),
475
+ // The storefront picks the default variant of an optionless product by
476
+ // this exact title, and hides the variant name when it sees it.
477
+ title: optionless ? "default" : v.label || v.sku,
478
+ sku: v.sku,
479
+ barcode: v.barcode ?? null,
480
+ price: toMajor(v.price),
481
+ mrp: toMajor(v.compare_at_price ?? v.price),
482
+ // The engine's own vocabulary, unchanged: `available` is on hand minus
483
+ // reserved; an untracked variant is never managed, so its count is moot.
484
+ stock: v.active ? v.available : 0,
485
+ manageInventory: v.active ? v.track_inventory : true,
486
+ allowBackorder: v.active && v.continue_selling,
487
+ active: v.active,
488
+ weight: v.weight_grams ?? null,
489
+ length: v.dimensions?.length_mm ?? null,
490
+ width: v.dimensions?.width_mm ?? null,
491
+ height: v.dimensions?.height_mm ?? null,
492
+ images: images.join(","),
493
+ img: images[0] ?? null,
494
+ image: images[0] ?? null,
495
+ thumbnail: images[0] ?? null,
496
+ // Values in option order — `options[i]` answers `p.options[i]`.
497
+ options: arr(v.options).map((value, i) => ({ optionId: str(options[i]?.id), value })),
498
+ metadata: v.metadata ?? null
499
+ };
500
+ }
501
+ function attributesOf(p) {
502
+ const answers = p.metadata?.category;
503
+ if (!answers || typeof answers !== "object" || Array.isArray(answers)) return [];
504
+ return Object.entries(answers).flatMap(([handle, raw]) => {
505
+ const values = (Array.isArray(raw) ? raw : [raw]).map((v) => String(v ?? "").trim()).filter(Boolean);
506
+ if (!values.length) return [];
507
+ const name = handle.replace(/[-_]+/g, " ").replace(/^./, (c) => c.toUpperCase());
508
+ return [{ name, value: values.join(", "), handle }];
509
+ });
510
+ }
511
+ function toProduct(p, extra = {}) {
512
+ const live = liveVariants(p);
513
+ const lead = live[0] ?? arr(p.variants)[0];
514
+ const optionless = arr(p.options).length === 0;
515
+ const images = productImages(p);
516
+ const prices = live.map((v) => toMajor(v.price));
517
+ const tracked = live.filter((v) => v.track_inventory);
518
+ const category = p.category;
519
+ const tags = arr(p.tags);
520
+ return {
521
+ id: str(p.id),
522
+ slug: p.slug,
523
+ title: p.title,
524
+ name: p.title,
525
+ subtitle: null,
526
+ description: p.description ?? "",
527
+ status: p.status === "active" ? "published" : p.status,
528
+ active: p.status === "active",
529
+ type: p.product_type ?? "",
530
+ currency: p.currency,
531
+ currencyCode: p.currency,
532
+ price: lead ? toMajor(lead.price) : 0,
533
+ mrp: lead ? toMajor(lead.compare_at_price ?? lead.price) : 0,
534
+ minPrice: prices.length ? Math.min(...prices) : 0,
535
+ maxPrice: prices.length ? Math.max(...prices) : 0,
536
+ sku: lead?.sku ?? null,
537
+ barcode: lead?.barcode ?? null,
538
+ weight: lead?.weight_grams ?? null,
539
+ originCountry: lead?.origin_country ?? null,
540
+ thumbnail: images[0] ?? null,
541
+ image_url: images[0] ?? null,
542
+ images: images.join(","),
543
+ imageList: images,
544
+ // Product-level stock is what a card shows before a variant is picked:
545
+ // buyable when any live variant is, counted only where counting means
546
+ // something.
547
+ manageInventory: live.length > 0 && tracked.length === live.length,
548
+ allowBackorder: live.some((v) => v.continue_selling),
549
+ stock: tracked.reduce((n, v) => n + Math.max(0, v.available), 0),
550
+ hasStock: live.some((v) => !v.track_inventory || v.continue_selling || v.available > 0),
551
+ categoryId: category ? str(category.id) : null,
552
+ category: category ? { id: str(category.id), name: category.title, slug: category.slug } : null,
553
+ categories: category ? [{ category: { id: str(category.id), name: category.title, slug: category.slug } }] : [],
554
+ categoryHierarchy: extra.categoryHierarchy ?? (category ? [{ name: category.title, slug: category.slug }] : []),
555
+ collections: arr(p.collections).map((c) => ({ id: str(c.id), name: c.title, slug: c.slug })),
556
+ // Free text on the engine's product, not a vendor record.
557
+ vendor: p.vendor ? { businessName: p.vendor } : null,
558
+ productTags: tags.join(","),
559
+ keywords: tags.join(","),
560
+ metaTitle: p.seo_title || p.title,
561
+ metaDescription: p.seo_description || null,
562
+ link: `/products/${p.slug}`,
563
+ options: arr(p.options).map((o) => ({
564
+ id: str(o.id),
565
+ title: o.name,
566
+ name: o.name,
567
+ type: "text",
568
+ values: arr(o.values).map((val) => ({ id: str(val.id), value: val.value }))
569
+ })),
570
+ variants: live.map((v) => toVariant(v, p, optionless)),
571
+ // Filled by the product page from ext/reviews when it is installed.
572
+ ratings: [],
573
+ rating: null,
574
+ ratingCount: 0,
575
+ reviewCount: 0,
576
+ attributes: attributesOf(p),
577
+ metadata: p.metadata ?? null,
578
+ createdAt: p.created_at,
579
+ updatedAt: p.updated_at
580
+ };
581
+ }
582
+ function fromSearchDoc(d) {
583
+ const price = minorToMajor(d.price_minor, d.currency);
584
+ return {
585
+ id: str(d.id),
586
+ slug: d.slug,
587
+ title: d.title,
588
+ name: d.title,
589
+ description: d.description,
590
+ currency: d.currency,
591
+ price,
592
+ mrp: price,
593
+ minPrice: price,
594
+ maxPrice: minorToMajor(d.price_max_minor, d.currency),
595
+ thumbnail: mediaUrl(d.image),
596
+ image_url: mediaUrl(d.image),
597
+ images: mediaUrl(d.image) ?? "",
598
+ stock: d.in_stock ? 1 : 0,
599
+ hasStock: d.in_stock,
600
+ // The index cannot say how much is left, only whether any is; a card
601
+ // built from it must not claim to count.
602
+ manageInventory: false,
603
+ allowBackorder: false,
604
+ productTags: arr(d.tags).join(","),
605
+ category: d.category ? { name: d.category.split(" / ").pop() ?? d.category, slug: "" } : null,
606
+ vendor: d.vendor ? { businessName: d.vendor } : null,
607
+ variants: [],
608
+ options: [],
609
+ ratings: [],
610
+ link: `/products/${d.slug}`,
611
+ updatedAt: new Date(d.updated_at * 1e3).toISOString()
612
+ };
613
+ }
614
+ function toCategory(c) {
615
+ return {
616
+ id: str(c.id),
617
+ name: c.title,
618
+ title: c.title,
619
+ slug: c.slug,
620
+ link: `/${c.slug}`,
621
+ parentCategoryId: c.parent_id === null || c.parent_id === void 0 ? null : str(c.parent_id),
622
+ path: c.full_name,
623
+ level: c.depth,
624
+ rank: c.position,
625
+ childCount: c.child_count,
626
+ thumbnail: mediaUrl(c.metadata?.image),
627
+ description: c.metadata?.description ?? null,
628
+ isActive: true,
629
+ isFeatured: false,
630
+ isMegamenu: false,
631
+ metaTitle: c.title,
632
+ children: arr(c.children).map(toCategory),
633
+ createdAt: c.created_at,
634
+ updatedAt: c.updated_at
635
+ };
636
+ }
637
+ function toCollection(c) {
638
+ return {
639
+ id: str(c.id),
640
+ name: c.title,
641
+ title: c.title,
642
+ slug: c.slug,
643
+ description: c.description || null,
644
+ subTitle: null,
645
+ rank: str(c.position),
646
+ thumbnail: mediaUrl(c.metadata?.image),
647
+ isActive: true,
648
+ metaTitle: c.title,
649
+ metaDescription: c.description || null,
650
+ createdAt: c.created_at,
651
+ updatedAt: c.updated_at
652
+ };
653
+ }
654
+ function fromEngineAddress(a, email) {
655
+ if (!a) return null;
656
+ const [firstName, ...rest] = (a.name ?? "").trim().split(/\s+/);
657
+ return {
658
+ firstName: firstName || "",
659
+ lastName: rest.join(" "),
660
+ email: email ?? null,
661
+ phone: a.phone ?? null,
662
+ address_1: a.line1,
663
+ address_2: a.line2 ?? "",
664
+ city: a.city,
665
+ state: a.state ?? "",
666
+ zip: a.postal_code,
667
+ country: a.country,
668
+ countryCode: a.country
669
+ };
670
+ }
671
+ function toEngineAddress(a) {
672
+ const name = [a.firstName, a.lastName].filter(Boolean).join(" ").trim();
673
+ return {
674
+ ...name ? { name } : {},
675
+ ...a.phone ? { phone: String(a.phone) } : {},
676
+ line1: String(a.address_1 ?? "").trim(),
677
+ ...a.address_2 || a.locality ? { line2: [a.address_2, a.locality].filter(Boolean).join(", ") } : {},
678
+ city: String(a.city ?? "").trim(),
679
+ ...a.state ? { state: String(a.state) } : {},
680
+ postal_code: String(a.zip ?? "").trim(),
681
+ country: String(a.countryCode || a.country || "").trim().toUpperCase()
682
+ };
683
+ }
684
+ function fromIdentityAddress(a, email) {
685
+ const [firstName, ...rest] = (a.name ?? "").trim().split(/\s+/);
686
+ return {
687
+ id: str(a.id),
688
+ firstName: firstName || "",
689
+ lastName: rest.join(" "),
690
+ email: email ?? null,
691
+ phone: a.phone || null,
692
+ address_1: a.line1,
693
+ address_2: a.line2,
694
+ city: a.city,
695
+ state: a.state,
696
+ zip: a.postal_code,
697
+ country: a.country,
698
+ countryCode: a.country,
699
+ label: a.label,
700
+ isPrimary: a.is_default,
701
+ createdAt: a.created_at,
702
+ updatedAt: a.updated_at
703
+ };
704
+ }
705
+ function toIdentityAddress(a) {
706
+ const e = toEngineAddress(a);
707
+ return {
708
+ name: e.name ?? "",
709
+ phone: e.phone ?? "",
710
+ line1: e.line1,
711
+ line2: e.line2 ?? "",
712
+ city: e.city,
713
+ state: e.state ?? "",
714
+ postal_code: e.postal_code,
715
+ country: e.country,
716
+ ...a.label ? { label: String(a.label) } : {},
717
+ ...a.isPrimary ? { is_default: true } : {}
718
+ };
719
+ }
720
+ function toCartLine(l, extra = {}) {
721
+ return {
722
+ id: str(l.id),
723
+ productId: str(l.product_id),
724
+ variantId: str(l.variant_id),
725
+ sku: l.sku,
726
+ title: l.title,
727
+ name: l.title,
728
+ slug: extra.slug ?? "",
729
+ thumbnail: mediaUrl(extra.thumbnail),
730
+ variantTitle: l.variant_label ?? "",
731
+ qty: l.quantity,
732
+ price: toMajor(l.unit_price),
733
+ mrp: toMajor(l.unit_price),
734
+ subtotal: toMajor(l.total),
735
+ total: toMajor(l.total),
736
+ inStock: l.in_stock,
737
+ priceChanged: l.price_changed,
738
+ currentPrice: toMajor(l.current_price),
739
+ // The storefront's review step lists only lines marked for checkout. The
740
+ // engine checks a cart out whole, so every line is.
741
+ isSelectedForCheckout: true,
742
+ variant: {
743
+ id: str(l.variant_id),
744
+ options: l.variant_label ? l.variant_label.split(" / ").map((value) => ({ option: { title: "" }, value })) : []
745
+ },
746
+ product: { id: str(l.product_id), categories: [] }
747
+ };
748
+ }
749
+ function toCart(c, extras = {}, lineExtra = () => ({})) {
750
+ const lines = c.status === "converted" ? [] : arr(c.line_items);
751
+ const currency = c.currency;
752
+ const subtotalMinor = lines.reduce((n, l) => n + l.total.amount_minor, 0);
753
+ const discountMinor = extras.discount && !extras.discount.free_shipping ? extras.discount.amount_minor : 0;
754
+ const shippingMinor = extras.discount?.free_shipping ? 0 : extras.shippingRate?.minor ?? 0;
755
+ const totalMinor = Math.max(0, subtotalMinor - discountMinor) + shippingMinor;
756
+ const money = (minor) => minorToMajor(minor, currency);
757
+ const ship = extras.shippingAddress ?? null;
758
+ return {
759
+ id: c.id,
760
+ status: c.status,
761
+ email: c.email ?? ship?.email ?? null,
762
+ phone: extras.phone ?? ship?.phone ?? null,
763
+ name: ship ? [ship.firstName, ship.lastName].filter(Boolean).join(" ") : null,
764
+ lineItems: lines.map((l) => toCartLine(l, lineExtra(l))),
765
+ qty: lines.reduce((n, l) => n + l.quantity, 0),
766
+ currencyCode: currency,
767
+ currency,
768
+ subtotal: money(subtotalMinor),
769
+ discountAmount: money(discountMinor),
770
+ savingAmount: money(discountMinor),
771
+ couponCode: extras.discount?.code ?? null,
772
+ discount: extras.discount ?? null,
773
+ shippingCharges: money(shippingMinor),
774
+ shippingRateId: extras.shippingRateId ?? null,
775
+ shippingMethod: extras.shippingRate?.name ?? null,
776
+ tax: 0,
777
+ taxIncluded: false,
778
+ codCharges: 0,
779
+ total: money(totalMinor),
780
+ shippingAddress: ship,
781
+ shippingAddressId: ship ? ship.id && ship.id !== "new" ? String(ship.id) : "cart" : null,
782
+ billingAddress: extras.billingAddress ?? ship,
783
+ 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,
784
+ paymentMethod: null,
785
+ needAddress: lines.length > 0,
786
+ completedAt: c.status === "converted" ? c.updated_at : null,
787
+ createdAt: c.created_at,
788
+ updatedAt: c.updated_at,
789
+ expiresAt: c.expires_at
790
+ };
791
+ }
792
+ function emptyCart(currency = "USD") {
793
+ return {
794
+ id: "",
795
+ status: "open",
796
+ email: null,
797
+ phone: null,
798
+ lineItems: [],
799
+ qty: 0,
800
+ currencyCode: currency,
801
+ subtotal: 0,
802
+ discountAmount: 0,
803
+ savingAmount: 0,
804
+ couponCode: null,
805
+ shippingCharges: 0,
806
+ tax: 0,
807
+ total: 0,
808
+ shippingAddress: null,
809
+ shippingAddressId: null,
810
+ billingAddress: null,
811
+ billingAddressId: null
812
+ };
813
+ }
814
+ var STATUS = {
815
+ pending: "Pending",
816
+ confirmed: "Confirmed",
817
+ partial: "Partially shipped",
818
+ shipped: "Shipped",
819
+ delivered: "Delivered",
820
+ cancelled: "Cancelled"
821
+ };
822
+ var PAYMENT_STATUS = {
823
+ pending: "Pending",
824
+ paid: "Paid",
825
+ failed: "Failed",
826
+ refunded: "Refunded"
827
+ };
828
+ function toOrderLine(l, currency) {
829
+ return {
830
+ id: str(l.id),
831
+ productId: str(l.product_id),
832
+ variantId: str(l.variant_id),
833
+ sku: l.sku,
834
+ title: l.title,
835
+ name: l.title,
836
+ slug: "",
837
+ thumbnail: mediaUrl(l.image_url),
838
+ img: mediaUrl(l.image_url),
839
+ variantTitle: l.variant_label ?? "",
840
+ qty: l.quantity,
841
+ shippedQty: l.shipped_quantity,
842
+ price: toMajor(l.unit_price),
843
+ mrp: toMajor(l.unit_price),
844
+ subtotal: toMajor(l.total),
845
+ total: toMajor(l.total),
846
+ currencyCode: currency,
847
+ status: l.shipped_quantity >= l.quantity ? "Shipped" : "Pending",
848
+ files: [],
849
+ usedOptions: []
850
+ };
851
+ }
852
+ function toOrder(o) {
853
+ const address = fromEngineAddress(o.address, o.email);
854
+ const code = (o.discounts ?? []).find((d) => d.code)?.code;
855
+ return {
856
+ id: str(o.id),
857
+ orderNo: o.number,
858
+ orderNumber: o.number,
859
+ number: o.number,
860
+ // The storefront links an order by this; the engine has no sub-orders,
861
+ // so an order is its own parent.
862
+ parentOrderNo: o.number,
863
+ status: STATUS[o.status] ?? o.status,
864
+ engineStatus: o.status,
865
+ paymentStatus: PAYMENT_STATUS[o.payment_status] ?? o.payment_status,
866
+ paymentMethod: o.payment_provider.toUpperCase(),
867
+ currencyCode: o.currency,
868
+ subtotal: toMajor(o.subtotal),
869
+ shippingCharges: toMajor(o.shipping),
870
+ discount: toMajor(o.discount),
871
+ tax: toMajor(o.tax),
872
+ taxIncluded: o.tax_inclusive,
873
+ total: toMajor(o.total),
874
+ refunded: toMajor(o.refunded),
875
+ codCharges: 0,
876
+ coupon: code ? { code } : null,
877
+ userEmail: o.email,
878
+ email: o.email,
879
+ phone: o.phone ?? null,
880
+ shippingAddress: address,
881
+ billingAddress: address,
882
+ shippingMethod: o.shipping_method ?? null,
883
+ shippingRate: o.shipping_method ? { name: o.shipping_method } : null,
884
+ lineItems: arr(o.line_items).map((l) => toOrderLine(l, o.currency)),
885
+ fulfillments: arr(o.fulfillments).map((f) => ({
886
+ id: str(f.id),
887
+ status: f.status,
888
+ carrier: f.carrier_name || f.carrier || f.provider,
889
+ trackingNumber: f.tracking ?? null,
890
+ trackingUrl: f.tracking_url ?? null,
891
+ lineItems: arr(f.lines).map((l) => ({ id: str(l.order_line_id), title: l.title, qty: l.quantity })),
892
+ createdAt: f.created_at
893
+ })),
894
+ tracking: [],
895
+ orderHistory: [],
896
+ isReplaceOrReturn: false,
897
+ createdAt: o.created_at,
898
+ updatedAt: o.updated_at
899
+ };
900
+ }
901
+ function toRating(r) {
939
902
  return {
940
- data: rows.map(map),
941
- count: meta?.total ?? rows.length,
942
- pageSize: meta?.limit ?? rows.length,
943
- noOfPage: meta?.total_pages ?? 1,
944
- page: meta?.page ?? 1
903
+ id: str(r.id),
904
+ rating: r.rating,
905
+ name: r.name,
906
+ title: r.title ?? "",
907
+ review: r.body,
908
+ message: r.body,
909
+ reply: r.reply ?? null,
910
+ verified: r.verified,
911
+ img: "",
912
+ createdAt: r.created_at
945
913
  };
946
914
  }
947
- var emptyPage = () => ({ data: [], count: 0, pageSize: 0, noOfPage: 0, page: 1 });
915
+ function toCmsPage(p) {
916
+ return {
917
+ id: str(p.id),
918
+ slug: p.slug,
919
+ name: p.title,
920
+ title: p.title,
921
+ content: p.body,
922
+ excerpt: p.excerpt ?? null,
923
+ layouts: [],
924
+ language: p.language,
925
+ metaTitle: p.seo?.title || p.title,
926
+ metaDescription: p.seo?.description || p.excerpt || null,
927
+ metaKeywords: p.seo?.keywords || null,
928
+ publishedAt: p.published_at ?? null,
929
+ createdAt: p.created_at,
930
+ updatedAt: p.updated_at
931
+ };
932
+ }
933
+ function toVendor(v) {
934
+ return {
935
+ id: str(v.id),
936
+ slug: v.slug,
937
+ name: v.name,
938
+ businessName: v.name,
939
+ about: v.about,
940
+ description: v.about,
941
+ website: v.website,
942
+ address: v.address,
943
+ banners: [],
944
+ rating: null
945
+ };
946
+ }
947
+ function toUser(c) {
948
+ const [firstName, ...rest] = (c.name ?? "").trim().split(/\s+/);
949
+ return {
950
+ id: str(c.id),
951
+ userId: str(c.id),
952
+ email: c.email,
953
+ emailVerified: c.email_verified,
954
+ phone: c.phone || null,
955
+ firstName: firstName || "",
956
+ lastName: rest.join(" "),
957
+ name: c.name,
958
+ fullName: c.name,
959
+ avatar: null,
960
+ role: "USER",
961
+ createdAt: c.created_at,
962
+ updatedAt: c.updated_at
963
+ };
964
+ }
965
+ var priceOf = (m) => m ? toMajor(m) : 0;
948
966
 
949
967
  // src/services/facts.ts
950
968
  var StoreFacts = class extends BaseService {
@@ -1099,8 +1117,11 @@ var CatalogService = class extends BaseService {
1099
1117
  sorted
1100
1118
  };
1101
1119
  }
1120
+ const attr = Object.entries(q.attributes ?? {}).flatMap(
1121
+ ([handle, values]) => values.filter(Boolean).map((v) => `${handle}:${v}`)
1122
+ );
1102
1123
  const env = await this.engine("/api/products", {
1103
- query: { q: search, page, limit, channel: this.creds.channel }
1124
+ query: { q: search, page, limit, channel: this.creds.channel, attr }
1104
1125
  });
1105
1126
  return { ...toPage(env, (p) => toProduct(p)), categoryHierarchy: hierarchy, sorted };
1106
1127
  }
@@ -1235,6 +1256,16 @@ var num = (v) => {
1235
1256
  const n = Number(v);
1236
1257
  return Number.isFinite(n) ? n : void 0;
1237
1258
  };
1259
+ function attributesFrom(p) {
1260
+ const out = {};
1261
+ for (const [key, value] of p) {
1262
+ if (!key.startsWith("attributes.")) continue;
1263
+ const handle = key.slice("attributes.".length);
1264
+ const values = value.split(",").map((v) => decodeURIComponent(v).trim()).filter(Boolean);
1265
+ if (handle && values.length) out[handle] = [...out[handle] ?? [], ...values];
1266
+ }
1267
+ return Object.keys(out).length ? out : void 0;
1268
+ }
1238
1269
  var SearchService = class extends CatalogService {
1239
1270
  emptyResult() {
1240
1271
  return {
@@ -1256,7 +1287,8 @@ var SearchService = class extends CatalogService {
1256
1287
  sort: p.get("sort") ?? void 0,
1257
1288
  category: slug || p.get("categories")?.split(",")[0] || void 0,
1258
1289
  priceFrom: num(p.get("priceFrom")) ?? num(from ?? null),
1259
- priceTo: num(p.get("priceTo")) ?? num(to ?? null)
1290
+ priceTo: num(p.get("priceTo")) ?? num(to ?? null),
1291
+ attributes: attributesFrom(p)
1260
1292
  });
1261
1293
  return {
1262
1294
  data: res.data,
@@ -1388,10 +1420,10 @@ var REMOVE = -9999999;
1388
1420
  var CartService = class extends BaseService {
1389
1421
  facts = new StoreFacts(this._fetch);
1390
1422
  async getCartByCartId(cartId) {
1391
- const id = clean(cartId);
1392
- if (!id) return emptyCart(await this.currency());
1423
+ const id2 = clean(cartId);
1424
+ if (!id2) return emptyCart(await this.currency());
1393
1425
  try {
1394
- return await this.present(await this.data(`/api/carts/${encodeURIComponent(id)}`));
1426
+ return await this.present(await this.data(`/api/carts/${encodeURIComponent(id2)}`));
1395
1427
  } catch (err) {
1396
1428
  if (err instanceof GoCommerceError && err.status === 404) return emptyCart(await this.currency());
1397
1429
  throw err;
@@ -1406,9 +1438,9 @@ var CartService = class extends BaseService {
1406
1438
  * to clear the bag on the success page.
1407
1439
  */
1408
1440
  async refereshCart() {
1409
- const id = storedCartId();
1410
- const cart = await this.getCartByCartId(id);
1411
- if (id && cart.status === "converted") forgetCartExtras(id);
1441
+ const id2 = storedCartId();
1442
+ const cart = await this.getCartByCartId(id2);
1443
+ if (id2 && cart.status === "converted") forgetCartExtras(id2);
1412
1444
  return cart;
1413
1445
  }
1414
1446
  /**
@@ -1421,21 +1453,21 @@ var CartService = class extends BaseService {
1421
1453
  qty = 1,
1422
1454
  variantId
1423
1455
  }) {
1424
- let id = clean(cartId) ?? storedCartId();
1425
- if (lineId && id) {
1426
- const cart = await this.data(`/api/carts/${encodeURIComponent(id)}`);
1456
+ let id2 = clean(cartId) ?? storedCartId();
1457
+ if (lineId && id2) {
1458
+ const cart = await this.data(`/api/carts/${encodeURIComponent(id2)}`);
1427
1459
  const line = (cart.line_items ?? []).find((l) => String(l.id) === String(lineId));
1428
1460
  if (line) {
1429
1461
  const next = qty === REMOVE ? 0 : line.quantity + Number(qty);
1430
- return this.setQuantity(id, line.id, next);
1462
+ return this.setQuantity(id2, line.id, next);
1431
1463
  }
1432
1464
  }
1433
- if (qty === REMOVE || Number(qty) <= 0) return this.getCartByCartId(id);
1465
+ if (qty === REMOVE || Number(qty) <= 0) return this.getCartByCartId(id2);
1434
1466
  if (!variantId) throw new GoCommerceError("Please choose an option before adding this to your bag.", 400);
1435
- if (!id) id = await this.open();
1467
+ if (!id2) id2 = await this.open();
1436
1468
  try {
1437
1469
  return await this.present(
1438
- await this.data(`/api/carts/${encodeURIComponent(id)}/line-items`, {
1470
+ await this.data(`/api/carts/${encodeURIComponent(id2)}/line-items`, {
1439
1471
  method: "POST",
1440
1472
  body: { variant_id: Number(variantId), quantity: Number(qty) }
1441
1473
  })
@@ -1459,15 +1491,15 @@ var CartService = class extends BaseService {
1459
1491
  lineId,
1460
1492
  qty
1461
1493
  }) {
1462
- const id = clean(cartId) ?? storedCartId();
1463
- if (!id) return emptyCart(await this.currency());
1464
- return this.setQuantity(id, lineId, Number(qty));
1494
+ const id2 = clean(cartId) ?? storedCartId();
1495
+ if (!id2) return emptyCart(await this.currency());
1496
+ return this.setQuantity(id2, lineId, Number(qty));
1465
1497
  }
1466
1498
  async removeCart({ cartId, lineId }) {
1467
- const id = clean(cartId) ?? storedCartId();
1468
- if (!id) return emptyCart(await this.currency());
1499
+ const id2 = clean(cartId) ?? storedCartId();
1500
+ if (!id2) return emptyCart(await this.currency());
1469
1501
  return this.present(
1470
- await this.data(`/api/carts/${encodeURIComponent(id)}/line-items/${encodeURIComponent(String(lineId))}`, {
1502
+ await this.data(`/api/carts/${encodeURIComponent(id2)}/line-items/${encodeURIComponent(String(lineId))}`, {
1471
1503
  method: "DELETE"
1472
1504
  })
1473
1505
  );
@@ -1478,8 +1510,8 @@ var CartService = class extends BaseService {
1478
1510
  * and the rest is kept for the checkout request.
1479
1511
  */
1480
1512
  async updateCart2(args) {
1481
- const id = clean(args.cartId) ?? storedCartId();
1482
- if (!id) return emptyCart(await this.currency());
1513
+ const id2 = clean(args.cartId) ?? storedCartId();
1514
+ if (!id2) return emptyCart(await this.currency());
1483
1515
  const patch = {};
1484
1516
  if (args.phone !== void 0) patch.phone = args.phone || void 0;
1485
1517
  if (args.shippingAddress !== void 0) {
@@ -1489,20 +1521,20 @@ var CartService = class extends BaseService {
1489
1521
  if (args.billingAddress !== void 0 || args.isBillingAddressSameAsShipping) {
1490
1522
  patch.billingAddress = args.isBillingAddressSameAsShipping ? null : args.billingAddress ?? null;
1491
1523
  }
1492
- const before = cartExtras(id);
1524
+ const before = cartExtras(id2);
1493
1525
  const country = (a) => String(a?.countryCode || a?.country || "").toUpperCase();
1494
1526
  if (args.shippingAddress !== void 0 && country(args.shippingAddress) !== country(before.shippingAddress)) {
1495
1527
  patch.shippingRateId = null;
1496
1528
  patch.shippingRate = null;
1497
1529
  }
1498
- saveCartExtras(id, patch);
1530
+ saveCartExtras(id2, patch);
1499
1531
  const email = args.email ?? (args.shippingAddress?.email ? String(args.shippingAddress.email) : void 0);
1500
1532
  if (email !== void 0) {
1501
1533
  return this.present(
1502
- await this.data(`/api/carts/${encodeURIComponent(id)}/email`, { method: "PUT", body: { email } })
1534
+ await this.data(`/api/carts/${encodeURIComponent(id2)}/email`, { method: "PUT", body: { email } })
1503
1535
  );
1504
1536
  }
1505
- return this.getCartByCartId(id);
1537
+ return this.getCartByCartId(id2);
1506
1538
  }
1507
1539
  /**
1508
1540
  * A discount code. The engine prices it against the basket now and decides
@@ -1510,34 +1542,34 @@ var CartService = class extends BaseService {
1510
1542
  * because the cart itself does not carry the code.
1511
1543
  */
1512
1544
  async applyCoupon({ cartId, couponCode }) {
1513
- const id = clean(cartId) ?? storedCartId();
1514
- if (!id) throw new GoCommerceError("Add something to your bag before applying a code.", 400);
1515
- const cart = await this.data(`/api/carts/${encodeURIComponent(id)}`);
1516
- const discount = await this.data(`/api/carts/${encodeURIComponent(id)}/discount`, {
1545
+ const id2 = clean(cartId) ?? storedCartId();
1546
+ if (!id2) throw new GoCommerceError("Add something to your bag before applying a code.", 400);
1547
+ const cart = await this.data(`/api/carts/${encodeURIComponent(id2)}`);
1548
+ const discount = await this.data(`/api/carts/${encodeURIComponent(id2)}/discount`, {
1517
1549
  method: "PUT",
1518
1550
  body: { code: String(couponCode ?? "").trim(), ...cart.email ? { email: cart.email } : {} }
1519
1551
  });
1520
- saveCartExtras(id, { discount: { ...discount, code: discount.code || String(couponCode).trim() } });
1552
+ saveCartExtras(id2, { discount: { ...discount, code: discount.code || String(couponCode).trim() } });
1521
1553
  return this.present(cart);
1522
1554
  }
1523
1555
  async removeCoupon(cartId) {
1524
- const id = clean(cartId) ?? storedCartId();
1525
- if (!id) return emptyCart(await this.currency());
1526
- await this.engine(`/api/carts/${encodeURIComponent(id)}/discount`, { method: "DELETE" });
1527
- saveCartExtras(id, { discount: null });
1528
- return this.getCartByCartId(id);
1556
+ const id2 = clean(cartId) ?? storedCartId();
1557
+ if (!id2) return emptyCart(await this.currency());
1558
+ await this.engine(`/api/carts/${encodeURIComponent(id2)}/discount`, { method: "DELETE" });
1559
+ saveCartExtras(id2, { discount: null });
1560
+ return this.getCartByCartId(id2);
1529
1561
  }
1530
1562
  /** The delivery option the shopper picked, priced from the engine's own quote. */
1531
1563
  async updateShippingRate({ cartId, shippingRateId }) {
1532
- const id = clean(cartId) ?? storedCartId();
1533
- if (!id) return emptyCart(await this.currency());
1534
- const ship = cartExtras(id).shippingAddress;
1564
+ const id2 = clean(cartId) ?? storedCartId();
1565
+ if (!id2) return emptyCart(await this.currency());
1566
+ const ship = cartExtras(id2).shippingAddress;
1535
1567
  const country = String(ship?.countryCode || ship?.country || "").toUpperCase();
1536
1568
  if (!country) throw new GoCommerceError("Add a delivery address first.", 400);
1537
- const quote2 = await this.data("/api/checkout/rates", { query: { cart: id, country, state: ship?.state ? String(ship.state) : void 0 } });
1569
+ const quote2 = await this.data("/api/checkout/rates", { query: { cart: id2, country, state: ship?.state ? String(ship.state) : void 0 } });
1538
1570
  const rate = (quote2.rates ?? []).find((r) => String(r.rate_id) === String(shippingRateId));
1539
1571
  if (!rate) throw new GoCommerceError("That delivery option is not available for this address any more.", 409);
1540
- saveCartExtras(id, {
1572
+ saveCartExtras(id2, {
1541
1573
  shippingRateId: String(rate.rate_id),
1542
1574
  shippingRate: {
1543
1575
  id: String(rate.rate_id),
@@ -1546,7 +1578,7 @@ var CartService = class extends BaseService {
1546
1578
  currency: rate.price.currency
1547
1579
  }
1548
1580
  });
1549
- return this.getCartByCartId(id);
1581
+ return this.getCartByCartId(id2);
1550
1582
  }
1551
1583
  /** The payment method is chosen at checkout, by which checkout route is called. */
1552
1584
  async updateCartPaymentMethod({ cartId }) {
@@ -1592,25 +1624,25 @@ var CartService = class extends BaseService {
1592
1624
  return { slug: p.slug, thumbnail: v?.image?.url ?? p.image_url ?? null };
1593
1625
  });
1594
1626
  }
1595
- productFor(id) {
1627
+ productFor(id2) {
1596
1628
  return remember(
1597
- `${this.engineBase()}|product|${id}`,
1598
- () => this.data(`/api/products/${id}`).catch(() => null)
1629
+ `${this.engineBase()}|product|${id2}`,
1630
+ () => this.data(`/api/products/${id2}`).catch(() => null)
1599
1631
  );
1600
1632
  }
1601
1633
  };
1602
- var clean = (id) => id && id !== "undefined" && id !== "null" ? id : null;
1634
+ var clean = (id2) => id2 && id2 !== "undefined" && id2 !== "null" ? id2 : null;
1603
1635
  var cartService = new CartService();
1604
1636
 
1605
1637
  // src/services/checkout-service.ts
1606
1638
  var CheckoutService = class extends BaseService {
1607
1639
  /** Delivery options for the address on the cart: `{ data: [{ id, name, base_rate }] }`. */
1608
1640
  async getShippingRates({ cartId } = {}) {
1609
- const id = cartId || storedCartId();
1610
- const ship = id ? cartExtras(id).shippingAddress : null;
1641
+ const id2 = cartId || storedCartId();
1642
+ const ship = id2 ? cartExtras(id2).shippingAddress : null;
1611
1643
  const country = String(ship?.countryCode || ship?.country || "").toUpperCase();
1612
- if (!id || !country) return { data: [] };
1613
- const quote2 = await this.data("/api/checkout/rates", { query: { cart: id, country, state: ship?.state ? String(ship.state) : void 0 } });
1644
+ if (!id2 || !country) return { data: [] };
1645
+ const quote2 = await this.data("/api/checkout/rates", { query: { cart: id2, country, state: ship?.state ? String(ship.state) : void 0 } });
1614
1646
  return {
1615
1647
  data: (quote2.rates ?? []).map((r) => ({
1616
1648
  id: String(r.rate_id),
@@ -1664,10 +1696,11 @@ var CheckoutService = class extends BaseService {
1664
1696
  if (!order) throw new GoCommerceError("That payment does not belong to an order placed from this browser.", 404);
1665
1697
  return { order_no: order.number, status: "processing" };
1666
1698
  }
1667
- // The family's other gateways. GoCommerce's Stripe is a Payment Element
1668
- // (a client secret, not a hosted page), and PayPal, Cashfree, PhonePe and
1669
- // Affirm are not engine gateways at all; paymentMethodService never lists
1670
- // them, so these are reached only by a storefront that hard-codes one.
1699
+ // The family's other gateway flows. GoCommerce's Stripe is a Payment
1700
+ // Element (a client secret, not a hosted page) — `checkout('stripe')` returns
1701
+ // that secret for a storefront that mounts the element. Cashfree, PhonePe and
1702
+ // Affirm are not engine gateways at all. paymentMethodService never offers
1703
+ // these codes, so they are reached only by a storefront that hard-codes one.
1671
1704
  async checkoutStripe() {
1672
1705
  throw unsupported(
1673
1706
  "checkoutStripe",
@@ -1677,11 +1710,34 @@ var CheckoutService = class extends BaseService {
1677
1710
  async checkoutStripeCapture() {
1678
1711
  throw unsupported("checkoutStripeCapture", "Stripe confirms payments to the engine by webhook");
1679
1712
  }
1680
- async checkoutPaypal() {
1681
- throw unsupported("checkoutPaypal", "the engine has no PayPal gateway");
1713
+ /**
1714
+ * The storefront's generic redirect flow, which it reaches through the code
1715
+ * `PAYPAL`. The engine has no PayPal gateway; paymentMethodService puts the
1716
+ * store's first hosted-page gateway — Adyen, Paddle, Lemon Squeezy, Creem,
1717
+ * Hyperswitch or RevenueCat — under that code, with its own name and a
1718
+ * neutral icon, because this is the one storefront flow that sends a shopper
1719
+ * to a URL and back. The shopper returns to the success page; the gateway's
1720
+ * webhook tells the engine the order is paid.
1721
+ */
1722
+ async checkoutPaypal({ cartId, origin } = {}) {
1723
+ const gateway = await this.redirectGateway();
1724
+ if (!gateway) throw unsupported("checkoutPaypal", "this store has no hosted-page payment gateway switched on");
1725
+ const id2 = cartId || storedCartId() || "";
1726
+ const back = origin ? `${origin.replace(/\/+$/, "")}/checkout/success?cart_id=${encodeURIComponent(id2)}` : void 0;
1727
+ const result = await this.place(gateway, id2, back ? { return_url: back } : {});
1728
+ const url = result.payment.client_data?.url;
1729
+ if (!url) throw new GoCommerceError("The payment page did not open. Please try again.", 502);
1730
+ return { redirect_url: url, order_no: result.order.number };
1682
1731
  }
1683
- async capturePaypalPayment() {
1684
- throw unsupported("capturePaypalPayment", "the engine has no PayPal gateway");
1732
+ /**
1733
+ * Nothing to capture: a hosted-page gateway confirms the payment to the
1734
+ * engine by webhook. Answering, rather than throwing, sends the storefront's
1735
+ * process page on to the success page instead of the failure page.
1736
+ */
1737
+ async capturePaypalPayment({
1738
+ order_no
1739
+ } = {}) {
1740
+ return { order_no: order_no ?? null, status: "processing" };
1685
1741
  }
1686
1742
  async checkoutCashfree() {
1687
1743
  throw unsupported("checkoutCashfree", "the engine has no Cashfree gateway");
@@ -1704,6 +1760,57 @@ var CheckoutService = class extends BaseService {
1704
1760
  async checkoutPOS() {
1705
1761
  throw unsupported("checkoutPOS", "point-of-sale orders are placed from the admin panel");
1706
1762
  }
1763
+ /**
1764
+ * Any payment method the engine has switched on, by its engine code —
1765
+ * `cod`, `stripe`, `razorpay`, `adyen`, `paddle`, `helcim`… For a storefront
1766
+ * that drives the payment itself: the answer says what to do next.
1767
+ *
1768
+ * - `kind: 'none'` — nothing to pay now (cash on delivery).
1769
+ * - `kind: 'client_action'` — mount the gateway's own widget with
1770
+ * `clientData`: Stripe's Payment Element takes `clientSecret`, Razorpay's
1771
+ * modal `razorpay_order_id` and `key_id`, Helcim's `checkout_token`.
1772
+ * - `kind: 'redirect'` — send the shopper to `redirectUrl`; `returnUrl` is
1773
+ * where the gateway brings them back.
1774
+ *
1775
+ * Either way the gateway's webhook tells the engine when the money arrives;
1776
+ * read `orderService.getOrder(orderNo).paymentStatus` to see it.
1777
+ */
1778
+ async checkout(code, {
1779
+ cartId,
1780
+ returnUrl,
1781
+ paymentData,
1782
+ metadata
1783
+ } = {}) {
1784
+ const result = await this.place(String(code).toLowerCase(), cartId, {
1785
+ ...returnUrl ? { return_url: returnUrl } : {},
1786
+ ...paymentData ? { payment_data: paymentData } : {},
1787
+ ...metadata ? { metadata } : {}
1788
+ });
1789
+ const data = result.payment.client_data ?? {};
1790
+ return {
1791
+ orderNo: result.order.number,
1792
+ order: toOrder(result.order),
1793
+ payment: {
1794
+ kind: result.payment.kind,
1795
+ provider: result.payment.provider,
1796
+ reference: result.payment.reference ?? null,
1797
+ clientSecret: data.client_secret ?? null,
1798
+ redirectUrl: data.url ?? null,
1799
+ clientData: data
1800
+ }
1801
+ };
1802
+ }
1803
+ /** Every payment method the engine has switched on, by engine code and name — drivable here or not. */
1804
+ async listMethods() {
1805
+ const options = await this.data(
1806
+ "/api/checkout"
1807
+ );
1808
+ return { methods: options.methods ?? [], currency: options.currency };
1809
+ }
1810
+ async redirectGateway() {
1811
+ const { methods } = await this.listMethods();
1812
+ return methods.find((m) => REDIRECT_GATEWAYS.has(m.code))?.code;
1813
+ }
1707
1814
  /**
1708
1815
  * One checkout attempt.
1709
1816
  *
@@ -1714,27 +1821,28 @@ var CheckoutService = class extends BaseService {
1714
1821
  * request — a different address, a different method — gets a new key, since
1715
1822
  * the engine refuses a key replayed with a different body.
1716
1823
  */
1717
- async place(code, cartId) {
1718
- const id = cartId || storedCartId();
1719
- if (!id) throw new GoCommerceError("Your bag is empty.", 400);
1720
- const extras = cartExtras(id);
1824
+ async place(code, cartId, extra = {}) {
1825
+ const id2 = cartId || storedCartId();
1826
+ if (!id2) throw new GoCommerceError("Your bag is empty.", 400);
1827
+ const extras = cartExtras(id2);
1721
1828
  const ship = extras.shippingAddress;
1722
1829
  if (!ship) throw new GoCommerceError("Add a delivery address first.", 400);
1723
- const cart = await this.data(`/api/carts/${encodeURIComponent(id)}`);
1830
+ const cart = await this.data(`/api/carts/${encodeURIComponent(id2)}`);
1724
1831
  const email = cart.email || (ship.email ? String(ship.email) : "");
1725
1832
  if (!email) throw new GoCommerceError("Add an email address so we can send your receipt.", 400);
1726
1833
  const address = toEngineAddress(ship);
1727
1834
  const body = {
1728
- cart_id: id,
1835
+ cart_id: id2,
1729
1836
  email,
1730
1837
  ...extras.phone || address.phone ? { phone: String(extras.phone || address.phone) } : {},
1731
1838
  ...address.name ? { name: address.name } : {},
1732
1839
  address,
1733
- ...extras.shippingRateId ? { shipping_rate_id: Number(extras.shippingRateId) } : {}
1840
+ ...extras.shippingRateId ? { shipping_rate_id: Number(extras.shippingRateId) } : {},
1841
+ ...extra
1734
1842
  };
1735
1843
  const serialised = JSON.stringify(body);
1736
1844
  const key = extras.attempt?.body === `${code}:${serialised}` ? extras.attempt.key : newKey();
1737
- saveCartExtras(id, { attempt: { key, body: `${code}:${serialised}` } });
1845
+ saveCartExtras(id2, { attempt: { key, body: `${code}:${serialised}` } });
1738
1846
  const result = await this.data(`/api/checkout/${encodeURIComponent(code)}`, {
1739
1847
  method: "POST",
1740
1848
  body,
@@ -1744,7 +1852,7 @@ var CheckoutService = class extends BaseService {
1744
1852
  rememberOrder({
1745
1853
  number: result.order.number,
1746
1854
  token: result.order.access_token,
1747
- cartId: id,
1855
+ cartId: id2,
1748
1856
  reference: result.payment.client_data?.razorpay_order_id ?? result.payment.reference
1749
1857
  });
1750
1858
  if (sessionToken()) {
@@ -1755,10 +1863,11 @@ var CheckoutService = class extends BaseService {
1755
1863
  }).catch(() => void 0);
1756
1864
  }
1757
1865
  }
1758
- saveCartExtras(id, { attempt: null });
1866
+ saveCartExtras(id2, { attempt: null });
1759
1867
  return result;
1760
1868
  }
1761
1869
  };
1870
+ var REDIRECT_GATEWAYS = /* @__PURE__ */ new Set(["adyen", "paddle", "lemonsqueezy", "creem", "hyperswitch", "revenuecat"]);
1762
1871
  var unsupported = (method, why) => new NotSupportedError("CheckoutService", method, why);
1763
1872
  function newKey() {
1764
1873
  const c = globalThis.crypto;
@@ -1778,23 +1887,25 @@ var PaymentMethodService = class extends BaseService {
1778
1887
  async list(_args = {}) {
1779
1888
  const options = await this.facts.checkoutOptions();
1780
1889
  const methods = options.methods ?? [];
1890
+ const redirect = methods.find((m) => REDIRECT_GATEWAYS.has(m.code))?.code;
1891
+ const drive = (code) => DRIVABLE[code] ?? (code === redirect ? { code: "PAYPAL", img: "/payment/card.svg" } : void 0);
1781
1892
  const data = methods.filter((m) => {
1782
- if (DRIVABLE[m.code]) return true;
1893
+ if (drive(m.code)) return true;
1783
1894
  if (!reported2.has(m.code)) {
1784
1895
  reported2.add(m.code);
1785
1896
  console.warn(
1786
- `[gocommerce] payment method "${m.code}" is switched on but this storefront cannot drive its flow, so it is not offered.`
1897
+ `[gocommerce] payment method "${m.code}" is switched on but this storefront cannot drive its flow, so it is not offered here; checkoutService.checkout('${m.code}') still reaches it.`
1787
1898
  );
1788
1899
  }
1789
1900
  return false;
1790
1901
  }).map((m, i) => ({
1791
1902
  id: m.code,
1792
- code: DRIVABLE[m.code].code,
1903
+ code: drive(m.code).code,
1793
1904
  engineCode: m.code,
1794
1905
  name: m.name,
1795
1906
  title: m.name,
1796
1907
  description: "",
1797
- img: DRIVABLE[m.code].img ?? null,
1908
+ img: drive(m.code).img ?? null,
1798
1909
  rank: i,
1799
1910
  active: true,
1800
1911
  isActive: true
@@ -1869,7 +1980,7 @@ var OrderService = class extends BaseService {
1869
1980
  productId: l.product_id,
1870
1981
  variantId: l.variant_id,
1871
1982
  title: l.title,
1872
- img: l.image_url ?? null,
1983
+ img: mediaUrl(l.image_url),
1873
1984
  price: toMajor(l.unit_price),
1874
1985
  qty: l.quantity
1875
1986
  });
@@ -1965,6 +2076,42 @@ var UserService = class extends BaseService {
1965
2076
  startSession(auth);
1966
2077
  return toUser(auth.record);
1967
2078
  }
2079
+ /**
2080
+ * Mails the signed-in shopper a link to confirm their address. The engine
2081
+ * sends none at signup, and a confirmed address is what customer-group
2082
+ * prices are keyed on. 409 once confirmed; 429 within a minute of the last.
2083
+ */
2084
+ async requestEmailVerification() {
2085
+ return this.account("/x/identity/me/email-verification", { method: "POST", auth: true });
2086
+ }
2087
+ /** Extends the session another thirty days. The token stays the same. */
2088
+ async refreshSession() {
2089
+ return startSession(await this.account("/x/identity/refresh", { method: "POST", auth: true }));
2090
+ }
2091
+ /**
2092
+ * Files a guest order under the signed-in account, so it appears in their
2093
+ * order history. The order's access token is the proof of ownership: pass
2094
+ * it, or leave it out for an order placed from this browser, whose token was
2095
+ * kept at checkout.
2096
+ *
2097
+ * Never done automatically at sign-in. On a shared computer that would file
2098
+ * one person's orders — address and all — under whoever signed in next.
2099
+ */
2100
+ async claimOrder({ number, token }) {
2101
+ const proof = token ?? placedOrder(number)?.token;
2102
+ if (!proof) {
2103
+ throw new GoCommerceError(
2104
+ "This order was not placed from this browser; enter the code from its confirmation.",
2105
+ 400
2106
+ );
2107
+ }
2108
+ const order = await this.account("/x/identity/me/orders", {
2109
+ method: "POST",
2110
+ auth: true,
2111
+ body: { number: String(number), token: proof }
2112
+ });
2113
+ return toOrder(order);
2114
+ }
1968
2115
  async deleteUser() {
1969
2116
  throw new NotSupportedError("UserService", "deleteUser", "an account is closed by the store, on request");
1970
2117
  }
@@ -2062,16 +2209,16 @@ var AddressService = class extends BaseService {
2062
2209
  page: env.meta?.page ?? page
2063
2210
  };
2064
2211
  }
2065
- async fetchAddress(id) {
2212
+ async fetchAddress(id2) {
2066
2213
  return fromIdentityAddress(
2067
- await this.data(`/x/identity/me/addresses/${enc(id)}`, { auth: true }),
2214
+ await this.data(`/x/identity/me/addresses/${enc(id2)}`, { auth: true }),
2068
2215
  currentMe()?.email
2069
2216
  );
2070
2217
  }
2071
2218
  /** New when it has no id (or the form's `'new'`), an edit when it has one — the storefront saves both here. */
2072
2219
  async saveAddress(address) {
2073
- const id = address?.id && address.id !== "new" ? String(address.id) : null;
2074
- if (id) return this.editAddress(id, address);
2220
+ const id2 = address?.id && address.id !== "new" ? String(address.id) : null;
2221
+ if (id2) return this.editAddress(id2, address);
2075
2222
  const a = await this.data("/x/identity/me/addresses", {
2076
2223
  method: "POST",
2077
2224
  auth: true,
@@ -2079,20 +2226,20 @@ var AddressService = class extends BaseService {
2079
2226
  });
2080
2227
  return fromIdentityAddress(a, currentMe()?.email);
2081
2228
  }
2082
- async editAddress(id, address) {
2083
- const a = await this.data(`/x/identity/me/addresses/${enc(id)}`, {
2229
+ async editAddress(id2, address) {
2230
+ const a = await this.data(`/x/identity/me/addresses/${enc(id2)}`, {
2084
2231
  method: "PATCH",
2085
2232
  auth: true,
2086
2233
  body: toIdentityAddress(address)
2087
2234
  });
2088
2235
  return fromIdentityAddress(a, currentMe()?.email);
2089
2236
  }
2090
- async deleteAddress(id) {
2091
- await this.engine(`/x/identity/me/addresses/${enc(id)}`, { method: "DELETE", auth: true });
2092
- return { id: String(id), deleted: true };
2237
+ async deleteAddress(id2) {
2238
+ await this.engine(`/x/identity/me/addresses/${enc(id2)}`, { method: "DELETE", auth: true });
2239
+ return { id: String(id2), deleted: true };
2093
2240
  }
2094
2241
  };
2095
- var enc = (id) => encodeURIComponent(String(id));
2242
+ var enc = (id2) => encodeURIComponent(String(id2));
2096
2243
  var addressService = new AddressService();
2097
2244
 
2098
2245
  // src/services/wishlist-service.ts
@@ -2144,6 +2291,18 @@ var WishlistService = class extends BaseService {
2144
2291
  const saved = new Set((list?.items ?? []).map((i) => String(i.product_id)));
2145
2292
  return (pairs ?? []).map((p) => ({ ...p, exists: saved.has(String(p.productId)) }));
2146
2293
  }
2294
+ /**
2295
+ * The address the store may write to when a saved product is back in stock
2296
+ * or on sale; `''` takes it off. A list that does not exist yet is made.
2297
+ */
2298
+ async setEmail(email) {
2299
+ const list = await this.current() ?? await this.create();
2300
+ await this.data(`/x/wishlist/${enc2(list.token)}`, {
2301
+ method: "PATCH",
2302
+ body: { email: String(email ?? "").trim() }
2303
+ });
2304
+ return { email: String(email ?? "").trim() || null };
2305
+ }
2147
2306
  async current() {
2148
2307
  const token = wishlistToken();
2149
2308
  if (!token) return null;
@@ -2226,6 +2385,15 @@ var MenuService = class extends BaseService {
2226
2385
  if (menus) return { data: menus };
2227
2386
  return resolveRestLocally("get", "/api/menu");
2228
2387
  }
2388
+ /** Every menu the store keeps, by handle — not only the header and footer. */
2389
+ async listMenus() {
2390
+ return await this.data("/x/navigation/menus") ?? [];
2391
+ }
2392
+ /** One menu by handle, in the storefront's shape: `{ menuId, name, items: [{ id, name, link, items }] }`. */
2393
+ async getMenu(handle) {
2394
+ const m = await this.data(`/x/navigation/menus/${encodeURIComponent(handle)}`);
2395
+ return { menuId: m.handle, name: m.title, items: (m.items ?? []).map(toItem) };
2396
+ }
2229
2397
  /** The header and footer menus, or `null` when the module is not there. Cached: a menu is store-level. */
2230
2398
  async engineMenus() {
2231
2399
  if (!await this.facts.pluginOn("navigation")) return null;
@@ -2244,6 +2412,24 @@ var MenuService = class extends BaseService {
2244
2412
  };
2245
2413
  var menuService = new MenuService();
2246
2414
 
2415
+ // src/services/newsletter-service.ts
2416
+ var NewsletterService = class extends BaseService {
2417
+ async subscribe({ email, name, source }) {
2418
+ return this.data("/x/newsletter/subscribe", {
2419
+ method: "POST",
2420
+ body: { email: String(email ?? "").trim(), ...name ? { name } : {}, ...source ? { source } : {} }
2421
+ });
2422
+ }
2423
+ /** The token from the unsubscribe link in a newsletter email. */
2424
+ async unsubscribe(token) {
2425
+ return this.data("/x/newsletter/unsubscribe", {
2426
+ method: "POST",
2427
+ body: { token: String(token ?? "") }
2428
+ });
2429
+ }
2430
+ };
2431
+ var newsletterService = new NewsletterService();
2432
+
2247
2433
  // src/services/store-service.ts
2248
2434
  function symbolFor(code) {
2249
2435
  try {
@@ -2277,6 +2463,22 @@ var StoreService = class extends BaseService {
2277
2463
  }
2278
2464
  return super.get(path);
2279
2465
  }
2466
+ /**
2467
+ * The storefront's newsletter box posts `{ email }` to Litekart's
2468
+ * `/api/newsletter/subscribe` through this verb; ext/newsletter is where
2469
+ * that list lives here.
2470
+ */
2471
+ async post(path, data) {
2472
+ if (path.split("?")[0] === "/api/newsletter/subscribe") {
2473
+ const { email, name } = data ?? {};
2474
+ return await new NewsletterService(this._fetch).subscribe({
2475
+ email: email ?? "",
2476
+ name,
2477
+ source: "storefront"
2478
+ });
2479
+ }
2480
+ return super.post(path, data);
2481
+ }
2280
2482
  async live() {
2281
2483
  const [ready, plugins, menus, base] = await Promise.all([
2282
2484
  this.facts.ready(),
@@ -2582,9 +2784,304 @@ var varniCustomProductService = new VarniCustomProductService();
2582
2784
  var pluginsService = pluginService;
2583
2785
  var settingsService = settingService;
2584
2786
 
2787
+ // src/services/variant-service.ts
2788
+ var VariantService = class extends BaseService {
2789
+ /** A product's variants, inactive ones included (filter on `active`), in the merchant's order. */
2790
+ async list(productId, { page = 1, limit = 50 } = {}) {
2791
+ const env = await this.engine("/api/variants", { query: { product_id: productId, page, limit } });
2792
+ return { data: (env.data ?? []).map(withAmounts), meta: env.meta };
2793
+ }
2794
+ async getOne(id2) {
2795
+ return withAmounts(await this.data(`/api/variants/${encodeURIComponent(String(id2))}`));
2796
+ }
2797
+ /**
2798
+ * What each approved seller asks for this variant, cheapest first. A cart
2799
+ * line takes a variant, not an offer, so this informs a shopper rather than
2800
+ * choosing who fulfils.
2801
+ */
2802
+ async offers(variantId) {
2803
+ const offers = await this.data(`/api/variants/${encodeURIComponent(String(variantId))}/offers`) ?? [];
2804
+ return offers.map((o) => ({ ...o, price: { ...o.price, amount: toMajor(o.price) } }));
2805
+ }
2806
+ };
2807
+ function withAmounts(v) {
2808
+ return {
2809
+ ...v,
2810
+ price: { ...v.price, amount: toMajor(v.price) },
2811
+ compare_at_price: v.compare_at_price ? { ...v.compare_at_price, amount: toMajor(v.compare_at_price) } : void 0
2812
+ };
2813
+ }
2814
+ var variantService = new VariantService();
2815
+
2816
+ // src/services/b2b-service.ts
2817
+ var B2bService = class extends BaseService {
2818
+ // ------------------------------------------------------------ company
2819
+ /** The company, the caller's role in it, and its credit: limit, outstanding, available, overdue. */
2820
+ async me() {
2821
+ const me = await this.data("/x/b2b/me", { auth: true });
2822
+ const { notes: _notes, metadata: _metadata, ...company } = me.company;
2823
+ return { company, role: me.role, credit: me.credit };
2824
+ }
2825
+ async members() {
2826
+ return await this.data("/x/b2b/members", { auth: true }) ?? [];
2827
+ }
2828
+ /** Admin only. Demoting the last admin is refused (409). */
2829
+ async setMemberRole(customerId, role) {
2830
+ return this.data(`/x/b2b/members/${id(customerId)}`, { method: "PATCH", auth: true, body: { role } });
2831
+ }
2832
+ /** An admin removes anyone; any member may remove themselves. */
2833
+ async removeMember(customerId) {
2834
+ await this.engine(`/x/b2b/members/${id(customerId)}`, { method: "DELETE", auth: true });
2835
+ }
2836
+ // ------------------------------------------------------------ invitations
2837
+ /** Admin only. Open invitations: not accepted, not expired. */
2838
+ async invitations() {
2839
+ return await this.data("/x/b2b/invitations", { auth: true }) ?? [];
2840
+ }
2841
+ /** Admin only. Emails a link; a second invitation to the same address replaces the first. */
2842
+ async invite({ email, role }) {
2843
+ return this.data("/x/b2b/invitations", {
2844
+ method: "POST",
2845
+ auth: true,
2846
+ body: { email: String(email ?? "").trim(), role }
2847
+ });
2848
+ }
2849
+ async withdrawInvitation(invitationId) {
2850
+ await this.engine(`/x/b2b/invitations/${id(invitationId)}`, { method: "DELETE", auth: true });
2851
+ }
2852
+ /**
2853
+ * The token from the invitation email, accepted by the account it was sent
2854
+ * to — which also confirms that account's email address.
2855
+ */
2856
+ async acceptInvitation(token) {
2857
+ return this.data("/x/b2b/invitations/accept", {
2858
+ method: "POST",
2859
+ auth: true,
2860
+ body: { token: String(token ?? "").trim() }
2861
+ });
2862
+ }
2863
+ // ------------------------------------------------------------ filling a basket
2864
+ /**
2865
+ * A quick order: up to 500 lines by SKU or variant id, into the bag (or a new
2866
+ * one), at the company's prices. A line that will not go in is reported in
2867
+ * `rejected` with its reason; the rest are added.
2868
+ */
2869
+ async addLines({
2870
+ cartId,
2871
+ lines
2872
+ }) {
2873
+ const res = await this.data("/x/b2b/cart/lines", {
2874
+ method: "POST",
2875
+ auth: true,
2876
+ body: {
2877
+ ...cartId ?? storedCartId() ? { cart_id: cartId ?? storedCartId() } : {},
2878
+ lines: (lines ?? []).map((l) => ({
2879
+ ...l.variantId ? { variant_id: Number(l.variantId) } : {},
2880
+ ...l.sku ? { sku: String(l.sku).trim() } : {},
2881
+ quantity: Number(l.quantity)
2882
+ }))
2883
+ }
2884
+ });
2885
+ rememberCartId(res.cart.id);
2886
+ return { cart: toCart(res.cart, cartExtras(res.cart.id)), rejected: res.rejected ?? [] };
2887
+ }
2888
+ /** A past order's lines in a new bag, at today's prices. Nothing is placed. */
2889
+ async reorder(orderId) {
2890
+ const res = await this.data(
2891
+ `/x/b2b/orders/${id(orderId)}/reorder`,
2892
+ {
2893
+ method: "POST",
2894
+ auth: true
2895
+ }
2896
+ );
2897
+ rememberCartId(res.cart.id);
2898
+ return { cart: toCart(res.cart, cartExtras(res.cart.id)), rejected: res.rejected ?? [] };
2899
+ }
2900
+ // ------------------------------------------------------------ placing an order
2901
+ /**
2902
+ * Places the bag through the company: on account by default, with a PO
2903
+ * number, optionally only some lines (`lineIds`). A buyer over the company's
2904
+ * approval threshold gets `{ kind: 'approval' }` instead of an order — the
2905
+ * bag stays as it was, and an approver places it later.
2906
+ *
2907
+ * Over the credit limit is 403 `credit_limit_exceeded`; `me().credit` says
2908
+ * how much is available before the shopper tries.
2909
+ */
2910
+ async checkout({
2911
+ cartId,
2912
+ lineIds,
2913
+ shippingRateId,
2914
+ returnUrl,
2915
+ paymentData,
2916
+ ...opts
2917
+ } = {}) {
2918
+ const cart = cartId || storedCartId();
2919
+ if (!cart) throw new GoCommerceError("Your bag is empty.", 400);
2920
+ const extras = cartExtras(cart);
2921
+ const rate = shippingRateId ?? extras.shippingRateId;
2922
+ const body = {
2923
+ cart_id: cart,
2924
+ ...this.placeBody(opts, extras.shippingAddress ?? void 0),
2925
+ ...rate ? { shipping_rate_id: Number(rate) } : {},
2926
+ ...lineIds?.length ? { line_ids: lineIds.map(Number) } : {},
2927
+ ...returnUrl ? { return_url: returnUrl } : {},
2928
+ ...paymentData ? { payment_data: paymentData } : {}
2929
+ };
2930
+ const res = await this.data("/x/b2b/checkout", {
2931
+ method: "POST",
2932
+ auth: true,
2933
+ body,
2934
+ headers: { "Idempotency-Key": attemptKey(`b2b:${JSON.stringify(body)}`) }
2935
+ });
2936
+ return this.placed(res, cart);
2937
+ }
2938
+ // ------------------------------------------------------------ the company's orders
2939
+ /** Admins and approvers see the company's; a buyer sees their own. `q` matches the number, PO or email. */
2940
+ async orders({
2941
+ page = 1,
2942
+ limit = 20,
2943
+ q,
2944
+ overdue
2945
+ } = {}) {
2946
+ return this.engine("/x/b2b/orders", {
2947
+ auth: true,
2948
+ query: { page, limit, q, ...overdue ? { overdue: "true" } : {} }
2949
+ });
2950
+ }
2951
+ // ------------------------------------------------------------ approvals
2952
+ async approvals({
2953
+ page = 1,
2954
+ limit = 20,
2955
+ status
2956
+ } = {}) {
2957
+ return this.engine("/x/b2b/approvals", { auth: true, query: { page, limit, status } });
2958
+ }
2959
+ async approval(approvalId) {
2960
+ return this.data(`/x/b2b/approvals/${id(approvalId)}`, { auth: true });
2961
+ }
2962
+ /**
2963
+ * Admin or approver: places the requested order at the prices it was
2964
+ * requested at. Credit limits and holds still apply. The payment intent is
2965
+ * the approver's to act on — for an account order there is nothing to do.
2966
+ */
2967
+ async approve(approvalId) {
2968
+ const res = await this.data(`/x/b2b/approvals/${id(approvalId)}/approve`, {
2969
+ method: "POST",
2970
+ auth: true
2971
+ });
2972
+ return { approval: res.approval, orderNo: res.order.number, order: toOrder(res.order), payment: res.payment };
2973
+ }
2974
+ async reject(approvalId, reason) {
2975
+ return this.data(`/x/b2b/approvals/${id(approvalId)}/reject`, {
2976
+ method: "POST",
2977
+ auth: true,
2978
+ ...reason ? { body: { reason } } : {}
2979
+ });
2980
+ }
2981
+ /** The buyer who asked withdraws the request. */
2982
+ async cancelApproval(approvalId) {
2983
+ return this.data(`/x/b2b/approvals/${id(approvalId)}/cancel`, { method: "POST", auth: true });
2984
+ }
2985
+ // ------------------------------------------------------------ quotes
2986
+ async quotes({ page = 1, limit = 20, status } = {}) {
2987
+ return this.engine("/x/b2b/quotes", { auth: true, query: { page, limit, status } });
2988
+ }
2989
+ async quote(quoteId) {
2990
+ return this.data(`/x/b2b/quotes/${id(quoteId)}`, { auth: true });
2991
+ }
2992
+ /** Asks the store to price these lines. The store answers with unit prices and an expiry. */
2993
+ async requestQuote({ lines, note }) {
2994
+ return this.data("/x/b2b/quotes", {
2995
+ method: "POST",
2996
+ auth: true,
2997
+ body: {
2998
+ lines: (lines ?? []).map((l) => ({ variant_id: Number(l.variantId), quantity: Number(l.quantity) })),
2999
+ ...note ? { note } : {}
3000
+ }
3001
+ });
3002
+ }
3003
+ /** Places the quote at its agreed prices — or, for a buyer over the threshold, asks for approval. */
3004
+ async acceptQuote(quoteId, opts = {}) {
3005
+ const res = await this.data(`/x/b2b/quotes/${id(quoteId)}/accept`, {
3006
+ method: "POST",
3007
+ auth: true,
3008
+ body: this.placeBody(opts)
3009
+ });
3010
+ return this.placed(res);
3011
+ }
3012
+ async declineQuote(quoteId) {
3013
+ return this.data(`/x/b2b/quotes/${id(quoteId)}/decline`, { method: "POST", auth: true });
3014
+ }
3015
+ // ------------------------------------------------------------ dealer leads
3016
+ /**
3017
+ * The public "find a dealer" form: no account needed. The engine routes it to
3018
+ * the dealer whose territory covers the address — or keeps it — and answers
3019
+ * the same either way, so the form never reveals who got it.
3020
+ */
3021
+ async submitLead(lead) {
3022
+ return this.data("/x/b2b/leads", {
3023
+ method: "POST",
3024
+ body: {
3025
+ ...lead.name ? { name: lead.name } : {},
3026
+ ...lead.email ? { email: lead.email } : {},
3027
+ ...lead.phone ? { phone: lead.phone } : {},
3028
+ ...lead.message ? { message: lead.message } : {},
3029
+ ...lead.country ? { country: lead.country } : {},
3030
+ ...lead.state ? { state: lead.state } : {},
3031
+ ...lead.postalCode ? { postal_code: lead.postalCode } : {},
3032
+ ...lead.productId ? { product_id: Number(lead.productId) } : {},
3033
+ ...lead.variantId ? { variant_id: Number(lead.variantId) } : {},
3034
+ ...lead.source ? { source: lead.source } : {}
3035
+ }
3036
+ });
3037
+ }
3038
+ /** A dealer's admins and approvers: the leads routed to their company. */
3039
+ async leads({ page = 1, limit = 20, status } = {}) {
3040
+ return this.engine("/x/b2b/leads", { auth: true, query: { page, limit, status } });
3041
+ }
3042
+ async setLeadStatus(leadId, status) {
3043
+ return this.data(`/x/b2b/leads/${id(leadId)}`, { method: "PATCH", auth: true, body: { status } });
3044
+ }
3045
+ // ------------------------------------------------------------ internals
3046
+ /** The fields checkout and quote acceptance share. Only declared ones: the engine refuses the rest. */
3047
+ placeBody(opts, fallback) {
3048
+ const raw = opts.address ?? fallback;
3049
+ if (!raw) throw new GoCommerceError("Add a delivery address first.", 400);
3050
+ const address = "line1" in raw ? raw : toEngineAddress(raw);
3051
+ return {
3052
+ address,
3053
+ ...opts.paymentMethod ? { payment_method: String(opts.paymentMethod).toLowerCase() } : {},
3054
+ ...opts.poNumber ? { po_number: String(opts.poNumber).trim() } : {},
3055
+ ...opts.name ?? address.name ? { name: opts.name ?? address.name } : {},
3056
+ ...opts.phone ?? address.phone ? { phone: opts.phone ?? address.phone } : {}
3057
+ };
3058
+ }
3059
+ placed(res, cartId) {
3060
+ if (res.approval && !res.order) return { kind: "approval", approval: res.approval };
3061
+ const order = res.order;
3062
+ if (order.access_token && cartId) {
3063
+ rememberOrder({ number: order.number, token: order.access_token, cartId, reference: res.payment?.reference });
3064
+ }
3065
+ return { kind: "order", orderNo: order.number, order: toOrder(order), payment: res.payment };
3066
+ }
3067
+ };
3068
+ var id = (v) => encodeURIComponent(String(v));
3069
+ var keys = /* @__PURE__ */ new Map();
3070
+ function attemptKey(request) {
3071
+ let key = keys.get(request);
3072
+ if (!key) {
3073
+ const c = globalThis.crypto;
3074
+ key = c?.randomUUID?.() ?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
3075
+ keys.set(request, key);
3076
+ }
3077
+ return key;
3078
+ }
3079
+ var b2bService = new B2bService();
3080
+
2585
3081
  exports.AddressService = AddressService;
2586
3082
  exports.AuthService = AuthService;
2587
3083
  exports.AutocompleteService = AutocompleteService;
3084
+ exports.B2bService = B2bService;
2588
3085
  exports.BannerService = BannerService;
2589
3086
  exports.BaseService = BaseService;
2590
3087
  exports.BlogService = BlogService;
@@ -2609,6 +3106,7 @@ exports.HomeService = HomeService;
2609
3106
  exports.InitService = InitService;
2610
3107
  exports.MeilisearchService = MeilisearchService;
2611
3108
  exports.MenuService = MenuService;
3109
+ exports.NewsletterService = NewsletterService;
2612
3110
  exports.NotSupportedError = NotSupportedError;
2613
3111
  exports.OrderService = OrderService;
2614
3112
  exports.PageService = PageService;
@@ -2619,6 +3117,7 @@ exports.PopularSearchService = PopularSearchService;
2619
3117
  exports.PopularityService = PopularityService;
2620
3118
  exports.ProductService = ProductService;
2621
3119
  exports.ProfileService = ProfileService;
3120
+ exports.REDIRECT_GATEWAYS = REDIRECT_GATEWAYS;
2622
3121
  exports.ReelsService = ReelsService;
2623
3122
  exports.RegionService = RegionService;
2624
3123
  exports.ReviewService = ReviewService;
@@ -2629,6 +3128,7 @@ exports.StateService = StateService;
2629
3128
  exports.StoreService = StoreService;
2630
3129
  exports.UploadService = UploadService;
2631
3130
  exports.UserService = UserService;
3131
+ exports.VariantService = VariantService;
2632
3132
  exports.VarniCustomDesignService = VarniCustomDesignService;
2633
3133
  exports.VarniCustomProductService = VarniCustomProductService;
2634
3134
  exports.VendorService = VendorService;
@@ -2637,6 +3137,7 @@ exports.WishlistService = WishlistService;
2637
3137
  exports.addressService = addressService;
2638
3138
  exports.authService = authService;
2639
3139
  exports.autocompleteService = autocompleteService;
3140
+ exports.b2bService = b2bService;
2640
3141
  exports.bannerService = bannerService;
2641
3142
  exports.blogService = blogService;
2642
3143
  exports.cartService = cartService;
@@ -2665,9 +3166,11 @@ exports.galleryService = galleryService;
2665
3166
  exports.hasStaticStore = hasStaticStore;
2666
3167
  exports.homeService = homeService;
2667
3168
  exports.initService = initService;
3169
+ exports.mediaUrl = mediaUrl;
2668
3170
  exports.meilisearchService = meilisearchService;
2669
3171
  exports.menuService = menuService;
2670
3172
  exports.minorToMajor = minorToMajor;
3173
+ exports.newsletterService = newsletterService;
2671
3174
  exports.orderService = orderService;
2672
3175
  exports.pageService = pageService;
2673
3176
  exports.paymentMethodService = paymentMethodService;
@@ -2710,6 +3213,7 @@ exports.toVariant = toVariant;
2710
3213
  exports.toVendor = toVendor;
2711
3214
  exports.uploadService = uploadService;
2712
3215
  exports.userService = userService;
3216
+ exports.variantService = variantService;
2713
3217
  exports.varniCustomDesignService = varniCustomDesignService;
2714
3218
  exports.varniCustomProductService = varniCustomProductService;
2715
3219
  exports.vendorService = vendorService;