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