@misiki/gocommerce-connector 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -18
- package/dist/{index-CJ836ZI3.d.cts → index-BfbB3jQb.d.cts} +1032 -4
- package/dist/{index-CJ836ZI3.d.ts → index-BfbB3jQb.d.ts} +1032 -4
- package/dist/index.cjs +582 -86
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +576 -87
- package/dist/index.js.map +1 -1
- package/dist/services/index.cjs +582 -86
- package/dist/services/index.cjs.map +1 -1
- package/dist/services/index.d.cts +1 -1
- package/dist/services/index.d.ts +1 -1
- package/dist/services/index.js +576 -87
- package/dist/services/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -190,6 +190,16 @@ function toVariant(v, p, optionless) {
|
|
|
190
190
|
metadata: v.metadata ?? null
|
|
191
191
|
};
|
|
192
192
|
}
|
|
193
|
+
function attributesOf(p) {
|
|
194
|
+
const answers = p.metadata?.category;
|
|
195
|
+
if (!answers || typeof answers !== "object" || Array.isArray(answers)) return [];
|
|
196
|
+
return Object.entries(answers).flatMap(([handle, raw]) => {
|
|
197
|
+
const values = (Array.isArray(raw) ? raw : [raw]).map((v) => String(v ?? "").trim()).filter(Boolean);
|
|
198
|
+
if (!values.length) return [];
|
|
199
|
+
const name = handle.replace(/[-_]+/g, " ").replace(/^./, (c) => c.toUpperCase());
|
|
200
|
+
return [{ name, value: values.join(", "), handle }];
|
|
201
|
+
});
|
|
202
|
+
}
|
|
193
203
|
function toProduct(p, extra = {}) {
|
|
194
204
|
const live = liveVariants(p);
|
|
195
205
|
const lead = live[0] ?? arr(p.variants)[0];
|
|
@@ -255,7 +265,7 @@ function toProduct(p, extra = {}) {
|
|
|
255
265
|
rating: null,
|
|
256
266
|
ratingCount: 0,
|
|
257
267
|
reviewCount: 0,
|
|
258
|
-
attributes:
|
|
268
|
+
attributes: attributesOf(p),
|
|
259
269
|
metadata: p.metadata ?? null,
|
|
260
270
|
createdAt: p.created_at,
|
|
261
271
|
updatedAt: p.updated_at
|
|
@@ -748,7 +758,7 @@ function storedCartId() {
|
|
|
748
758
|
if (!v || v === "undefined" || v === "null") return null;
|
|
749
759
|
return v;
|
|
750
760
|
}
|
|
751
|
-
var rememberCartId = (
|
|
761
|
+
var rememberCartId = (id2) => writeLocal("cart_id", id2);
|
|
752
762
|
var extrasKey = (cartId) => `gocommerce_cart_${cartId}`;
|
|
753
763
|
var cartExtras = (cartId) => readJSON(extrasKey(cartId)) ?? {};
|
|
754
764
|
function saveCartExtras(cartId, patch) {
|
|
@@ -816,7 +826,8 @@ var BaseService = class _BaseService {
|
|
|
816
826
|
* as what it is rather than as a JSON parse error three frames away.
|
|
817
827
|
*/
|
|
818
828
|
async engine(path, req = {}) {
|
|
819
|
-
const
|
|
829
|
+
const lang = (req.method ?? "GET") === "GET" ? this.creds.language : void 0;
|
|
830
|
+
const url = this.engineURL(path, lang ? { lang, ...req.query } : req.query);
|
|
820
831
|
const headers = { Accept: "application/json", ...req.headers };
|
|
821
832
|
if (req.body !== void 0) headers["Content-Type"] = "application/json";
|
|
822
833
|
if (this.creds.store) headers["X-Store"] = this.creds.store;
|
|
@@ -1099,8 +1110,11 @@ var CatalogService = class extends BaseService {
|
|
|
1099
1110
|
sorted
|
|
1100
1111
|
};
|
|
1101
1112
|
}
|
|
1113
|
+
const attr = Object.entries(q.attributes ?? {}).flatMap(
|
|
1114
|
+
([handle, values]) => values.filter(Boolean).map((v) => `${handle}:${v}`)
|
|
1115
|
+
);
|
|
1102
1116
|
const env = await this.engine("/api/products", {
|
|
1103
|
-
query: { q: search, page, limit, channel: this.creds.channel }
|
|
1117
|
+
query: { q: search, page, limit, channel: this.creds.channel, attr }
|
|
1104
1118
|
});
|
|
1105
1119
|
return { ...toPage(env, (p) => toProduct(p)), categoryHierarchy: hierarchy, sorted };
|
|
1106
1120
|
}
|
|
@@ -1235,6 +1249,16 @@ var num = (v) => {
|
|
|
1235
1249
|
const n = Number(v);
|
|
1236
1250
|
return Number.isFinite(n) ? n : void 0;
|
|
1237
1251
|
};
|
|
1252
|
+
function attributesFrom(p) {
|
|
1253
|
+
const out = {};
|
|
1254
|
+
for (const [key, value] of p) {
|
|
1255
|
+
if (!key.startsWith("attributes.")) continue;
|
|
1256
|
+
const handle = key.slice("attributes.".length);
|
|
1257
|
+
const values = value.split(",").map((v) => decodeURIComponent(v).trim()).filter(Boolean);
|
|
1258
|
+
if (handle && values.length) out[handle] = [...out[handle] ?? [], ...values];
|
|
1259
|
+
}
|
|
1260
|
+
return Object.keys(out).length ? out : void 0;
|
|
1261
|
+
}
|
|
1238
1262
|
var SearchService = class extends CatalogService {
|
|
1239
1263
|
emptyResult() {
|
|
1240
1264
|
return {
|
|
@@ -1256,7 +1280,8 @@ var SearchService = class extends CatalogService {
|
|
|
1256
1280
|
sort: p.get("sort") ?? void 0,
|
|
1257
1281
|
category: slug || p.get("categories")?.split(",")[0] || void 0,
|
|
1258
1282
|
priceFrom: num(p.get("priceFrom")) ?? num(from ?? null),
|
|
1259
|
-
priceTo: num(p.get("priceTo")) ?? num(to ?? null)
|
|
1283
|
+
priceTo: num(p.get("priceTo")) ?? num(to ?? null),
|
|
1284
|
+
attributes: attributesFrom(p)
|
|
1260
1285
|
});
|
|
1261
1286
|
return {
|
|
1262
1287
|
data: res.data,
|
|
@@ -1388,10 +1413,10 @@ var REMOVE = -9999999;
|
|
|
1388
1413
|
var CartService = class extends BaseService {
|
|
1389
1414
|
facts = new StoreFacts(this._fetch);
|
|
1390
1415
|
async getCartByCartId(cartId) {
|
|
1391
|
-
const
|
|
1392
|
-
if (!
|
|
1416
|
+
const id2 = clean(cartId);
|
|
1417
|
+
if (!id2) return emptyCart(await this.currency());
|
|
1393
1418
|
try {
|
|
1394
|
-
return await this.present(await this.data(`/api/carts/${encodeURIComponent(
|
|
1419
|
+
return await this.present(await this.data(`/api/carts/${encodeURIComponent(id2)}`));
|
|
1395
1420
|
} catch (err) {
|
|
1396
1421
|
if (err instanceof GoCommerceError && err.status === 404) return emptyCart(await this.currency());
|
|
1397
1422
|
throw err;
|
|
@@ -1406,9 +1431,9 @@ var CartService = class extends BaseService {
|
|
|
1406
1431
|
* to clear the bag on the success page.
|
|
1407
1432
|
*/
|
|
1408
1433
|
async refereshCart() {
|
|
1409
|
-
const
|
|
1410
|
-
const cart = await this.getCartByCartId(
|
|
1411
|
-
if (
|
|
1434
|
+
const id2 = storedCartId();
|
|
1435
|
+
const cart = await this.getCartByCartId(id2);
|
|
1436
|
+
if (id2 && cart.status === "converted") forgetCartExtras(id2);
|
|
1412
1437
|
return cart;
|
|
1413
1438
|
}
|
|
1414
1439
|
/**
|
|
@@ -1421,21 +1446,21 @@ var CartService = class extends BaseService {
|
|
|
1421
1446
|
qty = 1,
|
|
1422
1447
|
variantId
|
|
1423
1448
|
}) {
|
|
1424
|
-
let
|
|
1425
|
-
if (lineId &&
|
|
1426
|
-
const cart = await this.data(`/api/carts/${encodeURIComponent(
|
|
1449
|
+
let id2 = clean(cartId) ?? storedCartId();
|
|
1450
|
+
if (lineId && id2) {
|
|
1451
|
+
const cart = await this.data(`/api/carts/${encodeURIComponent(id2)}`);
|
|
1427
1452
|
const line = (cart.line_items ?? []).find((l) => String(l.id) === String(lineId));
|
|
1428
1453
|
if (line) {
|
|
1429
1454
|
const next = qty === REMOVE ? 0 : line.quantity + Number(qty);
|
|
1430
|
-
return this.setQuantity(
|
|
1455
|
+
return this.setQuantity(id2, line.id, next);
|
|
1431
1456
|
}
|
|
1432
1457
|
}
|
|
1433
|
-
if (qty === REMOVE || Number(qty) <= 0) return this.getCartByCartId(
|
|
1458
|
+
if (qty === REMOVE || Number(qty) <= 0) return this.getCartByCartId(id2);
|
|
1434
1459
|
if (!variantId) throw new GoCommerceError("Please choose an option before adding this to your bag.", 400);
|
|
1435
|
-
if (!
|
|
1460
|
+
if (!id2) id2 = await this.open();
|
|
1436
1461
|
try {
|
|
1437
1462
|
return await this.present(
|
|
1438
|
-
await this.data(`/api/carts/${encodeURIComponent(
|
|
1463
|
+
await this.data(`/api/carts/${encodeURIComponent(id2)}/line-items`, {
|
|
1439
1464
|
method: "POST",
|
|
1440
1465
|
body: { variant_id: Number(variantId), quantity: Number(qty) }
|
|
1441
1466
|
})
|
|
@@ -1459,15 +1484,15 @@ var CartService = class extends BaseService {
|
|
|
1459
1484
|
lineId,
|
|
1460
1485
|
qty
|
|
1461
1486
|
}) {
|
|
1462
|
-
const
|
|
1463
|
-
if (!
|
|
1464
|
-
return this.setQuantity(
|
|
1487
|
+
const id2 = clean(cartId) ?? storedCartId();
|
|
1488
|
+
if (!id2) return emptyCart(await this.currency());
|
|
1489
|
+
return this.setQuantity(id2, lineId, Number(qty));
|
|
1465
1490
|
}
|
|
1466
1491
|
async removeCart({ cartId, lineId }) {
|
|
1467
|
-
const
|
|
1468
|
-
if (!
|
|
1492
|
+
const id2 = clean(cartId) ?? storedCartId();
|
|
1493
|
+
if (!id2) return emptyCart(await this.currency());
|
|
1469
1494
|
return this.present(
|
|
1470
|
-
await this.data(`/api/carts/${encodeURIComponent(
|
|
1495
|
+
await this.data(`/api/carts/${encodeURIComponent(id2)}/line-items/${encodeURIComponent(String(lineId))}`, {
|
|
1471
1496
|
method: "DELETE"
|
|
1472
1497
|
})
|
|
1473
1498
|
);
|
|
@@ -1478,8 +1503,8 @@ var CartService = class extends BaseService {
|
|
|
1478
1503
|
* and the rest is kept for the checkout request.
|
|
1479
1504
|
*/
|
|
1480
1505
|
async updateCart2(args) {
|
|
1481
|
-
const
|
|
1482
|
-
if (!
|
|
1506
|
+
const id2 = clean(args.cartId) ?? storedCartId();
|
|
1507
|
+
if (!id2) return emptyCart(await this.currency());
|
|
1483
1508
|
const patch = {};
|
|
1484
1509
|
if (args.phone !== void 0) patch.phone = args.phone || void 0;
|
|
1485
1510
|
if (args.shippingAddress !== void 0) {
|
|
@@ -1489,20 +1514,20 @@ var CartService = class extends BaseService {
|
|
|
1489
1514
|
if (args.billingAddress !== void 0 || args.isBillingAddressSameAsShipping) {
|
|
1490
1515
|
patch.billingAddress = args.isBillingAddressSameAsShipping ? null : args.billingAddress ?? null;
|
|
1491
1516
|
}
|
|
1492
|
-
const before = cartExtras(
|
|
1517
|
+
const before = cartExtras(id2);
|
|
1493
1518
|
const country = (a) => String(a?.countryCode || a?.country || "").toUpperCase();
|
|
1494
1519
|
if (args.shippingAddress !== void 0 && country(args.shippingAddress) !== country(before.shippingAddress)) {
|
|
1495
1520
|
patch.shippingRateId = null;
|
|
1496
1521
|
patch.shippingRate = null;
|
|
1497
1522
|
}
|
|
1498
|
-
saveCartExtras(
|
|
1523
|
+
saveCartExtras(id2, patch);
|
|
1499
1524
|
const email = args.email ?? (args.shippingAddress?.email ? String(args.shippingAddress.email) : void 0);
|
|
1500
1525
|
if (email !== void 0) {
|
|
1501
1526
|
return this.present(
|
|
1502
|
-
await this.data(`/api/carts/${encodeURIComponent(
|
|
1527
|
+
await this.data(`/api/carts/${encodeURIComponent(id2)}/email`, { method: "PUT", body: { email } })
|
|
1503
1528
|
);
|
|
1504
1529
|
}
|
|
1505
|
-
return this.getCartByCartId(
|
|
1530
|
+
return this.getCartByCartId(id2);
|
|
1506
1531
|
}
|
|
1507
1532
|
/**
|
|
1508
1533
|
* A discount code. The engine prices it against the basket now and decides
|
|
@@ -1510,34 +1535,34 @@ var CartService = class extends BaseService {
|
|
|
1510
1535
|
* because the cart itself does not carry the code.
|
|
1511
1536
|
*/
|
|
1512
1537
|
async applyCoupon({ cartId, couponCode }) {
|
|
1513
|
-
const
|
|
1514
|
-
if (!
|
|
1515
|
-
const cart = await this.data(`/api/carts/${encodeURIComponent(
|
|
1516
|
-
const discount = await this.data(`/api/carts/${encodeURIComponent(
|
|
1538
|
+
const id2 = clean(cartId) ?? storedCartId();
|
|
1539
|
+
if (!id2) throw new GoCommerceError("Add something to your bag before applying a code.", 400);
|
|
1540
|
+
const cart = await this.data(`/api/carts/${encodeURIComponent(id2)}`);
|
|
1541
|
+
const discount = await this.data(`/api/carts/${encodeURIComponent(id2)}/discount`, {
|
|
1517
1542
|
method: "PUT",
|
|
1518
1543
|
body: { code: String(couponCode ?? "").trim(), ...cart.email ? { email: cart.email } : {} }
|
|
1519
1544
|
});
|
|
1520
|
-
saveCartExtras(
|
|
1545
|
+
saveCartExtras(id2, { discount: { ...discount, code: discount.code || String(couponCode).trim() } });
|
|
1521
1546
|
return this.present(cart);
|
|
1522
1547
|
}
|
|
1523
1548
|
async removeCoupon(cartId) {
|
|
1524
|
-
const
|
|
1525
|
-
if (!
|
|
1526
|
-
await this.engine(`/api/carts/${encodeURIComponent(
|
|
1527
|
-
saveCartExtras(
|
|
1528
|
-
return this.getCartByCartId(
|
|
1549
|
+
const id2 = clean(cartId) ?? storedCartId();
|
|
1550
|
+
if (!id2) return emptyCart(await this.currency());
|
|
1551
|
+
await this.engine(`/api/carts/${encodeURIComponent(id2)}/discount`, { method: "DELETE" });
|
|
1552
|
+
saveCartExtras(id2, { discount: null });
|
|
1553
|
+
return this.getCartByCartId(id2);
|
|
1529
1554
|
}
|
|
1530
1555
|
/** The delivery option the shopper picked, priced from the engine's own quote. */
|
|
1531
1556
|
async updateShippingRate({ cartId, shippingRateId }) {
|
|
1532
|
-
const
|
|
1533
|
-
if (!
|
|
1534
|
-
const ship = cartExtras(
|
|
1557
|
+
const id2 = clean(cartId) ?? storedCartId();
|
|
1558
|
+
if (!id2) return emptyCart(await this.currency());
|
|
1559
|
+
const ship = cartExtras(id2).shippingAddress;
|
|
1535
1560
|
const country = String(ship?.countryCode || ship?.country || "").toUpperCase();
|
|
1536
1561
|
if (!country) throw new GoCommerceError("Add a delivery address first.", 400);
|
|
1537
|
-
const quote2 = await this.data("/api/checkout/rates", { query: { cart:
|
|
1562
|
+
const quote2 = await this.data("/api/checkout/rates", { query: { cart: id2, country, state: ship?.state ? String(ship.state) : void 0 } });
|
|
1538
1563
|
const rate = (quote2.rates ?? []).find((r) => String(r.rate_id) === String(shippingRateId));
|
|
1539
1564
|
if (!rate) throw new GoCommerceError("That delivery option is not available for this address any more.", 409);
|
|
1540
|
-
saveCartExtras(
|
|
1565
|
+
saveCartExtras(id2, {
|
|
1541
1566
|
shippingRateId: String(rate.rate_id),
|
|
1542
1567
|
shippingRate: {
|
|
1543
1568
|
id: String(rate.rate_id),
|
|
@@ -1546,7 +1571,7 @@ var CartService = class extends BaseService {
|
|
|
1546
1571
|
currency: rate.price.currency
|
|
1547
1572
|
}
|
|
1548
1573
|
});
|
|
1549
|
-
return this.getCartByCartId(
|
|
1574
|
+
return this.getCartByCartId(id2);
|
|
1550
1575
|
}
|
|
1551
1576
|
/** The payment method is chosen at checkout, by which checkout route is called. */
|
|
1552
1577
|
async updateCartPaymentMethod({ cartId }) {
|
|
@@ -1592,25 +1617,25 @@ var CartService = class extends BaseService {
|
|
|
1592
1617
|
return { slug: p.slug, thumbnail: v?.image?.url ?? p.image_url ?? null };
|
|
1593
1618
|
});
|
|
1594
1619
|
}
|
|
1595
|
-
productFor(
|
|
1620
|
+
productFor(id2) {
|
|
1596
1621
|
return remember(
|
|
1597
|
-
`${this.engineBase()}|product|${
|
|
1598
|
-
() => this.data(`/api/products/${
|
|
1622
|
+
`${this.engineBase()}|product|${id2}`,
|
|
1623
|
+
() => this.data(`/api/products/${id2}`).catch(() => null)
|
|
1599
1624
|
);
|
|
1600
1625
|
}
|
|
1601
1626
|
};
|
|
1602
|
-
var clean = (
|
|
1627
|
+
var clean = (id2) => id2 && id2 !== "undefined" && id2 !== "null" ? id2 : null;
|
|
1603
1628
|
var cartService = new CartService();
|
|
1604
1629
|
|
|
1605
1630
|
// src/services/checkout-service.ts
|
|
1606
1631
|
var CheckoutService = class extends BaseService {
|
|
1607
1632
|
/** Delivery options for the address on the cart: `{ data: [{ id, name, base_rate }] }`. */
|
|
1608
1633
|
async getShippingRates({ cartId } = {}) {
|
|
1609
|
-
const
|
|
1610
|
-
const ship =
|
|
1634
|
+
const id2 = cartId || storedCartId();
|
|
1635
|
+
const ship = id2 ? cartExtras(id2).shippingAddress : null;
|
|
1611
1636
|
const country = String(ship?.countryCode || ship?.country || "").toUpperCase();
|
|
1612
|
-
if (!
|
|
1613
|
-
const quote2 = await this.data("/api/checkout/rates", { query: { cart:
|
|
1637
|
+
if (!id2 || !country) return { data: [] };
|
|
1638
|
+
const quote2 = await this.data("/api/checkout/rates", { query: { cart: id2, country, state: ship?.state ? String(ship.state) : void 0 } });
|
|
1614
1639
|
return {
|
|
1615
1640
|
data: (quote2.rates ?? []).map((r) => ({
|
|
1616
1641
|
id: String(r.rate_id),
|
|
@@ -1664,10 +1689,11 @@ var CheckoutService = class extends BaseService {
|
|
|
1664
1689
|
if (!order) throw new GoCommerceError("That payment does not belong to an order placed from this browser.", 404);
|
|
1665
1690
|
return { order_no: order.number, status: "processing" };
|
|
1666
1691
|
}
|
|
1667
|
-
// The family's other
|
|
1668
|
-
// (a client secret, not a hosted page)
|
|
1669
|
-
//
|
|
1670
|
-
//
|
|
1692
|
+
// The family's other gateway flows. GoCommerce's Stripe is a Payment
|
|
1693
|
+
// Element (a client secret, not a hosted page) — `checkout('stripe')` returns
|
|
1694
|
+
// that secret for a storefront that mounts the element. Cashfree, PhonePe and
|
|
1695
|
+
// Affirm are not engine gateways at all. paymentMethodService never offers
|
|
1696
|
+
// these codes, so they are reached only by a storefront that hard-codes one.
|
|
1671
1697
|
async checkoutStripe() {
|
|
1672
1698
|
throw unsupported(
|
|
1673
1699
|
"checkoutStripe",
|
|
@@ -1677,11 +1703,34 @@ var CheckoutService = class extends BaseService {
|
|
|
1677
1703
|
async checkoutStripeCapture() {
|
|
1678
1704
|
throw unsupported("checkoutStripeCapture", "Stripe confirms payments to the engine by webhook");
|
|
1679
1705
|
}
|
|
1680
|
-
|
|
1681
|
-
|
|
1706
|
+
/**
|
|
1707
|
+
* The storefront's generic redirect flow, which it reaches through the code
|
|
1708
|
+
* `PAYPAL`. The engine has no PayPal gateway; paymentMethodService puts the
|
|
1709
|
+
* store's first hosted-page gateway — Adyen, Paddle, Lemon Squeezy, Creem,
|
|
1710
|
+
* Hyperswitch or RevenueCat — under that code, with its own name and a
|
|
1711
|
+
* neutral icon, because this is the one storefront flow that sends a shopper
|
|
1712
|
+
* to a URL and back. The shopper returns to the success page; the gateway's
|
|
1713
|
+
* webhook tells the engine the order is paid.
|
|
1714
|
+
*/
|
|
1715
|
+
async checkoutPaypal({ cartId, origin } = {}) {
|
|
1716
|
+
const gateway = await this.redirectGateway();
|
|
1717
|
+
if (!gateway) throw unsupported("checkoutPaypal", "this store has no hosted-page payment gateway switched on");
|
|
1718
|
+
const id2 = cartId || storedCartId() || "";
|
|
1719
|
+
const back = origin ? `${origin.replace(/\/+$/, "")}/checkout/success?cart_id=${encodeURIComponent(id2)}` : void 0;
|
|
1720
|
+
const result = await this.place(gateway, id2, back ? { return_url: back } : {});
|
|
1721
|
+
const url = result.payment.client_data?.url;
|
|
1722
|
+
if (!url) throw new GoCommerceError("The payment page did not open. Please try again.", 502);
|
|
1723
|
+
return { redirect_url: url, order_no: result.order.number };
|
|
1682
1724
|
}
|
|
1683
|
-
|
|
1684
|
-
|
|
1725
|
+
/**
|
|
1726
|
+
* Nothing to capture: a hosted-page gateway confirms the payment to the
|
|
1727
|
+
* engine by webhook. Answering, rather than throwing, sends the storefront's
|
|
1728
|
+
* process page on to the success page instead of the failure page.
|
|
1729
|
+
*/
|
|
1730
|
+
async capturePaypalPayment({
|
|
1731
|
+
order_no
|
|
1732
|
+
} = {}) {
|
|
1733
|
+
return { order_no: order_no ?? null, status: "processing" };
|
|
1685
1734
|
}
|
|
1686
1735
|
async checkoutCashfree() {
|
|
1687
1736
|
throw unsupported("checkoutCashfree", "the engine has no Cashfree gateway");
|
|
@@ -1704,6 +1753,57 @@ var CheckoutService = class extends BaseService {
|
|
|
1704
1753
|
async checkoutPOS() {
|
|
1705
1754
|
throw unsupported("checkoutPOS", "point-of-sale orders are placed from the admin panel");
|
|
1706
1755
|
}
|
|
1756
|
+
/**
|
|
1757
|
+
* Any payment method the engine has switched on, by its engine code —
|
|
1758
|
+
* `cod`, `stripe`, `razorpay`, `adyen`, `paddle`, `helcim`… For a storefront
|
|
1759
|
+
* that drives the payment itself: the answer says what to do next.
|
|
1760
|
+
*
|
|
1761
|
+
* - `kind: 'none'` — nothing to pay now (cash on delivery).
|
|
1762
|
+
* - `kind: 'client_action'` — mount the gateway's own widget with
|
|
1763
|
+
* `clientData`: Stripe's Payment Element takes `clientSecret`, Razorpay's
|
|
1764
|
+
* modal `razorpay_order_id` and `key_id`, Helcim's `checkout_token`.
|
|
1765
|
+
* - `kind: 'redirect'` — send the shopper to `redirectUrl`; `returnUrl` is
|
|
1766
|
+
* where the gateway brings them back.
|
|
1767
|
+
*
|
|
1768
|
+
* Either way the gateway's webhook tells the engine when the money arrives;
|
|
1769
|
+
* read `orderService.getOrder(orderNo).paymentStatus` to see it.
|
|
1770
|
+
*/
|
|
1771
|
+
async checkout(code, {
|
|
1772
|
+
cartId,
|
|
1773
|
+
returnUrl,
|
|
1774
|
+
paymentData,
|
|
1775
|
+
metadata
|
|
1776
|
+
} = {}) {
|
|
1777
|
+
const result = await this.place(String(code).toLowerCase(), cartId, {
|
|
1778
|
+
...returnUrl ? { return_url: returnUrl } : {},
|
|
1779
|
+
...paymentData ? { payment_data: paymentData } : {},
|
|
1780
|
+
...metadata ? { metadata } : {}
|
|
1781
|
+
});
|
|
1782
|
+
const data = result.payment.client_data ?? {};
|
|
1783
|
+
return {
|
|
1784
|
+
orderNo: result.order.number,
|
|
1785
|
+
order: toOrder(result.order),
|
|
1786
|
+
payment: {
|
|
1787
|
+
kind: result.payment.kind,
|
|
1788
|
+
provider: result.payment.provider,
|
|
1789
|
+
reference: result.payment.reference ?? null,
|
|
1790
|
+
clientSecret: data.client_secret ?? null,
|
|
1791
|
+
redirectUrl: data.url ?? null,
|
|
1792
|
+
clientData: data
|
|
1793
|
+
}
|
|
1794
|
+
};
|
|
1795
|
+
}
|
|
1796
|
+
/** Every payment method the engine has switched on, by engine code and name — drivable here or not. */
|
|
1797
|
+
async listMethods() {
|
|
1798
|
+
const options = await this.data(
|
|
1799
|
+
"/api/checkout"
|
|
1800
|
+
);
|
|
1801
|
+
return { methods: options.methods ?? [], currency: options.currency };
|
|
1802
|
+
}
|
|
1803
|
+
async redirectGateway() {
|
|
1804
|
+
const { methods } = await this.listMethods();
|
|
1805
|
+
return methods.find((m) => REDIRECT_GATEWAYS.has(m.code))?.code;
|
|
1806
|
+
}
|
|
1707
1807
|
/**
|
|
1708
1808
|
* One checkout attempt.
|
|
1709
1809
|
*
|
|
@@ -1714,27 +1814,28 @@ var CheckoutService = class extends BaseService {
|
|
|
1714
1814
|
* request — a different address, a different method — gets a new key, since
|
|
1715
1815
|
* the engine refuses a key replayed with a different body.
|
|
1716
1816
|
*/
|
|
1717
|
-
async place(code, cartId) {
|
|
1718
|
-
const
|
|
1719
|
-
if (!
|
|
1720
|
-
const extras = cartExtras(
|
|
1817
|
+
async place(code, cartId, extra = {}) {
|
|
1818
|
+
const id2 = cartId || storedCartId();
|
|
1819
|
+
if (!id2) throw new GoCommerceError("Your bag is empty.", 400);
|
|
1820
|
+
const extras = cartExtras(id2);
|
|
1721
1821
|
const ship = extras.shippingAddress;
|
|
1722
1822
|
if (!ship) throw new GoCommerceError("Add a delivery address first.", 400);
|
|
1723
|
-
const cart = await this.data(`/api/carts/${encodeURIComponent(
|
|
1823
|
+
const cart = await this.data(`/api/carts/${encodeURIComponent(id2)}`);
|
|
1724
1824
|
const email = cart.email || (ship.email ? String(ship.email) : "");
|
|
1725
1825
|
if (!email) throw new GoCommerceError("Add an email address so we can send your receipt.", 400);
|
|
1726
1826
|
const address = toEngineAddress(ship);
|
|
1727
1827
|
const body = {
|
|
1728
|
-
cart_id:
|
|
1828
|
+
cart_id: id2,
|
|
1729
1829
|
email,
|
|
1730
1830
|
...extras.phone || address.phone ? { phone: String(extras.phone || address.phone) } : {},
|
|
1731
1831
|
...address.name ? { name: address.name } : {},
|
|
1732
1832
|
address,
|
|
1733
|
-
...extras.shippingRateId ? { shipping_rate_id: Number(extras.shippingRateId) } : {}
|
|
1833
|
+
...extras.shippingRateId ? { shipping_rate_id: Number(extras.shippingRateId) } : {},
|
|
1834
|
+
...extra
|
|
1734
1835
|
};
|
|
1735
1836
|
const serialised = JSON.stringify(body);
|
|
1736
1837
|
const key = extras.attempt?.body === `${code}:${serialised}` ? extras.attempt.key : newKey();
|
|
1737
|
-
saveCartExtras(
|
|
1838
|
+
saveCartExtras(id2, { attempt: { key, body: `${code}:${serialised}` } });
|
|
1738
1839
|
const result = await this.data(`/api/checkout/${encodeURIComponent(code)}`, {
|
|
1739
1840
|
method: "POST",
|
|
1740
1841
|
body,
|
|
@@ -1744,7 +1845,7 @@ var CheckoutService = class extends BaseService {
|
|
|
1744
1845
|
rememberOrder({
|
|
1745
1846
|
number: result.order.number,
|
|
1746
1847
|
token: result.order.access_token,
|
|
1747
|
-
cartId:
|
|
1848
|
+
cartId: id2,
|
|
1748
1849
|
reference: result.payment.client_data?.razorpay_order_id ?? result.payment.reference
|
|
1749
1850
|
});
|
|
1750
1851
|
if (sessionToken()) {
|
|
@@ -1755,10 +1856,11 @@ var CheckoutService = class extends BaseService {
|
|
|
1755
1856
|
}).catch(() => void 0);
|
|
1756
1857
|
}
|
|
1757
1858
|
}
|
|
1758
|
-
saveCartExtras(
|
|
1859
|
+
saveCartExtras(id2, { attempt: null });
|
|
1759
1860
|
return result;
|
|
1760
1861
|
}
|
|
1761
1862
|
};
|
|
1863
|
+
var REDIRECT_GATEWAYS = /* @__PURE__ */ new Set(["adyen", "paddle", "lemonsqueezy", "creem", "hyperswitch", "revenuecat"]);
|
|
1762
1864
|
var unsupported = (method, why) => new NotSupportedError("CheckoutService", method, why);
|
|
1763
1865
|
function newKey() {
|
|
1764
1866
|
const c = globalThis.crypto;
|
|
@@ -1778,23 +1880,25 @@ var PaymentMethodService = class extends BaseService {
|
|
|
1778
1880
|
async list(_args = {}) {
|
|
1779
1881
|
const options = await this.facts.checkoutOptions();
|
|
1780
1882
|
const methods = options.methods ?? [];
|
|
1883
|
+
const redirect = methods.find((m) => REDIRECT_GATEWAYS.has(m.code))?.code;
|
|
1884
|
+
const drive = (code) => DRIVABLE[code] ?? (code === redirect ? { code: "PAYPAL", img: "/payment/card.svg" } : void 0);
|
|
1781
1885
|
const data = methods.filter((m) => {
|
|
1782
|
-
if (
|
|
1886
|
+
if (drive(m.code)) return true;
|
|
1783
1887
|
if (!reported2.has(m.code)) {
|
|
1784
1888
|
reported2.add(m.code);
|
|
1785
1889
|
console.warn(
|
|
1786
|
-
`[gocommerce] payment method "${m.code}" is switched on but this storefront cannot drive its flow, so it is not offered.`
|
|
1890
|
+
`[gocommerce] payment method "${m.code}" is switched on but this storefront cannot drive its flow, so it is not offered here; checkoutService.checkout('${m.code}') still reaches it.`
|
|
1787
1891
|
);
|
|
1788
1892
|
}
|
|
1789
1893
|
return false;
|
|
1790
1894
|
}).map((m, i) => ({
|
|
1791
1895
|
id: m.code,
|
|
1792
|
-
code:
|
|
1896
|
+
code: drive(m.code).code,
|
|
1793
1897
|
engineCode: m.code,
|
|
1794
1898
|
name: m.name,
|
|
1795
1899
|
title: m.name,
|
|
1796
1900
|
description: "",
|
|
1797
|
-
img:
|
|
1901
|
+
img: drive(m.code).img ?? null,
|
|
1798
1902
|
rank: i,
|
|
1799
1903
|
active: true,
|
|
1800
1904
|
isActive: true
|
|
@@ -1965,6 +2069,42 @@ var UserService = class extends BaseService {
|
|
|
1965
2069
|
startSession(auth);
|
|
1966
2070
|
return toUser(auth.record);
|
|
1967
2071
|
}
|
|
2072
|
+
/**
|
|
2073
|
+
* Mails the signed-in shopper a link to confirm their address. The engine
|
|
2074
|
+
* sends none at signup, and a confirmed address is what customer-group
|
|
2075
|
+
* prices are keyed on. 409 once confirmed; 429 within a minute of the last.
|
|
2076
|
+
*/
|
|
2077
|
+
async requestEmailVerification() {
|
|
2078
|
+
return this.account("/x/identity/me/email-verification", { method: "POST", auth: true });
|
|
2079
|
+
}
|
|
2080
|
+
/** Extends the session another thirty days. The token stays the same. */
|
|
2081
|
+
async refreshSession() {
|
|
2082
|
+
return startSession(await this.account("/x/identity/refresh", { method: "POST", auth: true }));
|
|
2083
|
+
}
|
|
2084
|
+
/**
|
|
2085
|
+
* Files a guest order under the signed-in account, so it appears in their
|
|
2086
|
+
* order history. The order's access token is the proof of ownership: pass
|
|
2087
|
+
* it, or leave it out for an order placed from this browser, whose token was
|
|
2088
|
+
* kept at checkout.
|
|
2089
|
+
*
|
|
2090
|
+
* Never done automatically at sign-in. On a shared computer that would file
|
|
2091
|
+
* one person's orders — address and all — under whoever signed in next.
|
|
2092
|
+
*/
|
|
2093
|
+
async claimOrder({ number, token }) {
|
|
2094
|
+
const proof = token ?? placedOrder(number)?.token;
|
|
2095
|
+
if (!proof) {
|
|
2096
|
+
throw new GoCommerceError(
|
|
2097
|
+
"This order was not placed from this browser; enter the code from its confirmation.",
|
|
2098
|
+
400
|
|
2099
|
+
);
|
|
2100
|
+
}
|
|
2101
|
+
const order = await this.account("/x/identity/me/orders", {
|
|
2102
|
+
method: "POST",
|
|
2103
|
+
auth: true,
|
|
2104
|
+
body: { number: String(number), token: proof }
|
|
2105
|
+
});
|
|
2106
|
+
return toOrder(order);
|
|
2107
|
+
}
|
|
1968
2108
|
async deleteUser() {
|
|
1969
2109
|
throw new NotSupportedError("UserService", "deleteUser", "an account is closed by the store, on request");
|
|
1970
2110
|
}
|
|
@@ -2062,16 +2202,16 @@ var AddressService = class extends BaseService {
|
|
|
2062
2202
|
page: env.meta?.page ?? page
|
|
2063
2203
|
};
|
|
2064
2204
|
}
|
|
2065
|
-
async fetchAddress(
|
|
2205
|
+
async fetchAddress(id2) {
|
|
2066
2206
|
return fromIdentityAddress(
|
|
2067
|
-
await this.data(`/x/identity/me/addresses/${enc(
|
|
2207
|
+
await this.data(`/x/identity/me/addresses/${enc(id2)}`, { auth: true }),
|
|
2068
2208
|
currentMe()?.email
|
|
2069
2209
|
);
|
|
2070
2210
|
}
|
|
2071
2211
|
/** New when it has no id (or the form's `'new'`), an edit when it has one — the storefront saves both here. */
|
|
2072
2212
|
async saveAddress(address) {
|
|
2073
|
-
const
|
|
2074
|
-
if (
|
|
2213
|
+
const id2 = address?.id && address.id !== "new" ? String(address.id) : null;
|
|
2214
|
+
if (id2) return this.editAddress(id2, address);
|
|
2075
2215
|
const a = await this.data("/x/identity/me/addresses", {
|
|
2076
2216
|
method: "POST",
|
|
2077
2217
|
auth: true,
|
|
@@ -2079,20 +2219,20 @@ var AddressService = class extends BaseService {
|
|
|
2079
2219
|
});
|
|
2080
2220
|
return fromIdentityAddress(a, currentMe()?.email);
|
|
2081
2221
|
}
|
|
2082
|
-
async editAddress(
|
|
2083
|
-
const a = await this.data(`/x/identity/me/addresses/${enc(
|
|
2222
|
+
async editAddress(id2, address) {
|
|
2223
|
+
const a = await this.data(`/x/identity/me/addresses/${enc(id2)}`, {
|
|
2084
2224
|
method: "PATCH",
|
|
2085
2225
|
auth: true,
|
|
2086
2226
|
body: toIdentityAddress(address)
|
|
2087
2227
|
});
|
|
2088
2228
|
return fromIdentityAddress(a, currentMe()?.email);
|
|
2089
2229
|
}
|
|
2090
|
-
async deleteAddress(
|
|
2091
|
-
await this.engine(`/x/identity/me/addresses/${enc(
|
|
2092
|
-
return { id: String(
|
|
2230
|
+
async deleteAddress(id2) {
|
|
2231
|
+
await this.engine(`/x/identity/me/addresses/${enc(id2)}`, { method: "DELETE", auth: true });
|
|
2232
|
+
return { id: String(id2), deleted: true };
|
|
2093
2233
|
}
|
|
2094
2234
|
};
|
|
2095
|
-
var enc = (
|
|
2235
|
+
var enc = (id2) => encodeURIComponent(String(id2));
|
|
2096
2236
|
var addressService = new AddressService();
|
|
2097
2237
|
|
|
2098
2238
|
// src/services/wishlist-service.ts
|
|
@@ -2144,6 +2284,18 @@ var WishlistService = class extends BaseService {
|
|
|
2144
2284
|
const saved = new Set((list?.items ?? []).map((i) => String(i.product_id)));
|
|
2145
2285
|
return (pairs ?? []).map((p) => ({ ...p, exists: saved.has(String(p.productId)) }));
|
|
2146
2286
|
}
|
|
2287
|
+
/**
|
|
2288
|
+
* The address the store may write to when a saved product is back in stock
|
|
2289
|
+
* or on sale; `''` takes it off. A list that does not exist yet is made.
|
|
2290
|
+
*/
|
|
2291
|
+
async setEmail(email) {
|
|
2292
|
+
const list = await this.current() ?? await this.create();
|
|
2293
|
+
await this.data(`/x/wishlist/${enc2(list.token)}`, {
|
|
2294
|
+
method: "PATCH",
|
|
2295
|
+
body: { email: String(email ?? "").trim() }
|
|
2296
|
+
});
|
|
2297
|
+
return { email: String(email ?? "").trim() || null };
|
|
2298
|
+
}
|
|
2147
2299
|
async current() {
|
|
2148
2300
|
const token = wishlistToken();
|
|
2149
2301
|
if (!token) return null;
|
|
@@ -2226,6 +2378,15 @@ var MenuService = class extends BaseService {
|
|
|
2226
2378
|
if (menus) return { data: menus };
|
|
2227
2379
|
return resolveRestLocally("get", "/api/menu");
|
|
2228
2380
|
}
|
|
2381
|
+
/** Every menu the store keeps, by handle — not only the header and footer. */
|
|
2382
|
+
async listMenus() {
|
|
2383
|
+
return await this.data("/x/navigation/menus") ?? [];
|
|
2384
|
+
}
|
|
2385
|
+
/** One menu by handle, in the storefront's shape: `{ menuId, name, items: [{ id, name, link, items }] }`. */
|
|
2386
|
+
async getMenu(handle) {
|
|
2387
|
+
const m = await this.data(`/x/navigation/menus/${encodeURIComponent(handle)}`);
|
|
2388
|
+
return { menuId: m.handle, name: m.title, items: (m.items ?? []).map(toItem) };
|
|
2389
|
+
}
|
|
2229
2390
|
/** The header and footer menus, or `null` when the module is not there. Cached: a menu is store-level. */
|
|
2230
2391
|
async engineMenus() {
|
|
2231
2392
|
if (!await this.facts.pluginOn("navigation")) return null;
|
|
@@ -2244,6 +2405,24 @@ var MenuService = class extends BaseService {
|
|
|
2244
2405
|
};
|
|
2245
2406
|
var menuService = new MenuService();
|
|
2246
2407
|
|
|
2408
|
+
// src/services/newsletter-service.ts
|
|
2409
|
+
var NewsletterService = class extends BaseService {
|
|
2410
|
+
async subscribe({ email, name, source }) {
|
|
2411
|
+
return this.data("/x/newsletter/subscribe", {
|
|
2412
|
+
method: "POST",
|
|
2413
|
+
body: { email: String(email ?? "").trim(), ...name ? { name } : {}, ...source ? { source } : {} }
|
|
2414
|
+
});
|
|
2415
|
+
}
|
|
2416
|
+
/** The token from the unsubscribe link in a newsletter email. */
|
|
2417
|
+
async unsubscribe(token) {
|
|
2418
|
+
return this.data("/x/newsletter/unsubscribe", {
|
|
2419
|
+
method: "POST",
|
|
2420
|
+
body: { token: String(token ?? "") }
|
|
2421
|
+
});
|
|
2422
|
+
}
|
|
2423
|
+
};
|
|
2424
|
+
var newsletterService = new NewsletterService();
|
|
2425
|
+
|
|
2247
2426
|
// src/services/store-service.ts
|
|
2248
2427
|
function symbolFor(code) {
|
|
2249
2428
|
try {
|
|
@@ -2277,6 +2456,22 @@ var StoreService = class extends BaseService {
|
|
|
2277
2456
|
}
|
|
2278
2457
|
return super.get(path);
|
|
2279
2458
|
}
|
|
2459
|
+
/**
|
|
2460
|
+
* The storefront's newsletter box posts `{ email }` to Litekart's
|
|
2461
|
+
* `/api/newsletter/subscribe` through this verb; ext/newsletter is where
|
|
2462
|
+
* that list lives here.
|
|
2463
|
+
*/
|
|
2464
|
+
async post(path, data) {
|
|
2465
|
+
if (path.split("?")[0] === "/api/newsletter/subscribe") {
|
|
2466
|
+
const { email, name } = data ?? {};
|
|
2467
|
+
return await new NewsletterService(this._fetch).subscribe({
|
|
2468
|
+
email: email ?? "",
|
|
2469
|
+
name,
|
|
2470
|
+
source: "storefront"
|
|
2471
|
+
});
|
|
2472
|
+
}
|
|
2473
|
+
return super.post(path, data);
|
|
2474
|
+
}
|
|
2280
2475
|
async live() {
|
|
2281
2476
|
const [ready, plugins, menus, base] = await Promise.all([
|
|
2282
2477
|
this.facts.ready(),
|
|
@@ -2582,9 +2777,304 @@ var varniCustomProductService = new VarniCustomProductService();
|
|
|
2582
2777
|
var pluginsService = pluginService;
|
|
2583
2778
|
var settingsService = settingService;
|
|
2584
2779
|
|
|
2780
|
+
// src/services/variant-service.ts
|
|
2781
|
+
var VariantService = class extends BaseService {
|
|
2782
|
+
/** A product's variants, inactive ones included (filter on `active`), in the merchant's order. */
|
|
2783
|
+
async list(productId, { page = 1, limit = 50 } = {}) {
|
|
2784
|
+
const env = await this.engine("/api/variants", { query: { product_id: productId, page, limit } });
|
|
2785
|
+
return { data: (env.data ?? []).map(withAmounts), meta: env.meta };
|
|
2786
|
+
}
|
|
2787
|
+
async getOne(id2) {
|
|
2788
|
+
return withAmounts(await this.data(`/api/variants/${encodeURIComponent(String(id2))}`));
|
|
2789
|
+
}
|
|
2790
|
+
/**
|
|
2791
|
+
* What each approved seller asks for this variant, cheapest first. A cart
|
|
2792
|
+
* line takes a variant, not an offer, so this informs a shopper rather than
|
|
2793
|
+
* choosing who fulfils.
|
|
2794
|
+
*/
|
|
2795
|
+
async offers(variantId) {
|
|
2796
|
+
const offers = await this.data(`/api/variants/${encodeURIComponent(String(variantId))}/offers`) ?? [];
|
|
2797
|
+
return offers.map((o) => ({ ...o, price: { ...o.price, amount: toMajor(o.price) } }));
|
|
2798
|
+
}
|
|
2799
|
+
};
|
|
2800
|
+
function withAmounts(v) {
|
|
2801
|
+
return {
|
|
2802
|
+
...v,
|
|
2803
|
+
price: { ...v.price, amount: toMajor(v.price) },
|
|
2804
|
+
compare_at_price: v.compare_at_price ? { ...v.compare_at_price, amount: toMajor(v.compare_at_price) } : void 0
|
|
2805
|
+
};
|
|
2806
|
+
}
|
|
2807
|
+
var variantService = new VariantService();
|
|
2808
|
+
|
|
2809
|
+
// src/services/b2b-service.ts
|
|
2810
|
+
var B2bService = class extends BaseService {
|
|
2811
|
+
// ------------------------------------------------------------ company
|
|
2812
|
+
/** The company, the caller's role in it, and its credit: limit, outstanding, available, overdue. */
|
|
2813
|
+
async me() {
|
|
2814
|
+
const me = await this.data("/x/b2b/me", { auth: true });
|
|
2815
|
+
const { notes: _notes, metadata: _metadata, ...company } = me.company;
|
|
2816
|
+
return { company, role: me.role, credit: me.credit };
|
|
2817
|
+
}
|
|
2818
|
+
async members() {
|
|
2819
|
+
return await this.data("/x/b2b/members", { auth: true }) ?? [];
|
|
2820
|
+
}
|
|
2821
|
+
/** Admin only. Demoting the last admin is refused (409). */
|
|
2822
|
+
async setMemberRole(customerId, role) {
|
|
2823
|
+
return this.data(`/x/b2b/members/${id(customerId)}`, { method: "PATCH", auth: true, body: { role } });
|
|
2824
|
+
}
|
|
2825
|
+
/** An admin removes anyone; any member may remove themselves. */
|
|
2826
|
+
async removeMember(customerId) {
|
|
2827
|
+
await this.engine(`/x/b2b/members/${id(customerId)}`, { method: "DELETE", auth: true });
|
|
2828
|
+
}
|
|
2829
|
+
// ------------------------------------------------------------ invitations
|
|
2830
|
+
/** Admin only. Open invitations: not accepted, not expired. */
|
|
2831
|
+
async invitations() {
|
|
2832
|
+
return await this.data("/x/b2b/invitations", { auth: true }) ?? [];
|
|
2833
|
+
}
|
|
2834
|
+
/** Admin only. Emails a link; a second invitation to the same address replaces the first. */
|
|
2835
|
+
async invite({ email, role }) {
|
|
2836
|
+
return this.data("/x/b2b/invitations", {
|
|
2837
|
+
method: "POST",
|
|
2838
|
+
auth: true,
|
|
2839
|
+
body: { email: String(email ?? "").trim(), role }
|
|
2840
|
+
});
|
|
2841
|
+
}
|
|
2842
|
+
async withdrawInvitation(invitationId) {
|
|
2843
|
+
await this.engine(`/x/b2b/invitations/${id(invitationId)}`, { method: "DELETE", auth: true });
|
|
2844
|
+
}
|
|
2845
|
+
/**
|
|
2846
|
+
* The token from the invitation email, accepted by the account it was sent
|
|
2847
|
+
* to — which also confirms that account's email address.
|
|
2848
|
+
*/
|
|
2849
|
+
async acceptInvitation(token) {
|
|
2850
|
+
return this.data("/x/b2b/invitations/accept", {
|
|
2851
|
+
method: "POST",
|
|
2852
|
+
auth: true,
|
|
2853
|
+
body: { token: String(token ?? "").trim() }
|
|
2854
|
+
});
|
|
2855
|
+
}
|
|
2856
|
+
// ------------------------------------------------------------ filling a basket
|
|
2857
|
+
/**
|
|
2858
|
+
* A quick order: up to 500 lines by SKU or variant id, into the bag (or a new
|
|
2859
|
+
* one), at the company's prices. A line that will not go in is reported in
|
|
2860
|
+
* `rejected` with its reason; the rest are added.
|
|
2861
|
+
*/
|
|
2862
|
+
async addLines({
|
|
2863
|
+
cartId,
|
|
2864
|
+
lines
|
|
2865
|
+
}) {
|
|
2866
|
+
const res = await this.data("/x/b2b/cart/lines", {
|
|
2867
|
+
method: "POST",
|
|
2868
|
+
auth: true,
|
|
2869
|
+
body: {
|
|
2870
|
+
...cartId ?? storedCartId() ? { cart_id: cartId ?? storedCartId() } : {},
|
|
2871
|
+
lines: (lines ?? []).map((l) => ({
|
|
2872
|
+
...l.variantId ? { variant_id: Number(l.variantId) } : {},
|
|
2873
|
+
...l.sku ? { sku: String(l.sku).trim() } : {},
|
|
2874
|
+
quantity: Number(l.quantity)
|
|
2875
|
+
}))
|
|
2876
|
+
}
|
|
2877
|
+
});
|
|
2878
|
+
rememberCartId(res.cart.id);
|
|
2879
|
+
return { cart: toCart(res.cart, cartExtras(res.cart.id)), rejected: res.rejected ?? [] };
|
|
2880
|
+
}
|
|
2881
|
+
/** A past order's lines in a new bag, at today's prices. Nothing is placed. */
|
|
2882
|
+
async reorder(orderId) {
|
|
2883
|
+
const res = await this.data(
|
|
2884
|
+
`/x/b2b/orders/${id(orderId)}/reorder`,
|
|
2885
|
+
{
|
|
2886
|
+
method: "POST",
|
|
2887
|
+
auth: true
|
|
2888
|
+
}
|
|
2889
|
+
);
|
|
2890
|
+
rememberCartId(res.cart.id);
|
|
2891
|
+
return { cart: toCart(res.cart, cartExtras(res.cart.id)), rejected: res.rejected ?? [] };
|
|
2892
|
+
}
|
|
2893
|
+
// ------------------------------------------------------------ placing an order
|
|
2894
|
+
/**
|
|
2895
|
+
* Places the bag through the company: on account by default, with a PO
|
|
2896
|
+
* number, optionally only some lines (`lineIds`). A buyer over the company's
|
|
2897
|
+
* approval threshold gets `{ kind: 'approval' }` instead of an order — the
|
|
2898
|
+
* bag stays as it was, and an approver places it later.
|
|
2899
|
+
*
|
|
2900
|
+
* Over the credit limit is 403 `credit_limit_exceeded`; `me().credit` says
|
|
2901
|
+
* how much is available before the shopper tries.
|
|
2902
|
+
*/
|
|
2903
|
+
async checkout({
|
|
2904
|
+
cartId,
|
|
2905
|
+
lineIds,
|
|
2906
|
+
shippingRateId,
|
|
2907
|
+
returnUrl,
|
|
2908
|
+
paymentData,
|
|
2909
|
+
...opts
|
|
2910
|
+
} = {}) {
|
|
2911
|
+
const cart = cartId || storedCartId();
|
|
2912
|
+
if (!cart) throw new GoCommerceError("Your bag is empty.", 400);
|
|
2913
|
+
const extras = cartExtras(cart);
|
|
2914
|
+
const rate = shippingRateId ?? extras.shippingRateId;
|
|
2915
|
+
const body = {
|
|
2916
|
+
cart_id: cart,
|
|
2917
|
+
...this.placeBody(opts, extras.shippingAddress ?? void 0),
|
|
2918
|
+
...rate ? { shipping_rate_id: Number(rate) } : {},
|
|
2919
|
+
...lineIds?.length ? { line_ids: lineIds.map(Number) } : {},
|
|
2920
|
+
...returnUrl ? { return_url: returnUrl } : {},
|
|
2921
|
+
...paymentData ? { payment_data: paymentData } : {}
|
|
2922
|
+
};
|
|
2923
|
+
const res = await this.data("/x/b2b/checkout", {
|
|
2924
|
+
method: "POST",
|
|
2925
|
+
auth: true,
|
|
2926
|
+
body,
|
|
2927
|
+
headers: { "Idempotency-Key": attemptKey(`b2b:${JSON.stringify(body)}`) }
|
|
2928
|
+
});
|
|
2929
|
+
return this.placed(res, cart);
|
|
2930
|
+
}
|
|
2931
|
+
// ------------------------------------------------------------ the company's orders
|
|
2932
|
+
/** Admins and approvers see the company's; a buyer sees their own. `q` matches the number, PO or email. */
|
|
2933
|
+
async orders({
|
|
2934
|
+
page = 1,
|
|
2935
|
+
limit = 20,
|
|
2936
|
+
q,
|
|
2937
|
+
overdue
|
|
2938
|
+
} = {}) {
|
|
2939
|
+
return this.engine("/x/b2b/orders", {
|
|
2940
|
+
auth: true,
|
|
2941
|
+
query: { page, limit, q, ...overdue ? { overdue: "true" } : {} }
|
|
2942
|
+
});
|
|
2943
|
+
}
|
|
2944
|
+
// ------------------------------------------------------------ approvals
|
|
2945
|
+
async approvals({
|
|
2946
|
+
page = 1,
|
|
2947
|
+
limit = 20,
|
|
2948
|
+
status
|
|
2949
|
+
} = {}) {
|
|
2950
|
+
return this.engine("/x/b2b/approvals", { auth: true, query: { page, limit, status } });
|
|
2951
|
+
}
|
|
2952
|
+
async approval(approvalId) {
|
|
2953
|
+
return this.data(`/x/b2b/approvals/${id(approvalId)}`, { auth: true });
|
|
2954
|
+
}
|
|
2955
|
+
/**
|
|
2956
|
+
* Admin or approver: places the requested order at the prices it was
|
|
2957
|
+
* requested at. Credit limits and holds still apply. The payment intent is
|
|
2958
|
+
* the approver's to act on — for an account order there is nothing to do.
|
|
2959
|
+
*/
|
|
2960
|
+
async approve(approvalId) {
|
|
2961
|
+
const res = await this.data(`/x/b2b/approvals/${id(approvalId)}/approve`, {
|
|
2962
|
+
method: "POST",
|
|
2963
|
+
auth: true
|
|
2964
|
+
});
|
|
2965
|
+
return { approval: res.approval, orderNo: res.order.number, order: toOrder(res.order), payment: res.payment };
|
|
2966
|
+
}
|
|
2967
|
+
async reject(approvalId, reason) {
|
|
2968
|
+
return this.data(`/x/b2b/approvals/${id(approvalId)}/reject`, {
|
|
2969
|
+
method: "POST",
|
|
2970
|
+
auth: true,
|
|
2971
|
+
...reason ? { body: { reason } } : {}
|
|
2972
|
+
});
|
|
2973
|
+
}
|
|
2974
|
+
/** The buyer who asked withdraws the request. */
|
|
2975
|
+
async cancelApproval(approvalId) {
|
|
2976
|
+
return this.data(`/x/b2b/approvals/${id(approvalId)}/cancel`, { method: "POST", auth: true });
|
|
2977
|
+
}
|
|
2978
|
+
// ------------------------------------------------------------ quotes
|
|
2979
|
+
async quotes({ page = 1, limit = 20, status } = {}) {
|
|
2980
|
+
return this.engine("/x/b2b/quotes", { auth: true, query: { page, limit, status } });
|
|
2981
|
+
}
|
|
2982
|
+
async quote(quoteId) {
|
|
2983
|
+
return this.data(`/x/b2b/quotes/${id(quoteId)}`, { auth: true });
|
|
2984
|
+
}
|
|
2985
|
+
/** Asks the store to price these lines. The store answers with unit prices and an expiry. */
|
|
2986
|
+
async requestQuote({ lines, note }) {
|
|
2987
|
+
return this.data("/x/b2b/quotes", {
|
|
2988
|
+
method: "POST",
|
|
2989
|
+
auth: true,
|
|
2990
|
+
body: {
|
|
2991
|
+
lines: (lines ?? []).map((l) => ({ variant_id: Number(l.variantId), quantity: Number(l.quantity) })),
|
|
2992
|
+
...note ? { note } : {}
|
|
2993
|
+
}
|
|
2994
|
+
});
|
|
2995
|
+
}
|
|
2996
|
+
/** Places the quote at its agreed prices — or, for a buyer over the threshold, asks for approval. */
|
|
2997
|
+
async acceptQuote(quoteId, opts = {}) {
|
|
2998
|
+
const res = await this.data(`/x/b2b/quotes/${id(quoteId)}/accept`, {
|
|
2999
|
+
method: "POST",
|
|
3000
|
+
auth: true,
|
|
3001
|
+
body: this.placeBody(opts)
|
|
3002
|
+
});
|
|
3003
|
+
return this.placed(res);
|
|
3004
|
+
}
|
|
3005
|
+
async declineQuote(quoteId) {
|
|
3006
|
+
return this.data(`/x/b2b/quotes/${id(quoteId)}/decline`, { method: "POST", auth: true });
|
|
3007
|
+
}
|
|
3008
|
+
// ------------------------------------------------------------ dealer leads
|
|
3009
|
+
/**
|
|
3010
|
+
* The public "find a dealer" form: no account needed. The engine routes it to
|
|
3011
|
+
* the dealer whose territory covers the address — or keeps it — and answers
|
|
3012
|
+
* the same either way, so the form never reveals who got it.
|
|
3013
|
+
*/
|
|
3014
|
+
async submitLead(lead) {
|
|
3015
|
+
return this.data("/x/b2b/leads", {
|
|
3016
|
+
method: "POST",
|
|
3017
|
+
body: {
|
|
3018
|
+
...lead.name ? { name: lead.name } : {},
|
|
3019
|
+
...lead.email ? { email: lead.email } : {},
|
|
3020
|
+
...lead.phone ? { phone: lead.phone } : {},
|
|
3021
|
+
...lead.message ? { message: lead.message } : {},
|
|
3022
|
+
...lead.country ? { country: lead.country } : {},
|
|
3023
|
+
...lead.state ? { state: lead.state } : {},
|
|
3024
|
+
...lead.postalCode ? { postal_code: lead.postalCode } : {},
|
|
3025
|
+
...lead.productId ? { product_id: Number(lead.productId) } : {},
|
|
3026
|
+
...lead.variantId ? { variant_id: Number(lead.variantId) } : {},
|
|
3027
|
+
...lead.source ? { source: lead.source } : {}
|
|
3028
|
+
}
|
|
3029
|
+
});
|
|
3030
|
+
}
|
|
3031
|
+
/** A dealer's admins and approvers: the leads routed to their company. */
|
|
3032
|
+
async leads({ page = 1, limit = 20, status } = {}) {
|
|
3033
|
+
return this.engine("/x/b2b/leads", { auth: true, query: { page, limit, status } });
|
|
3034
|
+
}
|
|
3035
|
+
async setLeadStatus(leadId, status) {
|
|
3036
|
+
return this.data(`/x/b2b/leads/${id(leadId)}`, { method: "PATCH", auth: true, body: { status } });
|
|
3037
|
+
}
|
|
3038
|
+
// ------------------------------------------------------------ internals
|
|
3039
|
+
/** The fields checkout and quote acceptance share. Only declared ones: the engine refuses the rest. */
|
|
3040
|
+
placeBody(opts, fallback) {
|
|
3041
|
+
const raw = opts.address ?? fallback;
|
|
3042
|
+
if (!raw) throw new GoCommerceError("Add a delivery address first.", 400);
|
|
3043
|
+
const address = "line1" in raw ? raw : toEngineAddress(raw);
|
|
3044
|
+
return {
|
|
3045
|
+
address,
|
|
3046
|
+
...opts.paymentMethod ? { payment_method: String(opts.paymentMethod).toLowerCase() } : {},
|
|
3047
|
+
...opts.poNumber ? { po_number: String(opts.poNumber).trim() } : {},
|
|
3048
|
+
...opts.name ?? address.name ? { name: opts.name ?? address.name } : {},
|
|
3049
|
+
...opts.phone ?? address.phone ? { phone: opts.phone ?? address.phone } : {}
|
|
3050
|
+
};
|
|
3051
|
+
}
|
|
3052
|
+
placed(res, cartId) {
|
|
3053
|
+
if (res.approval && !res.order) return { kind: "approval", approval: res.approval };
|
|
3054
|
+
const order = res.order;
|
|
3055
|
+
if (order.access_token && cartId) {
|
|
3056
|
+
rememberOrder({ number: order.number, token: order.access_token, cartId, reference: res.payment?.reference });
|
|
3057
|
+
}
|
|
3058
|
+
return { kind: "order", orderNo: order.number, order: toOrder(order), payment: res.payment };
|
|
3059
|
+
}
|
|
3060
|
+
};
|
|
3061
|
+
var id = (v) => encodeURIComponent(String(v));
|
|
3062
|
+
var keys = /* @__PURE__ */ new Map();
|
|
3063
|
+
function attemptKey(request) {
|
|
3064
|
+
let key = keys.get(request);
|
|
3065
|
+
if (!key) {
|
|
3066
|
+
const c = globalThis.crypto;
|
|
3067
|
+
key = c?.randomUUID?.() ?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
3068
|
+
keys.set(request, key);
|
|
3069
|
+
}
|
|
3070
|
+
return key;
|
|
3071
|
+
}
|
|
3072
|
+
var b2bService = new B2bService();
|
|
3073
|
+
|
|
2585
3074
|
exports.AddressService = AddressService;
|
|
2586
3075
|
exports.AuthService = AuthService;
|
|
2587
3076
|
exports.AutocompleteService = AutocompleteService;
|
|
3077
|
+
exports.B2bService = B2bService;
|
|
2588
3078
|
exports.BannerService = BannerService;
|
|
2589
3079
|
exports.BaseService = BaseService;
|
|
2590
3080
|
exports.BlogService = BlogService;
|
|
@@ -2609,6 +3099,7 @@ exports.HomeService = HomeService;
|
|
|
2609
3099
|
exports.InitService = InitService;
|
|
2610
3100
|
exports.MeilisearchService = MeilisearchService;
|
|
2611
3101
|
exports.MenuService = MenuService;
|
|
3102
|
+
exports.NewsletterService = NewsletterService;
|
|
2612
3103
|
exports.NotSupportedError = NotSupportedError;
|
|
2613
3104
|
exports.OrderService = OrderService;
|
|
2614
3105
|
exports.PageService = PageService;
|
|
@@ -2619,6 +3110,7 @@ exports.PopularSearchService = PopularSearchService;
|
|
|
2619
3110
|
exports.PopularityService = PopularityService;
|
|
2620
3111
|
exports.ProductService = ProductService;
|
|
2621
3112
|
exports.ProfileService = ProfileService;
|
|
3113
|
+
exports.REDIRECT_GATEWAYS = REDIRECT_GATEWAYS;
|
|
2622
3114
|
exports.ReelsService = ReelsService;
|
|
2623
3115
|
exports.RegionService = RegionService;
|
|
2624
3116
|
exports.ReviewService = ReviewService;
|
|
@@ -2629,6 +3121,7 @@ exports.StateService = StateService;
|
|
|
2629
3121
|
exports.StoreService = StoreService;
|
|
2630
3122
|
exports.UploadService = UploadService;
|
|
2631
3123
|
exports.UserService = UserService;
|
|
3124
|
+
exports.VariantService = VariantService;
|
|
2632
3125
|
exports.VarniCustomDesignService = VarniCustomDesignService;
|
|
2633
3126
|
exports.VarniCustomProductService = VarniCustomProductService;
|
|
2634
3127
|
exports.VendorService = VendorService;
|
|
@@ -2637,6 +3130,7 @@ exports.WishlistService = WishlistService;
|
|
|
2637
3130
|
exports.addressService = addressService;
|
|
2638
3131
|
exports.authService = authService;
|
|
2639
3132
|
exports.autocompleteService = autocompleteService;
|
|
3133
|
+
exports.b2bService = b2bService;
|
|
2640
3134
|
exports.bannerService = bannerService;
|
|
2641
3135
|
exports.blogService = blogService;
|
|
2642
3136
|
exports.cartService = cartService;
|
|
@@ -2668,6 +3162,7 @@ exports.initService = initService;
|
|
|
2668
3162
|
exports.meilisearchService = meilisearchService;
|
|
2669
3163
|
exports.menuService = menuService;
|
|
2670
3164
|
exports.minorToMajor = minorToMajor;
|
|
3165
|
+
exports.newsletterService = newsletterService;
|
|
2671
3166
|
exports.orderService = orderService;
|
|
2672
3167
|
exports.pageService = pageService;
|
|
2673
3168
|
exports.paymentMethodService = paymentMethodService;
|
|
@@ -2710,6 +3205,7 @@ exports.toVariant = toVariant;
|
|
|
2710
3205
|
exports.toVendor = toVendor;
|
|
2711
3206
|
exports.uploadService = uploadService;
|
|
2712
3207
|
exports.userService = userService;
|
|
3208
|
+
exports.variantService = variantService;
|
|
2713
3209
|
exports.varniCustomDesignService = varniCustomDesignService;
|
|
2714
3210
|
exports.varniCustomProductService = varniCustomProductService;
|
|
2715
3211
|
exports.vendorService = vendorService;
|