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