@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.
@@ -165,7 +165,7 @@ function storedCartId() {
165
165
  if (!v || v === "undefined" || v === "null") return null;
166
166
  return v;
167
167
  }
168
- var rememberCartId = (id) => writeLocal("cart_id", id);
168
+ var rememberCartId = (id2) => writeLocal("cart_id", id2);
169
169
  var extrasKey = (cartId) => `gocommerce_cart_${cartId}`;
170
170
  var cartExtras = (cartId) => readJSON(extrasKey(cartId)) ?? {};
171
171
  function saveCartExtras(cartId, patch) {
@@ -189,6 +189,12 @@ var rememberWishlistToken = (token) => writeLocal(WISHLIST, token);
189
189
  var forgetWishlistToken = () => removeLocal(WISHLIST);
190
190
 
191
191
  // src/services/base.service.ts
192
+ function mediaUrl(url) {
193
+ if (!url) return null;
194
+ if (!url.startsWith("/") || url.startsWith("//")) return url;
195
+ const { apiUrl, proxyPath } = BaseService.getCredentials();
196
+ return (proxyPath || apiUrl || "").trim().replace(/\/+$/, "") + url;
197
+ }
192
198
  var BaseService = class _BaseService {
193
199
  static _credentials = { apiUrl: "" };
194
200
  _fetch;
@@ -233,7 +239,8 @@ var BaseService = class _BaseService {
233
239
  * as what it is rather than as a JSON parse error three frames away.
234
240
  */
235
241
  async engine(path, req = {}) {
236
- const url = this.engineURL(path, req.query);
242
+ const lang = (req.method ?? "GET") === "GET" ? this.creds.language : void 0;
243
+ const url = this.engineURL(path, lang ? { lang, ...req.query } : req.query);
237
244
  const headers = { Accept: "application/json", ...req.headers };
238
245
  if (req.body !== void 0) headers["Content-Type"] = "application/json";
239
246
  if (this.creds.store) headers["X-Store"] = this.creds.store;
@@ -411,7 +418,8 @@ var liveVariants = (p) => arr(p.variants).filter((v) => v.active);
411
418
  function variantImages(v) {
412
419
  const out = [];
413
420
  for (const img of [v.image, ...arr(v.images)]) {
414
- if (img?.url && (img.kind ?? "image") === "image" && !out.includes(img.url)) out.push(img.url);
421
+ const url = mediaUrl(img?.url);
422
+ if (url && (img?.kind ?? "image") === "image" && !out.includes(url)) out.push(url);
415
423
  }
416
424
  return out;
417
425
  }
@@ -420,7 +428,7 @@ function productImages(p) {
420
428
  const push = (url) => {
421
429
  if (url && !out.includes(url)) out.push(url);
422
430
  };
423
- push(p.image_url);
431
+ push(mediaUrl(p.image_url));
424
432
  for (const v of arr(p.variants)) for (const url of variantImages(v)) push(url);
425
433
  return out;
426
434
  }
@@ -456,6 +464,16 @@ function toVariant(v, p, optionless) {
456
464
  metadata: v.metadata ?? null
457
465
  };
458
466
  }
467
+ function attributesOf(p) {
468
+ const answers = p.metadata?.category;
469
+ if (!answers || typeof answers !== "object" || Array.isArray(answers)) return [];
470
+ return Object.entries(answers).flatMap(([handle, raw]) => {
471
+ const values = (Array.isArray(raw) ? raw : [raw]).map((v) => String(v ?? "").trim()).filter(Boolean);
472
+ if (!values.length) return [];
473
+ const name = handle.replace(/[-_]+/g, " ").replace(/^./, (c) => c.toUpperCase());
474
+ return [{ name, value: values.join(", "), handle }];
475
+ });
476
+ }
459
477
  function toProduct(p, extra = {}) {
460
478
  const live = liveVariants(p);
461
479
  const lead = live[0] ?? arr(p.variants)[0];
@@ -521,7 +539,7 @@ function toProduct(p, extra = {}) {
521
539
  rating: null,
522
540
  ratingCount: 0,
523
541
  reviewCount: 0,
524
- attributes: [],
542
+ attributes: attributesOf(p),
525
543
  metadata: p.metadata ?? null,
526
544
  createdAt: p.created_at,
527
545
  updatedAt: p.updated_at
@@ -540,9 +558,9 @@ function fromSearchDoc(d) {
540
558
  mrp: price,
541
559
  minPrice: price,
542
560
  maxPrice: minorToMajor(d.price_max_minor, d.currency),
543
- thumbnail: d.image ?? null,
544
- image_url: d.image ?? null,
545
- images: d.image ?? "",
561
+ thumbnail: mediaUrl(d.image),
562
+ image_url: mediaUrl(d.image),
563
+ images: mediaUrl(d.image) ?? "",
546
564
  stock: d.in_stock ? 1 : 0,
547
565
  hasStock: d.in_stock,
548
566
  // The index cannot say how much is left, only whether any is; a card
@@ -571,7 +589,7 @@ function toCategory(c) {
571
589
  level: c.depth,
572
590
  rank: c.position,
573
591
  childCount: c.child_count,
574
- thumbnail: c.metadata?.image ?? null,
592
+ thumbnail: mediaUrl(c.metadata?.image),
575
593
  description: c.metadata?.description ?? null,
576
594
  isActive: true,
577
595
  isFeatured: false,
@@ -591,7 +609,7 @@ function toCollection(c) {
591
609
  description: c.description || null,
592
610
  subTitle: null,
593
611
  rank: str(c.position),
594
- thumbnail: c.metadata?.image ?? null,
612
+ thumbnail: mediaUrl(c.metadata?.image),
595
613
  isActive: true,
596
614
  metaTitle: c.title,
597
615
  metaDescription: c.description || null,
@@ -674,7 +692,7 @@ function toCartLine(l, extra = {}) {
674
692
  title: l.title,
675
693
  name: l.title,
676
694
  slug: extra.slug ?? "",
677
- thumbnail: extra.thumbnail ?? null,
695
+ thumbnail: mediaUrl(extra.thumbnail),
678
696
  variantTitle: l.variant_label ?? "",
679
697
  qty: l.quantity,
680
698
  price: toMajor(l.unit_price),
@@ -782,8 +800,8 @@ function toOrderLine(l, currency) {
782
800
  title: l.title,
783
801
  name: l.title,
784
802
  slug: "",
785
- thumbnail: l.image_url ?? null,
786
- img: l.image_url ?? null,
803
+ thumbnail: mediaUrl(l.image_url),
804
+ img: mediaUrl(l.image_url),
787
805
  variantTitle: l.variant_label ?? "",
788
806
  qty: l.quantity,
789
807
  shippedQty: l.shipped_quantity,
@@ -1078,8 +1096,11 @@ var CatalogService = class extends BaseService {
1078
1096
  sorted
1079
1097
  };
1080
1098
  }
1099
+ const attr = Object.entries(q.attributes ?? {}).flatMap(
1100
+ ([handle, values]) => values.filter(Boolean).map((v) => `${handle}:${v}`)
1101
+ );
1081
1102
  const env = await this.engine("/api/products", {
1082
- query: { q: search, page, limit, channel: this.creds.channel }
1103
+ query: { q: search, page, limit, channel: this.creds.channel, attr }
1083
1104
  });
1084
1105
  return { ...toPage(env, (p) => toProduct(p)), categoryHierarchy: hierarchy, sorted };
1085
1106
  }
@@ -1214,6 +1235,16 @@ var num = (v) => {
1214
1235
  const n = Number(v);
1215
1236
  return Number.isFinite(n) ? n : void 0;
1216
1237
  };
1238
+ function attributesFrom(p) {
1239
+ const out = {};
1240
+ for (const [key, value] of p) {
1241
+ if (!key.startsWith("attributes.")) continue;
1242
+ const handle = key.slice("attributes.".length);
1243
+ const values = value.split(",").map((v) => decodeURIComponent(v).trim()).filter(Boolean);
1244
+ if (handle && values.length) out[handle] = [...out[handle] ?? [], ...values];
1245
+ }
1246
+ return Object.keys(out).length ? out : void 0;
1247
+ }
1217
1248
  var SearchService = class extends CatalogService {
1218
1249
  emptyResult() {
1219
1250
  return {
@@ -1235,7 +1266,8 @@ var SearchService = class extends CatalogService {
1235
1266
  sort: p.get("sort") ?? void 0,
1236
1267
  category: slug || p.get("categories")?.split(",")[0] || void 0,
1237
1268
  priceFrom: num(p.get("priceFrom")) ?? num(from ?? null),
1238
- priceTo: num(p.get("priceTo")) ?? num(to ?? null)
1269
+ priceTo: num(p.get("priceTo")) ?? num(to ?? null),
1270
+ attributes: attributesFrom(p)
1239
1271
  });
1240
1272
  return {
1241
1273
  data: res.data,
@@ -1367,10 +1399,10 @@ var REMOVE = -9999999;
1367
1399
  var CartService = class extends BaseService {
1368
1400
  facts = new StoreFacts(this._fetch);
1369
1401
  async getCartByCartId(cartId) {
1370
- const id = clean(cartId);
1371
- if (!id) return emptyCart(await this.currency());
1402
+ const id2 = clean(cartId);
1403
+ if (!id2) return emptyCart(await this.currency());
1372
1404
  try {
1373
- return await this.present(await this.data(`/api/carts/${encodeURIComponent(id)}`));
1405
+ return await this.present(await this.data(`/api/carts/${encodeURIComponent(id2)}`));
1374
1406
  } catch (err) {
1375
1407
  if (err instanceof GoCommerceError && err.status === 404) return emptyCart(await this.currency());
1376
1408
  throw err;
@@ -1385,9 +1417,9 @@ var CartService = class extends BaseService {
1385
1417
  * to clear the bag on the success page.
1386
1418
  */
1387
1419
  async refereshCart() {
1388
- const id = storedCartId();
1389
- const cart = await this.getCartByCartId(id);
1390
- if (id && cart.status === "converted") forgetCartExtras(id);
1420
+ const id2 = storedCartId();
1421
+ const cart = await this.getCartByCartId(id2);
1422
+ if (id2 && cart.status === "converted") forgetCartExtras(id2);
1391
1423
  return cart;
1392
1424
  }
1393
1425
  /**
@@ -1400,21 +1432,21 @@ var CartService = class extends BaseService {
1400
1432
  qty = 1,
1401
1433
  variantId
1402
1434
  }) {
1403
- let id = clean(cartId) ?? storedCartId();
1404
- if (lineId && id) {
1405
- const cart = await this.data(`/api/carts/${encodeURIComponent(id)}`);
1435
+ let id2 = clean(cartId) ?? storedCartId();
1436
+ if (lineId && id2) {
1437
+ const cart = await this.data(`/api/carts/${encodeURIComponent(id2)}`);
1406
1438
  const line = (cart.line_items ?? []).find((l) => String(l.id) === String(lineId));
1407
1439
  if (line) {
1408
1440
  const next = qty === REMOVE ? 0 : line.quantity + Number(qty);
1409
- return this.setQuantity(id, line.id, next);
1441
+ return this.setQuantity(id2, line.id, next);
1410
1442
  }
1411
1443
  }
1412
- if (qty === REMOVE || Number(qty) <= 0) return this.getCartByCartId(id);
1444
+ if (qty === REMOVE || Number(qty) <= 0) return this.getCartByCartId(id2);
1413
1445
  if (!variantId) throw new GoCommerceError("Please choose an option before adding this to your bag.", 400);
1414
- if (!id) id = await this.open();
1446
+ if (!id2) id2 = await this.open();
1415
1447
  try {
1416
1448
  return await this.present(
1417
- await this.data(`/api/carts/${encodeURIComponent(id)}/line-items`, {
1449
+ await this.data(`/api/carts/${encodeURIComponent(id2)}/line-items`, {
1418
1450
  method: "POST",
1419
1451
  body: { variant_id: Number(variantId), quantity: Number(qty) }
1420
1452
  })
@@ -1438,15 +1470,15 @@ var CartService = class extends BaseService {
1438
1470
  lineId,
1439
1471
  qty
1440
1472
  }) {
1441
- const id = clean(cartId) ?? storedCartId();
1442
- if (!id) return emptyCart(await this.currency());
1443
- return this.setQuantity(id, lineId, Number(qty));
1473
+ const id2 = clean(cartId) ?? storedCartId();
1474
+ if (!id2) return emptyCart(await this.currency());
1475
+ return this.setQuantity(id2, lineId, Number(qty));
1444
1476
  }
1445
1477
  async removeCart({ cartId, lineId }) {
1446
- const id = clean(cartId) ?? storedCartId();
1447
- if (!id) return emptyCart(await this.currency());
1478
+ const id2 = clean(cartId) ?? storedCartId();
1479
+ if (!id2) return emptyCart(await this.currency());
1448
1480
  return this.present(
1449
- await this.data(`/api/carts/${encodeURIComponent(id)}/line-items/${encodeURIComponent(String(lineId))}`, {
1481
+ await this.data(`/api/carts/${encodeURIComponent(id2)}/line-items/${encodeURIComponent(String(lineId))}`, {
1450
1482
  method: "DELETE"
1451
1483
  })
1452
1484
  );
@@ -1457,8 +1489,8 @@ var CartService = class extends BaseService {
1457
1489
  * and the rest is kept for the checkout request.
1458
1490
  */
1459
1491
  async updateCart2(args) {
1460
- const id = clean(args.cartId) ?? storedCartId();
1461
- if (!id) return emptyCart(await this.currency());
1492
+ const id2 = clean(args.cartId) ?? storedCartId();
1493
+ if (!id2) return emptyCart(await this.currency());
1462
1494
  const patch = {};
1463
1495
  if (args.phone !== void 0) patch.phone = args.phone || void 0;
1464
1496
  if (args.shippingAddress !== void 0) {
@@ -1468,20 +1500,20 @@ var CartService = class extends BaseService {
1468
1500
  if (args.billingAddress !== void 0 || args.isBillingAddressSameAsShipping) {
1469
1501
  patch.billingAddress = args.isBillingAddressSameAsShipping ? null : args.billingAddress ?? null;
1470
1502
  }
1471
- const before = cartExtras(id);
1503
+ const before = cartExtras(id2);
1472
1504
  const country = (a) => String(a?.countryCode || a?.country || "").toUpperCase();
1473
1505
  if (args.shippingAddress !== void 0 && country(args.shippingAddress) !== country(before.shippingAddress)) {
1474
1506
  patch.shippingRateId = null;
1475
1507
  patch.shippingRate = null;
1476
1508
  }
1477
- saveCartExtras(id, patch);
1509
+ saveCartExtras(id2, patch);
1478
1510
  const email = args.email ?? (args.shippingAddress?.email ? String(args.shippingAddress.email) : void 0);
1479
1511
  if (email !== void 0) {
1480
1512
  return this.present(
1481
- await this.data(`/api/carts/${encodeURIComponent(id)}/email`, { method: "PUT", body: { email } })
1513
+ await this.data(`/api/carts/${encodeURIComponent(id2)}/email`, { method: "PUT", body: { email } })
1482
1514
  );
1483
1515
  }
1484
- return this.getCartByCartId(id);
1516
+ return this.getCartByCartId(id2);
1485
1517
  }
1486
1518
  /**
1487
1519
  * A discount code. The engine prices it against the basket now and decides
@@ -1489,34 +1521,34 @@ var CartService = class extends BaseService {
1489
1521
  * because the cart itself does not carry the code.
1490
1522
  */
1491
1523
  async applyCoupon({ cartId, couponCode }) {
1492
- const id = clean(cartId) ?? storedCartId();
1493
- if (!id) throw new GoCommerceError("Add something to your bag before applying a code.", 400);
1494
- const cart = await this.data(`/api/carts/${encodeURIComponent(id)}`);
1495
- const discount = await this.data(`/api/carts/${encodeURIComponent(id)}/discount`, {
1524
+ const id2 = clean(cartId) ?? storedCartId();
1525
+ if (!id2) throw new GoCommerceError("Add something to your bag before applying a code.", 400);
1526
+ const cart = await this.data(`/api/carts/${encodeURIComponent(id2)}`);
1527
+ const discount = await this.data(`/api/carts/${encodeURIComponent(id2)}/discount`, {
1496
1528
  method: "PUT",
1497
1529
  body: { code: String(couponCode ?? "").trim(), ...cart.email ? { email: cart.email } : {} }
1498
1530
  });
1499
- saveCartExtras(id, { discount: { ...discount, code: discount.code || String(couponCode).trim() } });
1531
+ saveCartExtras(id2, { discount: { ...discount, code: discount.code || String(couponCode).trim() } });
1500
1532
  return this.present(cart);
1501
1533
  }
1502
1534
  async removeCoupon(cartId) {
1503
- const id = clean(cartId) ?? storedCartId();
1504
- if (!id) return emptyCart(await this.currency());
1505
- await this.engine(`/api/carts/${encodeURIComponent(id)}/discount`, { method: "DELETE" });
1506
- saveCartExtras(id, { discount: null });
1507
- return this.getCartByCartId(id);
1535
+ const id2 = clean(cartId) ?? storedCartId();
1536
+ if (!id2) return emptyCart(await this.currency());
1537
+ await this.engine(`/api/carts/${encodeURIComponent(id2)}/discount`, { method: "DELETE" });
1538
+ saveCartExtras(id2, { discount: null });
1539
+ return this.getCartByCartId(id2);
1508
1540
  }
1509
1541
  /** The delivery option the shopper picked, priced from the engine's own quote. */
1510
1542
  async updateShippingRate({ cartId, shippingRateId }) {
1511
- const id = clean(cartId) ?? storedCartId();
1512
- if (!id) return emptyCart(await this.currency());
1513
- const ship = cartExtras(id).shippingAddress;
1543
+ const id2 = clean(cartId) ?? storedCartId();
1544
+ if (!id2) return emptyCart(await this.currency());
1545
+ const ship = cartExtras(id2).shippingAddress;
1514
1546
  const country = String(ship?.countryCode || ship?.country || "").toUpperCase();
1515
1547
  if (!country) throw new GoCommerceError("Add a delivery address first.", 400);
1516
- const quote2 = await this.data("/api/checkout/rates", { query: { cart: id, country, state: ship?.state ? String(ship.state) : void 0 } });
1548
+ const quote2 = await this.data("/api/checkout/rates", { query: { cart: id2, country, state: ship?.state ? String(ship.state) : void 0 } });
1517
1549
  const rate = (quote2.rates ?? []).find((r) => String(r.rate_id) === String(shippingRateId));
1518
1550
  if (!rate) throw new GoCommerceError("That delivery option is not available for this address any more.", 409);
1519
- saveCartExtras(id, {
1551
+ saveCartExtras(id2, {
1520
1552
  shippingRateId: String(rate.rate_id),
1521
1553
  shippingRate: {
1522
1554
  id: String(rate.rate_id),
@@ -1525,7 +1557,7 @@ var CartService = class extends BaseService {
1525
1557
  currency: rate.price.currency
1526
1558
  }
1527
1559
  });
1528
- return this.getCartByCartId(id);
1560
+ return this.getCartByCartId(id2);
1529
1561
  }
1530
1562
  /** The payment method is chosen at checkout, by which checkout route is called. */
1531
1563
  async updateCartPaymentMethod({ cartId }) {
@@ -1571,25 +1603,25 @@ var CartService = class extends BaseService {
1571
1603
  return { slug: p.slug, thumbnail: v?.image?.url ?? p.image_url ?? null };
1572
1604
  });
1573
1605
  }
1574
- productFor(id) {
1606
+ productFor(id2) {
1575
1607
  return remember(
1576
- `${this.engineBase()}|product|${id}`,
1577
- () => this.data(`/api/products/${id}`).catch(() => null)
1608
+ `${this.engineBase()}|product|${id2}`,
1609
+ () => this.data(`/api/products/${id2}`).catch(() => null)
1578
1610
  );
1579
1611
  }
1580
1612
  };
1581
- var clean = (id) => id && id !== "undefined" && id !== "null" ? id : null;
1613
+ var clean = (id2) => id2 && id2 !== "undefined" && id2 !== "null" ? id2 : null;
1582
1614
  var cartService = new CartService();
1583
1615
 
1584
1616
  // src/services/checkout-service.ts
1585
1617
  var CheckoutService = class extends BaseService {
1586
1618
  /** Delivery options for the address on the cart: `{ data: [{ id, name, base_rate }] }`. */
1587
1619
  async getShippingRates({ cartId } = {}) {
1588
- const id = cartId || storedCartId();
1589
- const ship = id ? cartExtras(id).shippingAddress : null;
1620
+ const id2 = cartId || storedCartId();
1621
+ const ship = id2 ? cartExtras(id2).shippingAddress : null;
1590
1622
  const country = String(ship?.countryCode || ship?.country || "").toUpperCase();
1591
- if (!id || !country) return { data: [] };
1592
- const quote2 = await this.data("/api/checkout/rates", { query: { cart: id, country, state: ship?.state ? String(ship.state) : void 0 } });
1623
+ if (!id2 || !country) return { data: [] };
1624
+ const quote2 = await this.data("/api/checkout/rates", { query: { cart: id2, country, state: ship?.state ? String(ship.state) : void 0 } });
1593
1625
  return {
1594
1626
  data: (quote2.rates ?? []).map((r) => ({
1595
1627
  id: String(r.rate_id),
@@ -1643,10 +1675,11 @@ var CheckoutService = class extends BaseService {
1643
1675
  if (!order) throw new GoCommerceError("That payment does not belong to an order placed from this browser.", 404);
1644
1676
  return { order_no: order.number, status: "processing" };
1645
1677
  }
1646
- // The family's other gateways. GoCommerce's Stripe is a Payment Element
1647
- // (a client secret, not a hosted page), and PayPal, Cashfree, PhonePe and
1648
- // Affirm are not engine gateways at all; paymentMethodService never lists
1649
- // them, so these are reached only by a storefront that hard-codes one.
1678
+ // The family's other gateway flows. GoCommerce's Stripe is a Payment
1679
+ // Element (a client secret, not a hosted page) — `checkout('stripe')` returns
1680
+ // that secret for a storefront that mounts the element. Cashfree, PhonePe and
1681
+ // Affirm are not engine gateways at all. paymentMethodService never offers
1682
+ // these codes, so they are reached only by a storefront that hard-codes one.
1650
1683
  async checkoutStripe() {
1651
1684
  throw unsupported(
1652
1685
  "checkoutStripe",
@@ -1656,11 +1689,34 @@ var CheckoutService = class extends BaseService {
1656
1689
  async checkoutStripeCapture() {
1657
1690
  throw unsupported("checkoutStripeCapture", "Stripe confirms payments to the engine by webhook");
1658
1691
  }
1659
- async checkoutPaypal() {
1660
- throw unsupported("checkoutPaypal", "the engine has no PayPal gateway");
1692
+ /**
1693
+ * The storefront's generic redirect flow, which it reaches through the code
1694
+ * `PAYPAL`. The engine has no PayPal gateway; paymentMethodService puts the
1695
+ * store's first hosted-page gateway — Adyen, Paddle, Lemon Squeezy, Creem,
1696
+ * Hyperswitch or RevenueCat — under that code, with its own name and a
1697
+ * neutral icon, because this is the one storefront flow that sends a shopper
1698
+ * to a URL and back. The shopper returns to the success page; the gateway's
1699
+ * webhook tells the engine the order is paid.
1700
+ */
1701
+ async checkoutPaypal({ cartId, origin } = {}) {
1702
+ const gateway = await this.redirectGateway();
1703
+ if (!gateway) throw unsupported("checkoutPaypal", "this store has no hosted-page payment gateway switched on");
1704
+ const id2 = cartId || storedCartId() || "";
1705
+ const back = origin ? `${origin.replace(/\/+$/, "")}/checkout/success?cart_id=${encodeURIComponent(id2)}` : void 0;
1706
+ const result = await this.place(gateway, id2, back ? { return_url: back } : {});
1707
+ const url = result.payment.client_data?.url;
1708
+ if (!url) throw new GoCommerceError("The payment page did not open. Please try again.", 502);
1709
+ return { redirect_url: url, order_no: result.order.number };
1661
1710
  }
1662
- async capturePaypalPayment() {
1663
- throw unsupported("capturePaypalPayment", "the engine has no PayPal gateway");
1711
+ /**
1712
+ * Nothing to capture: a hosted-page gateway confirms the payment to the
1713
+ * engine by webhook. Answering, rather than throwing, sends the storefront's
1714
+ * process page on to the success page instead of the failure page.
1715
+ */
1716
+ async capturePaypalPayment({
1717
+ order_no
1718
+ } = {}) {
1719
+ return { order_no: order_no ?? null, status: "processing" };
1664
1720
  }
1665
1721
  async checkoutCashfree() {
1666
1722
  throw unsupported("checkoutCashfree", "the engine has no Cashfree gateway");
@@ -1683,6 +1739,57 @@ var CheckoutService = class extends BaseService {
1683
1739
  async checkoutPOS() {
1684
1740
  throw unsupported("checkoutPOS", "point-of-sale orders are placed from the admin panel");
1685
1741
  }
1742
+ /**
1743
+ * Any payment method the engine has switched on, by its engine code —
1744
+ * `cod`, `stripe`, `razorpay`, `adyen`, `paddle`, `helcim`… For a storefront
1745
+ * that drives the payment itself: the answer says what to do next.
1746
+ *
1747
+ * - `kind: 'none'` — nothing to pay now (cash on delivery).
1748
+ * - `kind: 'client_action'` — mount the gateway's own widget with
1749
+ * `clientData`: Stripe's Payment Element takes `clientSecret`, Razorpay's
1750
+ * modal `razorpay_order_id` and `key_id`, Helcim's `checkout_token`.
1751
+ * - `kind: 'redirect'` — send the shopper to `redirectUrl`; `returnUrl` is
1752
+ * where the gateway brings them back.
1753
+ *
1754
+ * Either way the gateway's webhook tells the engine when the money arrives;
1755
+ * read `orderService.getOrder(orderNo).paymentStatus` to see it.
1756
+ */
1757
+ async checkout(code, {
1758
+ cartId,
1759
+ returnUrl,
1760
+ paymentData,
1761
+ metadata
1762
+ } = {}) {
1763
+ const result = await this.place(String(code).toLowerCase(), cartId, {
1764
+ ...returnUrl ? { return_url: returnUrl } : {},
1765
+ ...paymentData ? { payment_data: paymentData } : {},
1766
+ ...metadata ? { metadata } : {}
1767
+ });
1768
+ const data = result.payment.client_data ?? {};
1769
+ return {
1770
+ orderNo: result.order.number,
1771
+ order: toOrder(result.order),
1772
+ payment: {
1773
+ kind: result.payment.kind,
1774
+ provider: result.payment.provider,
1775
+ reference: result.payment.reference ?? null,
1776
+ clientSecret: data.client_secret ?? null,
1777
+ redirectUrl: data.url ?? null,
1778
+ clientData: data
1779
+ }
1780
+ };
1781
+ }
1782
+ /** Every payment method the engine has switched on, by engine code and name — drivable here or not. */
1783
+ async listMethods() {
1784
+ const options = await this.data(
1785
+ "/api/checkout"
1786
+ );
1787
+ return { methods: options.methods ?? [], currency: options.currency };
1788
+ }
1789
+ async redirectGateway() {
1790
+ const { methods } = await this.listMethods();
1791
+ return methods.find((m) => REDIRECT_GATEWAYS.has(m.code))?.code;
1792
+ }
1686
1793
  /**
1687
1794
  * One checkout attempt.
1688
1795
  *
@@ -1693,27 +1800,28 @@ var CheckoutService = class extends BaseService {
1693
1800
  * request — a different address, a different method — gets a new key, since
1694
1801
  * the engine refuses a key replayed with a different body.
1695
1802
  */
1696
- async place(code, cartId) {
1697
- const id = cartId || storedCartId();
1698
- if (!id) throw new GoCommerceError("Your bag is empty.", 400);
1699
- const extras = cartExtras(id);
1803
+ async place(code, cartId, extra = {}) {
1804
+ const id2 = cartId || storedCartId();
1805
+ if (!id2) throw new GoCommerceError("Your bag is empty.", 400);
1806
+ const extras = cartExtras(id2);
1700
1807
  const ship = extras.shippingAddress;
1701
1808
  if (!ship) throw new GoCommerceError("Add a delivery address first.", 400);
1702
- const cart = await this.data(`/api/carts/${encodeURIComponent(id)}`);
1809
+ const cart = await this.data(`/api/carts/${encodeURIComponent(id2)}`);
1703
1810
  const email = cart.email || (ship.email ? String(ship.email) : "");
1704
1811
  if (!email) throw new GoCommerceError("Add an email address so we can send your receipt.", 400);
1705
1812
  const address = toEngineAddress(ship);
1706
1813
  const body = {
1707
- cart_id: id,
1814
+ cart_id: id2,
1708
1815
  email,
1709
1816
  ...extras.phone || address.phone ? { phone: String(extras.phone || address.phone) } : {},
1710
1817
  ...address.name ? { name: address.name } : {},
1711
1818
  address,
1712
- ...extras.shippingRateId ? { shipping_rate_id: Number(extras.shippingRateId) } : {}
1819
+ ...extras.shippingRateId ? { shipping_rate_id: Number(extras.shippingRateId) } : {},
1820
+ ...extra
1713
1821
  };
1714
1822
  const serialised = JSON.stringify(body);
1715
1823
  const key = extras.attempt?.body === `${code}:${serialised}` ? extras.attempt.key : newKey();
1716
- saveCartExtras(id, { attempt: { key, body: `${code}:${serialised}` } });
1824
+ saveCartExtras(id2, { attempt: { key, body: `${code}:${serialised}` } });
1717
1825
  const result = await this.data(`/api/checkout/${encodeURIComponent(code)}`, {
1718
1826
  method: "POST",
1719
1827
  body,
@@ -1723,7 +1831,7 @@ var CheckoutService = class extends BaseService {
1723
1831
  rememberOrder({
1724
1832
  number: result.order.number,
1725
1833
  token: result.order.access_token,
1726
- cartId: id,
1834
+ cartId: id2,
1727
1835
  reference: result.payment.client_data?.razorpay_order_id ?? result.payment.reference
1728
1836
  });
1729
1837
  if (sessionToken()) {
@@ -1734,10 +1842,11 @@ var CheckoutService = class extends BaseService {
1734
1842
  }).catch(() => void 0);
1735
1843
  }
1736
1844
  }
1737
- saveCartExtras(id, { attempt: null });
1845
+ saveCartExtras(id2, { attempt: null });
1738
1846
  return result;
1739
1847
  }
1740
1848
  };
1849
+ var REDIRECT_GATEWAYS = /* @__PURE__ */ new Set(["adyen", "paddle", "lemonsqueezy", "creem", "hyperswitch", "revenuecat"]);
1741
1850
  var unsupported = (method, why) => new NotSupportedError("CheckoutService", method, why);
1742
1851
  function newKey() {
1743
1852
  const c = globalThis.crypto;
@@ -1757,23 +1866,25 @@ var PaymentMethodService = class extends BaseService {
1757
1866
  async list(_args = {}) {
1758
1867
  const options = await this.facts.checkoutOptions();
1759
1868
  const methods = options.methods ?? [];
1869
+ const redirect = methods.find((m) => REDIRECT_GATEWAYS.has(m.code))?.code;
1870
+ const drive = (code) => DRIVABLE[code] ?? (code === redirect ? { code: "PAYPAL", img: "/payment/card.svg" } : void 0);
1760
1871
  const data = methods.filter((m) => {
1761
- if (DRIVABLE[m.code]) return true;
1872
+ if (drive(m.code)) return true;
1762
1873
  if (!reported2.has(m.code)) {
1763
1874
  reported2.add(m.code);
1764
1875
  console.warn(
1765
- `[gocommerce] payment method "${m.code}" is switched on but this storefront cannot drive its flow, so it is not offered.`
1876
+ `[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.`
1766
1877
  );
1767
1878
  }
1768
1879
  return false;
1769
1880
  }).map((m, i) => ({
1770
1881
  id: m.code,
1771
- code: DRIVABLE[m.code].code,
1882
+ code: drive(m.code).code,
1772
1883
  engineCode: m.code,
1773
1884
  name: m.name,
1774
1885
  title: m.name,
1775
1886
  description: "",
1776
- img: DRIVABLE[m.code].img ?? null,
1887
+ img: drive(m.code).img ?? null,
1777
1888
  rank: i,
1778
1889
  active: true,
1779
1890
  isActive: true
@@ -1848,7 +1959,7 @@ var OrderService = class extends BaseService {
1848
1959
  productId: l.product_id,
1849
1960
  variantId: l.variant_id,
1850
1961
  title: l.title,
1851
- img: l.image_url ?? null,
1962
+ img: mediaUrl(l.image_url),
1852
1963
  price: toMajor(l.unit_price),
1853
1964
  qty: l.quantity
1854
1965
  });
@@ -1944,6 +2055,42 @@ var UserService = class extends BaseService {
1944
2055
  startSession(auth);
1945
2056
  return toUser(auth.record);
1946
2057
  }
2058
+ /**
2059
+ * Mails the signed-in shopper a link to confirm their address. The engine
2060
+ * sends none at signup, and a confirmed address is what customer-group
2061
+ * prices are keyed on. 409 once confirmed; 429 within a minute of the last.
2062
+ */
2063
+ async requestEmailVerification() {
2064
+ return this.account("/x/identity/me/email-verification", { method: "POST", auth: true });
2065
+ }
2066
+ /** Extends the session another thirty days. The token stays the same. */
2067
+ async refreshSession() {
2068
+ return startSession(await this.account("/x/identity/refresh", { method: "POST", auth: true }));
2069
+ }
2070
+ /**
2071
+ * Files a guest order under the signed-in account, so it appears in their
2072
+ * order history. The order's access token is the proof of ownership: pass
2073
+ * it, or leave it out for an order placed from this browser, whose token was
2074
+ * kept at checkout.
2075
+ *
2076
+ * Never done automatically at sign-in. On a shared computer that would file
2077
+ * one person's orders — address and all — under whoever signed in next.
2078
+ */
2079
+ async claimOrder({ number, token }) {
2080
+ const proof = token ?? placedOrder(number)?.token;
2081
+ if (!proof) {
2082
+ throw new GoCommerceError(
2083
+ "This order was not placed from this browser; enter the code from its confirmation.",
2084
+ 400
2085
+ );
2086
+ }
2087
+ const order = await this.account("/x/identity/me/orders", {
2088
+ method: "POST",
2089
+ auth: true,
2090
+ body: { number: String(number), token: proof }
2091
+ });
2092
+ return toOrder(order);
2093
+ }
1947
2094
  async deleteUser() {
1948
2095
  throw new NotSupportedError("UserService", "deleteUser", "an account is closed by the store, on request");
1949
2096
  }
@@ -2041,16 +2188,16 @@ var AddressService = class extends BaseService {
2041
2188
  page: env.meta?.page ?? page
2042
2189
  };
2043
2190
  }
2044
- async fetchAddress(id) {
2191
+ async fetchAddress(id2) {
2045
2192
  return fromIdentityAddress(
2046
- await this.data(`/x/identity/me/addresses/${enc(id)}`, { auth: true }),
2193
+ await this.data(`/x/identity/me/addresses/${enc(id2)}`, { auth: true }),
2047
2194
  currentMe()?.email
2048
2195
  );
2049
2196
  }
2050
2197
  /** New when it has no id (or the form's `'new'`), an edit when it has one — the storefront saves both here. */
2051
2198
  async saveAddress(address) {
2052
- const id = address?.id && address.id !== "new" ? String(address.id) : null;
2053
- if (id) return this.editAddress(id, address);
2199
+ const id2 = address?.id && address.id !== "new" ? String(address.id) : null;
2200
+ if (id2) return this.editAddress(id2, address);
2054
2201
  const a = await this.data("/x/identity/me/addresses", {
2055
2202
  method: "POST",
2056
2203
  auth: true,
@@ -2058,20 +2205,20 @@ var AddressService = class extends BaseService {
2058
2205
  });
2059
2206
  return fromIdentityAddress(a, currentMe()?.email);
2060
2207
  }
2061
- async editAddress(id, address) {
2062
- const a = await this.data(`/x/identity/me/addresses/${enc(id)}`, {
2208
+ async editAddress(id2, address) {
2209
+ const a = await this.data(`/x/identity/me/addresses/${enc(id2)}`, {
2063
2210
  method: "PATCH",
2064
2211
  auth: true,
2065
2212
  body: toIdentityAddress(address)
2066
2213
  });
2067
2214
  return fromIdentityAddress(a, currentMe()?.email);
2068
2215
  }
2069
- async deleteAddress(id) {
2070
- await this.engine(`/x/identity/me/addresses/${enc(id)}`, { method: "DELETE", auth: true });
2071
- return { id: String(id), deleted: true };
2216
+ async deleteAddress(id2) {
2217
+ await this.engine(`/x/identity/me/addresses/${enc(id2)}`, { method: "DELETE", auth: true });
2218
+ return { id: String(id2), deleted: true };
2072
2219
  }
2073
2220
  };
2074
- var enc = (id) => encodeURIComponent(String(id));
2221
+ var enc = (id2) => encodeURIComponent(String(id2));
2075
2222
  var addressService = new AddressService();
2076
2223
 
2077
2224
  // src/services/wishlist-service.ts
@@ -2123,6 +2270,18 @@ var WishlistService = class extends BaseService {
2123
2270
  const saved = new Set((list?.items ?? []).map((i) => String(i.product_id)));
2124
2271
  return (pairs ?? []).map((p) => ({ ...p, exists: saved.has(String(p.productId)) }));
2125
2272
  }
2273
+ /**
2274
+ * The address the store may write to when a saved product is back in stock
2275
+ * or on sale; `''` takes it off. A list that does not exist yet is made.
2276
+ */
2277
+ async setEmail(email) {
2278
+ const list = await this.current() ?? await this.create();
2279
+ await this.data(`/x/wishlist/${enc2(list.token)}`, {
2280
+ method: "PATCH",
2281
+ body: { email: String(email ?? "").trim() }
2282
+ });
2283
+ return { email: String(email ?? "").trim() || null };
2284
+ }
2126
2285
  async current() {
2127
2286
  const token = wishlistToken();
2128
2287
  if (!token) return null;
@@ -2205,6 +2364,15 @@ var MenuService = class extends BaseService {
2205
2364
  if (menus) return { data: menus };
2206
2365
  return resolveRestLocally("get", "/api/menu");
2207
2366
  }
2367
+ /** Every menu the store keeps, by handle — not only the header and footer. */
2368
+ async listMenus() {
2369
+ return await this.data("/x/navigation/menus") ?? [];
2370
+ }
2371
+ /** One menu by handle, in the storefront's shape: `{ menuId, name, items: [{ id, name, link, items }] }`. */
2372
+ async getMenu(handle) {
2373
+ const m = await this.data(`/x/navigation/menus/${encodeURIComponent(handle)}`);
2374
+ return { menuId: m.handle, name: m.title, items: (m.items ?? []).map(toItem) };
2375
+ }
2208
2376
  /** The header and footer menus, or `null` when the module is not there. Cached: a menu is store-level. */
2209
2377
  async engineMenus() {
2210
2378
  if (!await this.facts.pluginOn("navigation")) return null;
@@ -2226,6 +2394,24 @@ var readStaticStore = async () => {
2226
2394
  return {};
2227
2395
  };
2228
2396
 
2397
+ // src/services/newsletter-service.ts
2398
+ var NewsletterService = class extends BaseService {
2399
+ async subscribe({ email, name, source }) {
2400
+ return this.data("/x/newsletter/subscribe", {
2401
+ method: "POST",
2402
+ body: { email: String(email ?? "").trim(), ...name ? { name } : {}, ...source ? { source } : {} }
2403
+ });
2404
+ }
2405
+ /** The token from the unsubscribe link in a newsletter email. */
2406
+ async unsubscribe(token) {
2407
+ return this.data("/x/newsletter/unsubscribe", {
2408
+ method: "POST",
2409
+ body: { token: String(token ?? "") }
2410
+ });
2411
+ }
2412
+ };
2413
+ var newsletterService = new NewsletterService();
2414
+
2229
2415
  // src/services/store-service.ts
2230
2416
  function symbolFor(code) {
2231
2417
  try {
@@ -2259,6 +2445,22 @@ var StoreService = class extends BaseService {
2259
2445
  }
2260
2446
  return super.get(path);
2261
2447
  }
2448
+ /**
2449
+ * The storefront's newsletter box posts `{ email }` to Litekart's
2450
+ * `/api/newsletter/subscribe` through this verb; ext/newsletter is where
2451
+ * that list lives here.
2452
+ */
2453
+ async post(path, data) {
2454
+ if (path.split("?")[0] === "/api/newsletter/subscribe") {
2455
+ const { email, name } = data ?? {};
2456
+ return await new NewsletterService(this._fetch).subscribe({
2457
+ email: email ?? "",
2458
+ name,
2459
+ source: "storefront"
2460
+ });
2461
+ }
2462
+ return super.post(path, data);
2463
+ }
2262
2464
  async live() {
2263
2465
  const [ready, plugins, menus, base] = await Promise.all([
2264
2466
  this.facts.ready(),
@@ -2564,6 +2766,300 @@ var varniCustomProductService = new VarniCustomProductService();
2564
2766
  var pluginsService = pluginService;
2565
2767
  var settingsService = settingService;
2566
2768
 
2567
- export { AddressService, AuthService, AutocompleteService, BannerService, BaseService, BlogService, CartService, CatalogService, CategoryService, ChatService, CheckoutService, CollectionService, ContactService, CountryService, CouponService, CurrencyService, DealService, DemoRequestService, EnquiryService, FaqService, FeedbackService, GalleryService, HomeService, InitService, MeilisearchService, MenuService, 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, collectionService, contactService, countryService, couponService, currencyService, dealService, demoRequestService, emptyPage, enquiryService, faqService, feedbackService, galleryService, homeService, initService, meilisearchService, menuService, orderService, pageService, paymentMethodService, pluginService, pluginsService, popularSearchService, popularityService, productService, profileService, qs, reelsService, regionService, reviewService, searchService, settingService, settingsService, stateService, staticStore, storeService, toPage, uploadService, userService, varniCustomDesignService, varniCustomProductService, vendorService, warrantyService, wishlistService };
2769
+ // src/services/variant-service.ts
2770
+ var VariantService = class extends BaseService {
2771
+ /** A product's variants, inactive ones included (filter on `active`), in the merchant's order. */
2772
+ async list(productId, { page = 1, limit = 50 } = {}) {
2773
+ const env = await this.engine("/api/variants", { query: { product_id: productId, page, limit } });
2774
+ return { data: (env.data ?? []).map(withAmounts), meta: env.meta };
2775
+ }
2776
+ async getOne(id2) {
2777
+ return withAmounts(await this.data(`/api/variants/${encodeURIComponent(String(id2))}`));
2778
+ }
2779
+ /**
2780
+ * What each approved seller asks for this variant, cheapest first. A cart
2781
+ * line takes a variant, not an offer, so this informs a shopper rather than
2782
+ * choosing who fulfils.
2783
+ */
2784
+ async offers(variantId) {
2785
+ const offers = await this.data(`/api/variants/${encodeURIComponent(String(variantId))}/offers`) ?? [];
2786
+ return offers.map((o) => ({ ...o, price: { ...o.price, amount: toMajor(o.price) } }));
2787
+ }
2788
+ };
2789
+ function withAmounts(v) {
2790
+ return {
2791
+ ...v,
2792
+ price: { ...v.price, amount: toMajor(v.price) },
2793
+ compare_at_price: v.compare_at_price ? { ...v.compare_at_price, amount: toMajor(v.compare_at_price) } : void 0
2794
+ };
2795
+ }
2796
+ var variantService = new VariantService();
2797
+
2798
+ // src/services/b2b-service.ts
2799
+ var B2bService = class extends BaseService {
2800
+ // ------------------------------------------------------------ company
2801
+ /** The company, the caller's role in it, and its credit: limit, outstanding, available, overdue. */
2802
+ async me() {
2803
+ const me = await this.data("/x/b2b/me", { auth: true });
2804
+ const { notes: _notes, metadata: _metadata, ...company } = me.company;
2805
+ return { company, role: me.role, credit: me.credit };
2806
+ }
2807
+ async members() {
2808
+ return await this.data("/x/b2b/members", { auth: true }) ?? [];
2809
+ }
2810
+ /** Admin only. Demoting the last admin is refused (409). */
2811
+ async setMemberRole(customerId, role) {
2812
+ return this.data(`/x/b2b/members/${id(customerId)}`, { method: "PATCH", auth: true, body: { role } });
2813
+ }
2814
+ /** An admin removes anyone; any member may remove themselves. */
2815
+ async removeMember(customerId) {
2816
+ await this.engine(`/x/b2b/members/${id(customerId)}`, { method: "DELETE", auth: true });
2817
+ }
2818
+ // ------------------------------------------------------------ invitations
2819
+ /** Admin only. Open invitations: not accepted, not expired. */
2820
+ async invitations() {
2821
+ return await this.data("/x/b2b/invitations", { auth: true }) ?? [];
2822
+ }
2823
+ /** Admin only. Emails a link; a second invitation to the same address replaces the first. */
2824
+ async invite({ email, role }) {
2825
+ return this.data("/x/b2b/invitations", {
2826
+ method: "POST",
2827
+ auth: true,
2828
+ body: { email: String(email ?? "").trim(), role }
2829
+ });
2830
+ }
2831
+ async withdrawInvitation(invitationId) {
2832
+ await this.engine(`/x/b2b/invitations/${id(invitationId)}`, { method: "DELETE", auth: true });
2833
+ }
2834
+ /**
2835
+ * The token from the invitation email, accepted by the account it was sent
2836
+ * to — which also confirms that account's email address.
2837
+ */
2838
+ async acceptInvitation(token) {
2839
+ return this.data("/x/b2b/invitations/accept", {
2840
+ method: "POST",
2841
+ auth: true,
2842
+ body: { token: String(token ?? "").trim() }
2843
+ });
2844
+ }
2845
+ // ------------------------------------------------------------ filling a basket
2846
+ /**
2847
+ * A quick order: up to 500 lines by SKU or variant id, into the bag (or a new
2848
+ * one), at the company's prices. A line that will not go in is reported in
2849
+ * `rejected` with its reason; the rest are added.
2850
+ */
2851
+ async addLines({
2852
+ cartId,
2853
+ lines
2854
+ }) {
2855
+ const res = await this.data("/x/b2b/cart/lines", {
2856
+ method: "POST",
2857
+ auth: true,
2858
+ body: {
2859
+ ...cartId ?? storedCartId() ? { cart_id: cartId ?? storedCartId() } : {},
2860
+ lines: (lines ?? []).map((l) => ({
2861
+ ...l.variantId ? { variant_id: Number(l.variantId) } : {},
2862
+ ...l.sku ? { sku: String(l.sku).trim() } : {},
2863
+ quantity: Number(l.quantity)
2864
+ }))
2865
+ }
2866
+ });
2867
+ rememberCartId(res.cart.id);
2868
+ return { cart: toCart(res.cart, cartExtras(res.cart.id)), rejected: res.rejected ?? [] };
2869
+ }
2870
+ /** A past order's lines in a new bag, at today's prices. Nothing is placed. */
2871
+ async reorder(orderId) {
2872
+ const res = await this.data(
2873
+ `/x/b2b/orders/${id(orderId)}/reorder`,
2874
+ {
2875
+ method: "POST",
2876
+ auth: true
2877
+ }
2878
+ );
2879
+ rememberCartId(res.cart.id);
2880
+ return { cart: toCart(res.cart, cartExtras(res.cart.id)), rejected: res.rejected ?? [] };
2881
+ }
2882
+ // ------------------------------------------------------------ placing an order
2883
+ /**
2884
+ * Places the bag through the company: on account by default, with a PO
2885
+ * number, optionally only some lines (`lineIds`). A buyer over the company's
2886
+ * approval threshold gets `{ kind: 'approval' }` instead of an order — the
2887
+ * bag stays as it was, and an approver places it later.
2888
+ *
2889
+ * Over the credit limit is 403 `credit_limit_exceeded`; `me().credit` says
2890
+ * how much is available before the shopper tries.
2891
+ */
2892
+ async checkout({
2893
+ cartId,
2894
+ lineIds,
2895
+ shippingRateId,
2896
+ returnUrl,
2897
+ paymentData,
2898
+ ...opts
2899
+ } = {}) {
2900
+ const cart = cartId || storedCartId();
2901
+ if (!cart) throw new GoCommerceError("Your bag is empty.", 400);
2902
+ const extras = cartExtras(cart);
2903
+ const rate = shippingRateId ?? extras.shippingRateId;
2904
+ const body = {
2905
+ cart_id: cart,
2906
+ ...this.placeBody(opts, extras.shippingAddress ?? void 0),
2907
+ ...rate ? { shipping_rate_id: Number(rate) } : {},
2908
+ ...lineIds?.length ? { line_ids: lineIds.map(Number) } : {},
2909
+ ...returnUrl ? { return_url: returnUrl } : {},
2910
+ ...paymentData ? { payment_data: paymentData } : {}
2911
+ };
2912
+ const res = await this.data("/x/b2b/checkout", {
2913
+ method: "POST",
2914
+ auth: true,
2915
+ body,
2916
+ headers: { "Idempotency-Key": attemptKey(`b2b:${JSON.stringify(body)}`) }
2917
+ });
2918
+ return this.placed(res, cart);
2919
+ }
2920
+ // ------------------------------------------------------------ the company's orders
2921
+ /** Admins and approvers see the company's; a buyer sees their own. `q` matches the number, PO or email. */
2922
+ async orders({
2923
+ page = 1,
2924
+ limit = 20,
2925
+ q,
2926
+ overdue
2927
+ } = {}) {
2928
+ return this.engine("/x/b2b/orders", {
2929
+ auth: true,
2930
+ query: { page, limit, q, ...overdue ? { overdue: "true" } : {} }
2931
+ });
2932
+ }
2933
+ // ------------------------------------------------------------ approvals
2934
+ async approvals({
2935
+ page = 1,
2936
+ limit = 20,
2937
+ status
2938
+ } = {}) {
2939
+ return this.engine("/x/b2b/approvals", { auth: true, query: { page, limit, status } });
2940
+ }
2941
+ async approval(approvalId) {
2942
+ return this.data(`/x/b2b/approvals/${id(approvalId)}`, { auth: true });
2943
+ }
2944
+ /**
2945
+ * Admin or approver: places the requested order at the prices it was
2946
+ * requested at. Credit limits and holds still apply. The payment intent is
2947
+ * the approver's to act on — for an account order there is nothing to do.
2948
+ */
2949
+ async approve(approvalId) {
2950
+ const res = await this.data(`/x/b2b/approvals/${id(approvalId)}/approve`, {
2951
+ method: "POST",
2952
+ auth: true
2953
+ });
2954
+ return { approval: res.approval, orderNo: res.order.number, order: toOrder(res.order), payment: res.payment };
2955
+ }
2956
+ async reject(approvalId, reason) {
2957
+ return this.data(`/x/b2b/approvals/${id(approvalId)}/reject`, {
2958
+ method: "POST",
2959
+ auth: true,
2960
+ ...reason ? { body: { reason } } : {}
2961
+ });
2962
+ }
2963
+ /** The buyer who asked withdraws the request. */
2964
+ async cancelApproval(approvalId) {
2965
+ return this.data(`/x/b2b/approvals/${id(approvalId)}/cancel`, { method: "POST", auth: true });
2966
+ }
2967
+ // ------------------------------------------------------------ quotes
2968
+ async quotes({ page = 1, limit = 20, status } = {}) {
2969
+ return this.engine("/x/b2b/quotes", { auth: true, query: { page, limit, status } });
2970
+ }
2971
+ async quote(quoteId) {
2972
+ return this.data(`/x/b2b/quotes/${id(quoteId)}`, { auth: true });
2973
+ }
2974
+ /** Asks the store to price these lines. The store answers with unit prices and an expiry. */
2975
+ async requestQuote({ lines, note }) {
2976
+ return this.data("/x/b2b/quotes", {
2977
+ method: "POST",
2978
+ auth: true,
2979
+ body: {
2980
+ lines: (lines ?? []).map((l) => ({ variant_id: Number(l.variantId), quantity: Number(l.quantity) })),
2981
+ ...note ? { note } : {}
2982
+ }
2983
+ });
2984
+ }
2985
+ /** Places the quote at its agreed prices — or, for a buyer over the threshold, asks for approval. */
2986
+ async acceptQuote(quoteId, opts = {}) {
2987
+ const res = await this.data(`/x/b2b/quotes/${id(quoteId)}/accept`, {
2988
+ method: "POST",
2989
+ auth: true,
2990
+ body: this.placeBody(opts)
2991
+ });
2992
+ return this.placed(res);
2993
+ }
2994
+ async declineQuote(quoteId) {
2995
+ return this.data(`/x/b2b/quotes/${id(quoteId)}/decline`, { method: "POST", auth: true });
2996
+ }
2997
+ // ------------------------------------------------------------ dealer leads
2998
+ /**
2999
+ * The public "find a dealer" form: no account needed. The engine routes it to
3000
+ * the dealer whose territory covers the address — or keeps it — and answers
3001
+ * the same either way, so the form never reveals who got it.
3002
+ */
3003
+ async submitLead(lead) {
3004
+ return this.data("/x/b2b/leads", {
3005
+ method: "POST",
3006
+ body: {
3007
+ ...lead.name ? { name: lead.name } : {},
3008
+ ...lead.email ? { email: lead.email } : {},
3009
+ ...lead.phone ? { phone: lead.phone } : {},
3010
+ ...lead.message ? { message: lead.message } : {},
3011
+ ...lead.country ? { country: lead.country } : {},
3012
+ ...lead.state ? { state: lead.state } : {},
3013
+ ...lead.postalCode ? { postal_code: lead.postalCode } : {},
3014
+ ...lead.productId ? { product_id: Number(lead.productId) } : {},
3015
+ ...lead.variantId ? { variant_id: Number(lead.variantId) } : {},
3016
+ ...lead.source ? { source: lead.source } : {}
3017
+ }
3018
+ });
3019
+ }
3020
+ /** A dealer's admins and approvers: the leads routed to their company. */
3021
+ async leads({ page = 1, limit = 20, status } = {}) {
3022
+ return this.engine("/x/b2b/leads", { auth: true, query: { page, limit, status } });
3023
+ }
3024
+ async setLeadStatus(leadId, status) {
3025
+ return this.data(`/x/b2b/leads/${id(leadId)}`, { method: "PATCH", auth: true, body: { status } });
3026
+ }
3027
+ // ------------------------------------------------------------ internals
3028
+ /** The fields checkout and quote acceptance share. Only declared ones: the engine refuses the rest. */
3029
+ placeBody(opts, fallback) {
3030
+ const raw = opts.address ?? fallback;
3031
+ if (!raw) throw new GoCommerceError("Add a delivery address first.", 400);
3032
+ const address = "line1" in raw ? raw : toEngineAddress(raw);
3033
+ return {
3034
+ address,
3035
+ ...opts.paymentMethod ? { payment_method: String(opts.paymentMethod).toLowerCase() } : {},
3036
+ ...opts.poNumber ? { po_number: String(opts.poNumber).trim() } : {},
3037
+ ...opts.name ?? address.name ? { name: opts.name ?? address.name } : {},
3038
+ ...opts.phone ?? address.phone ? { phone: opts.phone ?? address.phone } : {}
3039
+ };
3040
+ }
3041
+ placed(res, cartId) {
3042
+ if (res.approval && !res.order) return { kind: "approval", approval: res.approval };
3043
+ const order = res.order;
3044
+ if (order.access_token && cartId) {
3045
+ rememberOrder({ number: order.number, token: order.access_token, cartId, reference: res.payment?.reference });
3046
+ }
3047
+ return { kind: "order", orderNo: order.number, order: toOrder(order), payment: res.payment };
3048
+ }
3049
+ };
3050
+ var id = (v) => encodeURIComponent(String(v));
3051
+ var keys = /* @__PURE__ */ new Map();
3052
+ function attemptKey(request) {
3053
+ let key = keys.get(request);
3054
+ if (!key) {
3055
+ const c = globalThis.crypto;
3056
+ key = c?.randomUUID?.() ?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
3057
+ keys.set(request, key);
3058
+ }
3059
+ return key;
3060
+ }
3061
+ var b2bService = new B2bService();
3062
+
3063
+ export { AddressService, AuthService, AutocompleteService, B2bService, BannerService, BaseService, BlogService, CartService, CatalogService, CategoryService, ChatService, CheckoutService, CollectionService, ContactService, CountryService, CouponService, CurrencyService, DealService, DemoRequestService, EnquiryService, FaqService, FeedbackService, GalleryService, HomeService, InitService, MeilisearchService, MenuService, NewsletterService, 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, collectionService, contactService, countryService, couponService, currencyService, dealService, demoRequestService, emptyPage, enquiryService, faqService, feedbackService, galleryService, homeService, initService, mediaUrl, meilisearchService, menuService, newsletterService, orderService, pageService, paymentMethodService, pluginService, pluginsService, popularSearchService, popularityService, productService, profileService, qs, reelsService, regionService, reviewService, searchService, settingService, settingsService, stateService, staticStore, storeService, toPage, uploadService, userService, variantService, varniCustomDesignService, varniCustomProductService, vendorService, warrantyService, wishlistService };
2568
3064
  //# sourceMappingURL=index.js.map
2569
3065
  //# sourceMappingURL=index.js.map