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