@misiki/gocommerce-connector 0.28.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/{index-BaFtBtaA.d.cts → index-BdXhbm20.d.cts} +145 -24
- package/dist/{index-BaFtBtaA.d.ts → index-BdXhbm20.d.ts} +145 -24
- package/dist/index.cjs +204 -26
- 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 +204 -27
- package/dist/index.js.map +1 -1
- package/dist/services/index.cjs +202 -26
- 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 +202 -26
- package/dist/services/index.js.map +1 -1
- package/package.json +1 -1
package/dist/services/index.cjs
CHANGED
|
@@ -1087,7 +1087,22 @@ function toCartOffer(o, currency) {
|
|
|
1087
1087
|
applies: o.applies,
|
|
1088
1088
|
amount: o.amount ? toMajor(o.amount) : null,
|
|
1089
1089
|
remaining: o.remaining ? toMajor(o.remaining) : null,
|
|
1090
|
-
reason: o.reason ?? ""
|
|
1090
|
+
reason: o.reason ?? "",
|
|
1091
|
+
autoApply: !!o.auto_apply
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
function toCheckoutQuote(q) {
|
|
1095
|
+
const d = q.discount;
|
|
1096
|
+
return {
|
|
1097
|
+
subtotal: toMajor(q.subtotal),
|
|
1098
|
+
shippingCharges: toMajor(q.shipping),
|
|
1099
|
+
shippingMethod: q.shipping_method ?? null,
|
|
1100
|
+
discount: toMajor(d?.amount),
|
|
1101
|
+
coupon: d ? { code: d.code, title: d.title, autoApplied: d.auto_applied, freeShipping: !!d.free_shipping } : null,
|
|
1102
|
+
tax: toMajor(q.tax),
|
|
1103
|
+
taxIncluded: q.tax_inclusive,
|
|
1104
|
+
codCharges: toMajor(q.payment_fee),
|
|
1105
|
+
total: toMajor(q.total)
|
|
1091
1106
|
};
|
|
1092
1107
|
}
|
|
1093
1108
|
function toFreeShipping(f) {
|
|
@@ -1112,6 +1127,14 @@ function remember(key, load, ttl = TTL_MS) {
|
|
|
1112
1127
|
});
|
|
1113
1128
|
return value;
|
|
1114
1129
|
}
|
|
1130
|
+
var known = /* @__PURE__ */ new Map();
|
|
1131
|
+
function keep(key, value) {
|
|
1132
|
+
known.set(key, value);
|
|
1133
|
+
return value;
|
|
1134
|
+
}
|
|
1135
|
+
function recall(key) {
|
|
1136
|
+
return known.get(key);
|
|
1137
|
+
}
|
|
1115
1138
|
|
|
1116
1139
|
// src/services/facts.ts
|
|
1117
1140
|
var StoreFacts = class extends BaseService {
|
|
@@ -1574,7 +1597,23 @@ function attributesFrom(p) {
|
|
|
1574
1597
|
return Object.keys(out).length ? out : void 0;
|
|
1575
1598
|
}
|
|
1576
1599
|
var counts = (values) => Object.fromEntries((values ?? []).map((v) => [v.value, v.count]));
|
|
1577
|
-
function
|
|
1600
|
+
function categoryFacets(counted, tree) {
|
|
1601
|
+
const byId = new Map(tree.map((c) => [c.id, c]));
|
|
1602
|
+
const totals = /* @__PURE__ */ new Map();
|
|
1603
|
+
for (const { id: id2, count } of counted) {
|
|
1604
|
+
for (let c = byId.get(id2); c; c = c.parent_id === null ? void 0 : byId.get(c.parent_id)) {
|
|
1605
|
+
totals.set(c.id, (totals.get(c.id) ?? 0) + count);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
const kept = tree.filter((c) => totals.has(c.id)).sort((a, b) => a.position - b.position || a.id - b.id);
|
|
1609
|
+
const parentOf = (c) => c.parent_id !== null && byId.has(c.parent_id) ? c.parent_id : null;
|
|
1610
|
+
const under = (parent) => kept.filter((c) => parentOf(c) === parent).flatMap((c) => [{ name: c.title, slug: c.slug, count: totals.get(c.id), depth: c.depth }, ...under(c.id)]);
|
|
1611
|
+
return [
|
|
1612
|
+
...under(null),
|
|
1613
|
+
...counted.filter((c) => !byId.has(c.id)).map((c) => ({ name: c.title, slug: c.slug, count: c.count, depth: 0 }))
|
|
1614
|
+
];
|
|
1615
|
+
}
|
|
1616
|
+
function toStorefrontFacets(f, tree = []) {
|
|
1578
1617
|
if (!f) return { priceStat: {}, categories: [], tags: [], allFilters: {} };
|
|
1579
1618
|
const allFilters = {};
|
|
1580
1619
|
const group = (key, values) => {
|
|
@@ -1585,7 +1624,7 @@ function toStorefrontFacets(f) {
|
|
|
1585
1624
|
for (const [handle, values] of Object.entries(f.attributes ?? {})) group(`attributes.${handle}`, values);
|
|
1586
1625
|
return {
|
|
1587
1626
|
priceStat: f.price ? { min: toMajor(f.price.min), max: toMajor(f.price.max) } : {},
|
|
1588
|
-
categories: (f.categories ?? []
|
|
1627
|
+
categories: categoryFacets(f.categories ?? [], tree),
|
|
1589
1628
|
tags: (f.tags ?? []).map((t) => ({ name: t.value, count: t.count })),
|
|
1590
1629
|
allFilters
|
|
1591
1630
|
};
|
|
@@ -1618,13 +1657,14 @@ var SearchService = class extends CatalogService {
|
|
|
1618
1657
|
productTypes: listParam(p, "product_type"),
|
|
1619
1658
|
facets: true
|
|
1620
1659
|
});
|
|
1660
|
+
const tree = res.facets?.categories?.length ? await this.facts.categories().catch(() => []) : [];
|
|
1621
1661
|
return {
|
|
1622
1662
|
data: res.data,
|
|
1623
1663
|
count: res.count,
|
|
1624
1664
|
totalPages: res.noOfPage,
|
|
1625
1665
|
page: res.page,
|
|
1626
1666
|
categoryHierarchy: res.categoryHierarchy,
|
|
1627
|
-
facets: toStorefrontFacets(res.facets)
|
|
1667
|
+
facets: toStorefrontFacets(res.facets, tree)
|
|
1628
1668
|
};
|
|
1629
1669
|
} catch (err) {
|
|
1630
1670
|
if (err?.status === 404) return this.emptyResult();
|
|
@@ -2092,6 +2132,7 @@ var customizerService = new CustomizerService();
|
|
|
2092
2132
|
|
|
2093
2133
|
// src/services/checkout-service.ts
|
|
2094
2134
|
var CheckoutService = class extends BaseService {
|
|
2135
|
+
expected = /* @__PURE__ */ new Map();
|
|
2095
2136
|
/** Delivery options for the address on the cart: `{ data: [{ id, name, base_rate }] }`. */
|
|
2096
2137
|
async getShippingRates({ cartId } = {}) {
|
|
2097
2138
|
const id2 = cartId || storedCartId();
|
|
@@ -2263,6 +2304,64 @@ var CheckoutService = class extends BaseService {
|
|
|
2263
2304
|
);
|
|
2264
2305
|
return { methods: options.methods ?? [], currency: options.currency };
|
|
2265
2306
|
}
|
|
2307
|
+
/**
|
|
2308
|
+
* What checkout would charge for the cart now, by the storefront's method
|
|
2309
|
+
* code (`COD`, `RAZORPAY`, `PAYPAL`) or none yet (D185). Null until the
|
|
2310
|
+
* address step has stored an address, and null from an engine before D185.
|
|
2311
|
+
*
|
|
2312
|
+
* A refusal is checkout's own: the same GoCommerceError, with the same
|
|
2313
|
+
* message, that placing the order would raise now.
|
|
2314
|
+
*/
|
|
2315
|
+
async getQuote({ cartId, method = "" } = {}) {
|
|
2316
|
+
const id2 = cartId || storedCartId();
|
|
2317
|
+
if (!id2 || !cartExtras(id2).shippingAddress) return null;
|
|
2318
|
+
const { extras, email, address } = await this.storedCheckout(id2);
|
|
2319
|
+
const code = method.toLowerCase() === "paypal" ? await this.redirectGateway() : method.toLowerCase();
|
|
2320
|
+
try {
|
|
2321
|
+
const quote2 = await this.data("/api/checkout/quote", {
|
|
2322
|
+
query: {
|
|
2323
|
+
cart: id2,
|
|
2324
|
+
country: address.country,
|
|
2325
|
+
state: address.state,
|
|
2326
|
+
shipping_rate_id: extras.shippingRateId || void 0,
|
|
2327
|
+
method: code || void 0,
|
|
2328
|
+
email: email || void 0
|
|
2329
|
+
}
|
|
2330
|
+
});
|
|
2331
|
+
return toCheckoutQuote(quote2);
|
|
2332
|
+
} catch (err) {
|
|
2333
|
+
if (err instanceof GoCommerceError && (err.status === 405 || err.status === 404 && err.message.startsWith("no route for "))) {
|
|
2334
|
+
return null;
|
|
2335
|
+
}
|
|
2336
|
+
throw err;
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
/**
|
|
2340
|
+
* The total on screen as the shopper confirms, from getQuote, or null when nothing was quoted
|
|
2341
|
+
* (D185). Every order placed for the cart then carries it, and checkout refuses one whose own
|
|
2342
|
+
* total differs instead of charging it: `onChange` gets checkout's figures now and the sentence
|
|
2343
|
+
* the placing call fails with.
|
|
2344
|
+
*/
|
|
2345
|
+
expectTotal({
|
|
2346
|
+
cartId,
|
|
2347
|
+
total,
|
|
2348
|
+
onChange
|
|
2349
|
+
}) {
|
|
2350
|
+
const id2 = cartId || storedCartId();
|
|
2351
|
+
if (!id2) return;
|
|
2352
|
+
if (typeof total === "number") this.expected.set(id2, { total, onChange });
|
|
2353
|
+
else this.expected.delete(id2);
|
|
2354
|
+
}
|
|
2355
|
+
async storedCheckout(cartId) {
|
|
2356
|
+
const id2 = cartId || storedCartId();
|
|
2357
|
+
if (!id2) throw new GoCommerceError("Your bag is empty.", 400);
|
|
2358
|
+
const extras = cartExtras(id2);
|
|
2359
|
+
const ship = extras.shippingAddress;
|
|
2360
|
+
if (!ship) throw new GoCommerceError("Add a delivery address first.", 400);
|
|
2361
|
+
const cart = await this.data(`/api/carts/${encodeURIComponent(id2)}`);
|
|
2362
|
+
const email = cart.email || (ship.email ? String(ship.email) : "");
|
|
2363
|
+
return { id: id2, extras, email, address: toEngineAddress(ship), currency: cart.currency };
|
|
2364
|
+
}
|
|
2266
2365
|
async redirectGateway() {
|
|
2267
2366
|
const { methods } = await this.listMethods();
|
|
2268
2367
|
return methods.find((m) => REDIRECT_GATEWAYS.has(m.code))?.code;
|
|
@@ -2278,15 +2377,8 @@ var CheckoutService = class extends BaseService {
|
|
|
2278
2377
|
* the engine refuses a key replayed with a different body.
|
|
2279
2378
|
*/
|
|
2280
2379
|
async place(code, cartId, extra = {}) {
|
|
2281
|
-
const id2 =
|
|
2282
|
-
if (!id2) throw new GoCommerceError("Your bag is empty.", 400);
|
|
2283
|
-
const extras = cartExtras(id2);
|
|
2284
|
-
const ship = extras.shippingAddress;
|
|
2285
|
-
if (!ship) throw new GoCommerceError("Add a delivery address first.", 400);
|
|
2286
|
-
const cart = await this.data(`/api/carts/${encodeURIComponent(id2)}`);
|
|
2287
|
-
const email = cart.email || (ship.email ? String(ship.email) : "");
|
|
2380
|
+
const { id: id2, extras, email, address, currency } = await this.storedCheckout(cartId);
|
|
2288
2381
|
if (!email) throw new GoCommerceError("Add an email address so we can send your receipt.", 400);
|
|
2289
|
-
const address = toEngineAddress(ship);
|
|
2290
2382
|
const body = {
|
|
2291
2383
|
cart_id: id2,
|
|
2292
2384
|
email,
|
|
@@ -2309,10 +2401,23 @@ var CheckoutService = class extends BaseService {
|
|
|
2309
2401
|
...meta ? { meta_capi: meta } : {},
|
|
2310
2402
|
...attribution ? { attribution } : {}
|
|
2311
2403
|
};
|
|
2404
|
+
const expected = this.expected.get(id2);
|
|
2312
2405
|
const result = await this.data(`/api/checkout/${encodeURIComponent(code)}`, {
|
|
2313
2406
|
method: "POST",
|
|
2314
|
-
body:
|
|
2407
|
+
body: {
|
|
2408
|
+
...Object.keys(metadata).length ? { ...body, metadata } : body,
|
|
2409
|
+
...expected ? { quoted_total_minor: toMinor(expected.total, currency) } : {}
|
|
2410
|
+
},
|
|
2315
2411
|
headers: { "Idempotency-Key": key }
|
|
2412
|
+
}).catch((err) => {
|
|
2413
|
+
if (keyAlreadyUsed(err)) {
|
|
2414
|
+
throw new GoCommerceError("This order was already placed in another tab. Check that tab for its confirmation.", 400, err.code);
|
|
2415
|
+
}
|
|
2416
|
+
const now = movedTotal(err);
|
|
2417
|
+
if (!expected || !now) throw err;
|
|
2418
|
+
const message = `Your total changed to ${shopperAmount(now.total, currency)}. Check the new total and confirm again.`;
|
|
2419
|
+
expected.onChange?.(now, message);
|
|
2420
|
+
throw new GoCommerceError(message, 409, "total_changed", now);
|
|
2316
2421
|
});
|
|
2317
2422
|
if (result.order.access_token) {
|
|
2318
2423
|
rememberOrder({
|
|
@@ -2335,6 +2440,21 @@ var CheckoutService = class extends BaseService {
|
|
|
2335
2440
|
};
|
|
2336
2441
|
var REDIRECT_GATEWAYS = /* @__PURE__ */ new Set(["adyen", "paddle", "lemonsqueezy", "creem", "hyperswitch", "revenuecat"]);
|
|
2337
2442
|
var unsupported = (method, why) => new NotSupportedError("CheckoutService", method, why);
|
|
2443
|
+
function keyAlreadyUsed(err) {
|
|
2444
|
+
return err instanceof GoCommerceError && err.status === 400 && err.message.includes("Idempotency-Key was already used");
|
|
2445
|
+
}
|
|
2446
|
+
function movedTotal(err) {
|
|
2447
|
+
if (!(err instanceof GoCommerceError) || err.code !== "total_changed") return null;
|
|
2448
|
+
const now = err.details;
|
|
2449
|
+
return now?.total ? toCheckoutQuote(now) : null;
|
|
2450
|
+
}
|
|
2451
|
+
function shopperAmount(amount, currency) {
|
|
2452
|
+
return new Intl.NumberFormat(currency === "INR" ? "en-IN" : "en", {
|
|
2453
|
+
style: "currency",
|
|
2454
|
+
currency,
|
|
2455
|
+
...Number.isInteger(amount) ? { minimumFractionDigits: 0 } : {}
|
|
2456
|
+
}).format(amount);
|
|
2457
|
+
}
|
|
2338
2458
|
function newKey() {
|
|
2339
2459
|
const c = globalThis.crypto;
|
|
2340
2460
|
if (c?.randomUUID) return c.randomUUID();
|
|
@@ -2396,7 +2516,8 @@ var CouponService = class extends BaseService {
|
|
|
2396
2516
|
minAmount: o.minAmount,
|
|
2397
2517
|
description: o.applies ? o.title : o.remaining !== null ? `${o.title}. Add ${o.remaining} more to use this.` : o.reason || o.title,
|
|
2398
2518
|
applies: o.applies,
|
|
2399
|
-
remaining: o.remaining
|
|
2519
|
+
remaining: o.remaining,
|
|
2520
|
+
autoApply: o.autoApply
|
|
2400
2521
|
}));
|
|
2401
2522
|
return { data, count: data.length, pageSize: data.length, noOfPage: 1, page: 1 };
|
|
2402
2523
|
}
|
|
@@ -3152,15 +3273,18 @@ var StoreService = class extends BaseService {
|
|
|
3152
3273
|
async getStoreByIdOrDomain(_args = {}) {
|
|
3153
3274
|
const base = await readStaticStore();
|
|
3154
3275
|
const store = this.store;
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
if (isStoreMissing(err)) {
|
|
3276
|
+
const [plugins, live] = await Promise.all([
|
|
3277
|
+
this.facts.plugins().catch((err) => {
|
|
3278
|
+
if (store && isStoreMissing(err)) {
|
|
3158
3279
|
throw new GoCommerceError(`store not found: no store "${store}" on this engine`, 404, "store_not_found");
|
|
3159
3280
|
}
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3281
|
+
return void 0;
|
|
3282
|
+
}),
|
|
3283
|
+
this.live().catch(() => ({}))
|
|
3284
|
+
]);
|
|
3285
|
+
const known2 = `${this.creds.apiUrl ?? ""}|${store ?? ""}|closed`;
|
|
3286
|
+
const closed = plugins ? keep(known2, closedOf(plugins)) : recall(known2);
|
|
3287
|
+
return { ...base, ...live, closed, id: base.id || "gocommerce" };
|
|
3164
3288
|
}
|
|
3165
3289
|
async getStore(storeId) {
|
|
3166
3290
|
return this.getStoreByIdOrDomain({ storeId });
|
|
@@ -3195,6 +3319,46 @@ var StoreService = class extends BaseService {
|
|
|
3195
3319
|
}
|
|
3196
3320
|
return super.post(path, data);
|
|
3197
3321
|
}
|
|
3322
|
+
/**
|
|
3323
|
+
* A closed store's password (D170), traded for a pass. Never throws. The
|
|
3324
|
+
* engine allows 20 tries an hour per client, and sees this server as the
|
|
3325
|
+
* client unless `clientAddress`, the shopper's, travels as `X-Forwarded-For`.
|
|
3326
|
+
*/
|
|
3327
|
+
async unlockStore(password, { clientAddress } = {}) {
|
|
3328
|
+
const typed = password.trim();
|
|
3329
|
+
if (!typed) return { outcome: "wrong" };
|
|
3330
|
+
try {
|
|
3331
|
+
return { outcome: "unlocked", pass: await this.storePass({ password: typed }, clientAddress) };
|
|
3332
|
+
} catch (err) {
|
|
3333
|
+
return { outcome: err instanceof GoCommerceError && UNLOCK_REFUSED[err.status] || "unavailable" };
|
|
3334
|
+
}
|
|
3335
|
+
}
|
|
3336
|
+
/**
|
|
3337
|
+
* A pass checked without spending a try: the pass, or null when it is wrong,
|
|
3338
|
+
* expired, ended by a new password, or the store takes none now. Throws when
|
|
3339
|
+
* the engine cannot say.
|
|
3340
|
+
*/
|
|
3341
|
+
async checkStorePass(token) {
|
|
3342
|
+
if (!token) return null;
|
|
3343
|
+
try {
|
|
3344
|
+
return await this.storePass({ token });
|
|
3345
|
+
} catch (err) {
|
|
3346
|
+
if (err instanceof GoCommerceError && (err.status === 422 || err.status === 404)) return null;
|
|
3347
|
+
throw err;
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
async storePass(body, clientAddress) {
|
|
3351
|
+
const answer = await this.data("/api/store/password", {
|
|
3352
|
+
method: "POST",
|
|
3353
|
+
body,
|
|
3354
|
+
headers: clientAddress && ONE_IP_ADDRESS.test(clientAddress) ? { "X-Forwarded-For": clientAddress } : void 0
|
|
3355
|
+
});
|
|
3356
|
+
const expiresAt = new Date(answer?.expires_at ?? NaN);
|
|
3357
|
+
if (!answer?.token || Number.isNaN(expiresAt.getTime())) {
|
|
3358
|
+
throw new GoCommerceError("The commerce engine answered a store pass without its token or expiry.", 502);
|
|
3359
|
+
}
|
|
3360
|
+
return { token: answer.token, expiresAt };
|
|
3361
|
+
}
|
|
3198
3362
|
async live() {
|
|
3199
3363
|
const [ready, plugins, menus, base, profile, ships] = await Promise.all([
|
|
3200
3364
|
this.facts.ready(),
|
|
@@ -3312,6 +3476,17 @@ var StoreService = class extends BaseService {
|
|
|
3312
3476
|
}
|
|
3313
3477
|
};
|
|
3314
3478
|
var storeService = new StoreService();
|
|
3479
|
+
var ONE_IP_ADDRESS = /^[\da-f.:]{2,45}$/i;
|
|
3480
|
+
var UNLOCK_REFUSED = {
|
|
3481
|
+
422: "wrong",
|
|
3482
|
+
429: "throttled",
|
|
3483
|
+
404: "open"
|
|
3484
|
+
};
|
|
3485
|
+
function closedOf(plugins) {
|
|
3486
|
+
const plugin = plugins?.get("store-close");
|
|
3487
|
+
if (!plugin) return void 0;
|
|
3488
|
+
return { message: String(plugin.settings?.message ?? ""), password: plugin.settings?.password === true };
|
|
3489
|
+
}
|
|
3315
3490
|
var AD_TAGS = [
|
|
3316
3491
|
["meta-pixel", "pixel_id", "facebookPixel"],
|
|
3317
3492
|
["microsoft-clarity", "project_id", "microsoftClarity"],
|
|
@@ -3340,10 +3515,10 @@ function googleTags(plugins) {
|
|
|
3340
3515
|
}
|
|
3341
3516
|
function deliverable(base, ships) {
|
|
3342
3517
|
if (ships.everywhere || !ships.countries.length) return {};
|
|
3343
|
-
const
|
|
3344
|
-
const countries = ships.countries.map((code) =>
|
|
3345
|
-
const
|
|
3346
|
-
return { countries, ...
|
|
3518
|
+
const known2 = new Map((base.countries ?? []).map((c) => [String(c.code).toUpperCase(), c]));
|
|
3519
|
+
const countries = ships.countries.map((code) => known2.get(code) ?? { code, name: regionName(code) });
|
|
3520
|
+
const keep2 = base.country && ships.countries.includes(String(base.country.code).toUpperCase());
|
|
3521
|
+
return { countries, ...keep2 ? {} : { country: countries[0] } };
|
|
3347
3522
|
}
|
|
3348
3523
|
function regionName(code) {
|
|
3349
3524
|
try {
|
|
@@ -3498,7 +3673,8 @@ function toSection(s) {
|
|
|
3498
3673
|
title: c.title,
|
|
3499
3674
|
slug: c.slug,
|
|
3500
3675
|
link: `/collections/${c.slug}`,
|
|
3501
|
-
image: mediaUrl(c.image) ?? null
|
|
3676
|
+
image: mediaUrl(c.image) ?? null,
|
|
3677
|
+
description: c.description || null
|
|
3502
3678
|
}));
|
|
3503
3679
|
}
|
|
3504
3680
|
return out;
|