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