@misiki/gocommerce-connector 0.29.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/dist/index.cjs CHANGED
@@ -134,8 +134,17 @@ function remember(key, load, ttl = TTL_MS) {
134
134
  });
135
135
  return value;
136
136
  }
137
+ var known = /* @__PURE__ */ new Map();
138
+ function keep(key, value) {
139
+ known.set(key, value);
140
+ return value;
141
+ }
142
+ function recall(key) {
143
+ return known.get(key);
144
+ }
137
145
  function forgetAll() {
138
146
  entries.clear();
147
+ known.clear();
139
148
  }
140
149
 
141
150
  // src/browser.ts
@@ -1120,7 +1129,22 @@ function toCartOffer(o, currency) {
1120
1129
  applies: o.applies,
1121
1130
  amount: o.amount ? toMajor(o.amount) : null,
1122
1131
  remaining: o.remaining ? toMajor(o.remaining) : null,
1123
- reason: o.reason ?? ""
1132
+ reason: o.reason ?? "",
1133
+ autoApply: !!o.auto_apply
1134
+ };
1135
+ }
1136
+ function toCheckoutQuote(q) {
1137
+ const d = q.discount;
1138
+ return {
1139
+ subtotal: toMajor(q.subtotal),
1140
+ shippingCharges: toMajor(q.shipping),
1141
+ shippingMethod: q.shipping_method ?? null,
1142
+ discount: toMajor(d?.amount),
1143
+ coupon: d ? { code: d.code, title: d.title, autoApplied: d.auto_applied, freeShipping: !!d.free_shipping } : null,
1144
+ tax: toMajor(q.tax),
1145
+ taxIncluded: q.tax_inclusive,
1146
+ codCharges: toMajor(q.payment_fee),
1147
+ total: toMajor(q.total)
1124
1148
  };
1125
1149
  }
1126
1150
  function toFreeShipping(f) {
@@ -2128,6 +2152,7 @@ var customizerService = new CustomizerService();
2128
2152
 
2129
2153
  // src/services/checkout-service.ts
2130
2154
  var CheckoutService = class extends BaseService {
2155
+ expected = /* @__PURE__ */ new Map();
2131
2156
  /** Delivery options for the address on the cart: `{ data: [{ id, name, base_rate }] }`. */
2132
2157
  async getShippingRates({ cartId } = {}) {
2133
2158
  const id2 = cartId || storedCartId();
@@ -2299,6 +2324,64 @@ var CheckoutService = class extends BaseService {
2299
2324
  );
2300
2325
  return { methods: options.methods ?? [], currency: options.currency };
2301
2326
  }
2327
+ /**
2328
+ * What checkout would charge for the cart now, by the storefront's method
2329
+ * code (`COD`, `RAZORPAY`, `PAYPAL`) or none yet (D185). Null until the
2330
+ * address step has stored an address, and null from an engine before D185.
2331
+ *
2332
+ * A refusal is checkout's own: the same GoCommerceError, with the same
2333
+ * message, that placing the order would raise now.
2334
+ */
2335
+ async getQuote({ cartId, method = "" } = {}) {
2336
+ const id2 = cartId || storedCartId();
2337
+ if (!id2 || !cartExtras(id2).shippingAddress) return null;
2338
+ const { extras, email, address } = await this.storedCheckout(id2);
2339
+ const code = method.toLowerCase() === "paypal" ? await this.redirectGateway() : method.toLowerCase();
2340
+ try {
2341
+ const quote2 = await this.data("/api/checkout/quote", {
2342
+ query: {
2343
+ cart: id2,
2344
+ country: address.country,
2345
+ state: address.state,
2346
+ shipping_rate_id: extras.shippingRateId || void 0,
2347
+ method: code || void 0,
2348
+ email: email || void 0
2349
+ }
2350
+ });
2351
+ return toCheckoutQuote(quote2);
2352
+ } catch (err) {
2353
+ if (err instanceof GoCommerceError && (err.status === 405 || err.status === 404 && err.message.startsWith("no route for "))) {
2354
+ return null;
2355
+ }
2356
+ throw err;
2357
+ }
2358
+ }
2359
+ /**
2360
+ * The total on screen as the shopper confirms, from getQuote, or null when nothing was quoted
2361
+ * (D185). Every order placed for the cart then carries it, and checkout refuses one whose own
2362
+ * total differs instead of charging it: `onChange` gets checkout's figures now and the sentence
2363
+ * the placing call fails with.
2364
+ */
2365
+ expectTotal({
2366
+ cartId,
2367
+ total,
2368
+ onChange
2369
+ }) {
2370
+ const id2 = cartId || storedCartId();
2371
+ if (!id2) return;
2372
+ if (typeof total === "number") this.expected.set(id2, { total, onChange });
2373
+ else this.expected.delete(id2);
2374
+ }
2375
+ async storedCheckout(cartId) {
2376
+ const id2 = cartId || storedCartId();
2377
+ if (!id2) throw new GoCommerceError("Your bag is empty.", 400);
2378
+ const extras = cartExtras(id2);
2379
+ const ship = extras.shippingAddress;
2380
+ if (!ship) throw new GoCommerceError("Add a delivery address first.", 400);
2381
+ const cart = await this.data(`/api/carts/${encodeURIComponent(id2)}`);
2382
+ const email = cart.email || (ship.email ? String(ship.email) : "");
2383
+ return { id: id2, extras, email, address: toEngineAddress(ship), currency: cart.currency };
2384
+ }
2302
2385
  async redirectGateway() {
2303
2386
  const { methods } = await this.listMethods();
2304
2387
  return methods.find((m) => REDIRECT_GATEWAYS.has(m.code))?.code;
@@ -2314,15 +2397,8 @@ var CheckoutService = class extends BaseService {
2314
2397
  * the engine refuses a key replayed with a different body.
2315
2398
  */
2316
2399
  async place(code, cartId, extra = {}) {
2317
- const id2 = cartId || storedCartId();
2318
- if (!id2) throw new GoCommerceError("Your bag is empty.", 400);
2319
- const extras = cartExtras(id2);
2320
- const ship = extras.shippingAddress;
2321
- if (!ship) throw new GoCommerceError("Add a delivery address first.", 400);
2322
- const cart = await this.data(`/api/carts/${encodeURIComponent(id2)}`);
2323
- const email = cart.email || (ship.email ? String(ship.email) : "");
2400
+ const { id: id2, extras, email, address, currency } = await this.storedCheckout(cartId);
2324
2401
  if (!email) throw new GoCommerceError("Add an email address so we can send your receipt.", 400);
2325
- const address = toEngineAddress(ship);
2326
2402
  const body = {
2327
2403
  cart_id: id2,
2328
2404
  email,
@@ -2345,10 +2421,23 @@ var CheckoutService = class extends BaseService {
2345
2421
  ...meta ? { meta_capi: meta } : {},
2346
2422
  ...attribution ? { attribution } : {}
2347
2423
  };
2424
+ const expected = this.expected.get(id2);
2348
2425
  const result = await this.data(`/api/checkout/${encodeURIComponent(code)}`, {
2349
2426
  method: "POST",
2350
- body: Object.keys(metadata).length ? { ...body, metadata } : body,
2427
+ body: {
2428
+ ...Object.keys(metadata).length ? { ...body, metadata } : body,
2429
+ ...expected ? { quoted_total_minor: toMinor(expected.total, currency) } : {}
2430
+ },
2351
2431
  headers: { "Idempotency-Key": key }
2432
+ }).catch((err) => {
2433
+ if (keyAlreadyUsed(err)) {
2434
+ throw new GoCommerceError("This order was already placed in another tab. Check that tab for its confirmation.", 400, err.code);
2435
+ }
2436
+ const now = movedTotal(err);
2437
+ if (!expected || !now) throw err;
2438
+ const message = `Your total changed to ${shopperAmount(now.total, currency)}. Check the new total and confirm again.`;
2439
+ expected.onChange?.(now, message);
2440
+ throw new GoCommerceError(message, 409, "total_changed", now);
2352
2441
  });
2353
2442
  if (result.order.access_token) {
2354
2443
  rememberOrder({
@@ -2371,6 +2460,21 @@ var CheckoutService = class extends BaseService {
2371
2460
  };
2372
2461
  var REDIRECT_GATEWAYS = /* @__PURE__ */ new Set(["adyen", "paddle", "lemonsqueezy", "creem", "hyperswitch", "revenuecat"]);
2373
2462
  var unsupported = (method, why) => new NotSupportedError("CheckoutService", method, why);
2463
+ function keyAlreadyUsed(err) {
2464
+ return err instanceof GoCommerceError && err.status === 400 && err.message.includes("Idempotency-Key was already used");
2465
+ }
2466
+ function movedTotal(err) {
2467
+ if (!(err instanceof GoCommerceError) || err.code !== "total_changed") return null;
2468
+ const now = err.details;
2469
+ return now?.total ? toCheckoutQuote(now) : null;
2470
+ }
2471
+ function shopperAmount(amount, currency) {
2472
+ return new Intl.NumberFormat(currency === "INR" ? "en-IN" : "en", {
2473
+ style: "currency",
2474
+ currency,
2475
+ ...Number.isInteger(amount) ? { minimumFractionDigits: 0 } : {}
2476
+ }).format(amount);
2477
+ }
2374
2478
  function newKey() {
2375
2479
  const c = globalThis.crypto;
2376
2480
  if (c?.randomUUID) return c.randomUUID();
@@ -2432,7 +2536,8 @@ var CouponService = class extends BaseService {
2432
2536
  minAmount: o.minAmount,
2433
2537
  description: o.applies ? o.title : o.remaining !== null ? `${o.title}. Add ${o.remaining} more to use this.` : o.reason || o.title,
2434
2538
  applies: o.applies,
2435
- remaining: o.remaining
2539
+ remaining: o.remaining,
2540
+ autoApply: o.autoApply
2436
2541
  }));
2437
2542
  return { data, count: data.length, pageSize: data.length, noOfPage: 1, page: 1 };
2438
2543
  }
@@ -3185,15 +3290,18 @@ var StoreService = class extends BaseService {
3185
3290
  async getStoreByIdOrDomain(_args = {}) {
3186
3291
  const base = await readStaticStore();
3187
3292
  const store = this.store;
3188
- if (store) {
3189
- await this.facts.plugins().catch((err) => {
3190
- if (isStoreMissing(err)) {
3293
+ const [plugins, live] = await Promise.all([
3294
+ this.facts.plugins().catch((err) => {
3295
+ if (store && isStoreMissing(err)) {
3191
3296
  throw new GoCommerceError(`store not found: no store "${store}" on this engine`, 404, "store_not_found");
3192
3297
  }
3193
- });
3194
- }
3195
- const live = await this.live().catch(() => ({}));
3196
- return { ...base, ...live, id: base.id || "gocommerce" };
3298
+ return void 0;
3299
+ }),
3300
+ this.live().catch(() => ({}))
3301
+ ]);
3302
+ const known2 = `${this.creds.apiUrl ?? ""}|${store ?? ""}|closed`;
3303
+ const closed = plugins ? keep(known2, closedOf(plugins)) : recall(known2);
3304
+ return { ...base, ...live, closed, id: base.id || "gocommerce" };
3197
3305
  }
3198
3306
  async getStore(storeId) {
3199
3307
  return this.getStoreByIdOrDomain({ storeId });
@@ -3228,6 +3336,46 @@ var StoreService = class extends BaseService {
3228
3336
  }
3229
3337
  return super.post(path, data);
3230
3338
  }
3339
+ /**
3340
+ * A closed store's password (D170), traded for a pass. Never throws. The
3341
+ * engine allows 20 tries an hour per client, and sees this server as the
3342
+ * client unless `clientAddress`, the shopper's, travels as `X-Forwarded-For`.
3343
+ */
3344
+ async unlockStore(password, { clientAddress } = {}) {
3345
+ const typed = password.trim();
3346
+ if (!typed) return { outcome: "wrong" };
3347
+ try {
3348
+ return { outcome: "unlocked", pass: await this.storePass({ password: typed }, clientAddress) };
3349
+ } catch (err) {
3350
+ return { outcome: err instanceof GoCommerceError && UNLOCK_REFUSED[err.status] || "unavailable" };
3351
+ }
3352
+ }
3353
+ /**
3354
+ * A pass checked without spending a try: the pass, or null when it is wrong,
3355
+ * expired, ended by a new password, or the store takes none now. Throws when
3356
+ * the engine cannot say.
3357
+ */
3358
+ async checkStorePass(token) {
3359
+ if (!token) return null;
3360
+ try {
3361
+ return await this.storePass({ token });
3362
+ } catch (err) {
3363
+ if (err instanceof GoCommerceError && (err.status === 422 || err.status === 404)) return null;
3364
+ throw err;
3365
+ }
3366
+ }
3367
+ async storePass(body, clientAddress) {
3368
+ const answer = await this.data("/api/store/password", {
3369
+ method: "POST",
3370
+ body,
3371
+ headers: clientAddress && ONE_IP_ADDRESS.test(clientAddress) ? { "X-Forwarded-For": clientAddress } : void 0
3372
+ });
3373
+ const expiresAt = new Date(answer?.expires_at ?? NaN);
3374
+ if (!answer?.token || Number.isNaN(expiresAt.getTime())) {
3375
+ throw new GoCommerceError("The commerce engine answered a store pass without its token or expiry.", 502);
3376
+ }
3377
+ return { token: answer.token, expiresAt };
3378
+ }
3231
3379
  async live() {
3232
3380
  const [ready, plugins, menus, base, profile, ships] = await Promise.all([
3233
3381
  this.facts.ready(),
@@ -3345,6 +3493,17 @@ var StoreService = class extends BaseService {
3345
3493
  }
3346
3494
  };
3347
3495
  var storeService = new StoreService();
3496
+ var ONE_IP_ADDRESS = /^[\da-f.:]{2,45}$/i;
3497
+ var UNLOCK_REFUSED = {
3498
+ 422: "wrong",
3499
+ 429: "throttled",
3500
+ 404: "open"
3501
+ };
3502
+ function closedOf(plugins) {
3503
+ const plugin = plugins?.get("store-close");
3504
+ if (!plugin) return void 0;
3505
+ return { message: String(plugin.settings?.message ?? ""), password: plugin.settings?.password === true };
3506
+ }
3348
3507
  var AD_TAGS = [
3349
3508
  ["meta-pixel", "pixel_id", "facebookPixel"],
3350
3509
  ["microsoft-clarity", "project_id", "microsoftClarity"],
@@ -3373,10 +3532,10 @@ function googleTags(plugins) {
3373
3532
  }
3374
3533
  function deliverable(base, ships) {
3375
3534
  if (ships.everywhere || !ships.countries.length) return {};
3376
- const known = new Map((base.countries ?? []).map((c) => [String(c.code).toUpperCase(), c]));
3377
- const countries = ships.countries.map((code) => known.get(code) ?? { code, name: regionName(code) });
3378
- const keep = base.country && ships.countries.includes(String(base.country.code).toUpperCase());
3379
- return { countries, ...keep ? {} : { country: countries[0] } };
3535
+ const known2 = new Map((base.countries ?? []).map((c) => [String(c.code).toUpperCase(), c]));
3536
+ const countries = ships.countries.map((code) => known2.get(code) ?? { code, name: regionName(code) });
3537
+ const keep2 = base.country && ships.countries.includes(String(base.country.code).toUpperCase());
3538
+ return { countries, ...keep2 ? {} : { country: countries[0] } };
3380
3539
  }
3381
3540
  function regionName(code) {
3382
3541
  try {
@@ -3531,7 +3690,8 @@ function toSection(s) {
3531
3690
  title: c.title,
3532
3691
  slug: c.slug,
3533
3692
  link: `/collections/${c.slug}`,
3534
- image: mediaUrl(c.image) ?? null
3693
+ image: mediaUrl(c.image) ?? null,
3694
+ description: c.description || null
3535
3695
  }));
3536
3696
  }
3537
3697
  return out;
@@ -4250,6 +4410,7 @@ exports.toCart = toCart;
4250
4410
  exports.toCartLine = toCartLine;
4251
4411
  exports.toCartOffer = toCartOffer;
4252
4412
  exports.toCategory = toCategory;
4413
+ exports.toCheckoutQuote = toCheckoutQuote;
4253
4414
  exports.toCmsPage = toCmsPage;
4254
4415
  exports.toCollection = toCollection;
4255
4416
  exports.toEngineAddress = toEngineAddress;