@misiki/gocommerce-connector 0.31.0 → 0.32.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.
@@ -2196,9 +2196,9 @@ var CheckoutService = class extends BaseService {
2196
2196
  }
2197
2197
  // The family's other gateway flows. GoCommerce's Stripe is a Payment
2198
2198
  // Element (a client secret, not a hosted page) — `checkout('stripe')` returns
2199
- // that secret for a storefront that mounts the element. Cashfree, PhonePe and
2200
- // Affirm are not engine gateways at all. paymentMethodService never offers
2201
- // these codes, so they are reached only by a storefront that hard-codes one.
2199
+ // that secret for a storefront that mounts the element. PhonePe and Affirm are
2200
+ // not engine gateways at all. paymentMethodService never offers these codes,
2201
+ // so they are reached only by a storefront that hard-codes one.
2202
2202
  async checkoutStripe() {
2203
2203
  throw unsupported(
2204
2204
  "checkoutStripe",
@@ -2210,18 +2210,18 @@ var CheckoutService = class extends BaseService {
2210
2210
  }
2211
2211
  /**
2212
2212
  * The storefront's generic redirect flow, which it reaches through the code
2213
- * `PAYPAL`. The engine has no PayPal gateway; paymentMethodService puts the
2214
- * store's first hosted-page gateway — Adyen, Paddle, Lemon Squeezy, Creem,
2215
- * Hyperswitch or RevenueCat — under that code, with its own name and a
2216
- * neutral icon, because this is the one storefront flow that sends a shopper
2217
- * to a URL and back. The shopper returns to the success page; the gateway's
2218
- * webhook tells the engine the order is paid.
2213
+ * `PAYPAL`. paymentMethodService puts the store's first hosted-page gateway
2214
+ * — PayPal, Adyen, Paddle, Lemon Squeezy, Creem, Hyperswitch or RevenueCat —
2215
+ * under that code, with its own name and a neutral icon, because this is the
2216
+ * one storefront flow that sends a shopper to a URL and back. The shopper
2217
+ * returns to the success page; the gateway's webhook tells the engine the
2218
+ * order is paid. PayPal's webhook is also what captures the approved payment.
2219
2219
  */
2220
2220
  async checkoutPaypal({ cartId, origin } = {}) {
2221
2221
  const gateway = await this.redirectGateway();
2222
2222
  if (!gateway) throw unsupported("checkoutPaypal", "this store has no hosted-page payment gateway switched on");
2223
2223
  const id2 = cartId || storedCartId() || "";
2224
- const back = origin ? `${origin.replace(/\/+$/, "")}/checkout/success?cart_id=${encodeURIComponent(id2)}` : void 0;
2224
+ const back = origin ? successUrl(origin, id2) : void 0;
2225
2225
  const result = await this.place(gateway, id2, back ? { return_url: back } : {});
2226
2226
  const url = result.payment.client_data?.url;
2227
2227
  if (!url) throw new GoCommerceError("The payment page did not open. Please try again.", 502);
@@ -2237,11 +2237,40 @@ var CheckoutService = class extends BaseService {
2237
2237
  } = {}) {
2238
2238
  return { order_no: order_no ?? null, status: "processing" };
2239
2239
  }
2240
- async checkoutCashfree() {
2241
- throw unsupported("checkoutCashfree", "the engine has no Cashfree gateway");
2240
+ /**
2241
+ * Cashfree's JavaScript checkout. The engine has already created the Cashfree
2242
+ * order; this hands the storefront the session its modal opens. `order_no` is
2243
+ * the store's number, which the storefront prefers over `order_id`, Cashfree's.
2244
+ *
2245
+ * The return URL is for the one case the modal cannot open, an in-app browser,
2246
+ * when Cashfree sends the shopper back to it instead. Only https is sent: every
2247
+ * Cashfree example is, and a plain-http dev origin should not risk the order.
2248
+ */
2249
+ async checkoutCashfree({ cartId, origin } = {}) {
2250
+ const { id: id2, extras, address } = await this.storedCheckout(cartId);
2251
+ if (!extras.phone && !address.phone) throw new GoCommerceError("Add a phone number to pay with Cashfree.", 400);
2252
+ const result = await this.place("cashfree", id2, origin?.startsWith("https:") ? { return_url: successUrl(origin, id2) } : {});
2253
+ const data = result.payment.client_data ?? {};
2254
+ if (result.payment.kind !== "client_action" || !data.payment_session_id) {
2255
+ throw new GoCommerceError("The payment page did not open. Please try again.", 502);
2256
+ }
2257
+ cashfreeEnvironment = data.environment;
2258
+ return {
2259
+ order_no: result.order.number,
2260
+ payment_session_id: data.payment_session_id,
2261
+ order_id: data.order_id,
2262
+ environment: data.environment
2263
+ };
2242
2264
  }
2243
- async captureCashfreePayment() {
2244
- throw unsupported("captureCashfreePayment", "the engine has no Cashfree gateway");
2265
+ /**
2266
+ * Nothing to capture: the signed webhook tells the engine the order is paid.
2267
+ * The storefront calls this on the server, after Cashfree's modal resolves
2268
+ * with any outcome, so it can neither claim the payment nor, without the
2269
+ * browser's order token, check it. Answering sends the shopper to the success
2270
+ * page; throwing would call a payment that may have gone through failed.
2271
+ */
2272
+ async captureCashfreePayment({ order_no } = {}) {
2273
+ return { order_no: order_no ?? null, status: "processing" };
2245
2274
  }
2246
2275
  async checkoutPhonepe() {
2247
2276
  throw unsupported("checkoutPhonepe", "the engine has no PhonePe gateway");
@@ -2307,7 +2336,7 @@ var CheckoutService = class extends BaseService {
2307
2336
  }
2308
2337
  /**
2309
2338
  * What checkout would charge for the cart now, by the storefront's method
2310
- * code (`COD`, `RAZORPAY`, `PAYPAL`) or none yet (D185). Null until the
2339
+ * code (`COD`, `RAZORPAY`, `CASHFREE`, `PAYPAL`) or none yet (D185). Null until the
2311
2340
  * address step has stored an address, and null from an engine before D185.
2312
2341
  *
2313
2342
  * A refusal is checkout's own: the same GoCommerceError, with the same
@@ -2315,7 +2344,7 @@ var CheckoutService = class extends BaseService {
2315
2344
  */
2316
2345
  async getQuote({ cartId, method = "" } = {}) {
2317
2346
  const id2 = cartId || storedCartId();
2318
- if (!id2 || !cartExtras(id2).shippingAddress) return null;
2347
+ if (!id2 || !cartExtras(id2).shippingAddress || cartExtras(id2).attempt?.placed) return null;
2319
2348
  const { extras, email, address } = await this.storedCheckout(id2);
2320
2349
  const code = method.toLowerCase() === "paypal" ? await this.redirectGateway() : method.toLowerCase();
2321
2350
  try {
@@ -2376,6 +2405,11 @@ var CheckoutService = class extends BaseService {
2376
2405
  * key resumes that order, and without one the shopper is stuck. A changed
2377
2406
  * request — a different address, a different method — gets a new key, since
2378
2407
  * the engine refuses a key replayed with a different body.
2408
+ *
2409
+ * A gateway's modal keeps the key after the order is placed, along with exactly
2410
+ * what placed it. The cart is converted by then, so a shopper who closes the
2411
+ * window can pay again only under this key, and the engine then reopens the same
2412
+ * gateway order rather than risk a second charge on a new one (D232).
2379
2413
  */
2380
2414
  async place(code, cartId, extra = {}) {
2381
2415
  const { id: id2, extras, email, address, currency } = await this.storedCheckout(cartId);
@@ -2392,23 +2426,27 @@ var CheckoutService = class extends BaseService {
2392
2426
  const serialised = JSON.stringify(body);
2393
2427
  const retry = extras.attempt?.body === `${code}:${serialised}`;
2394
2428
  const key = retry ? extras.attempt.key : newKey();
2429
+ const placed = retry ? extras.attempt?.placed : void 0;
2395
2430
  const meta = retry ? extras.attempt?.meta : metaBrowser();
2396
2431
  const attribution = retry ? extras.attempt?.attribution : landingAttribution();
2397
- saveCartExtras(id2, {
2398
- attempt: { key, body: `${code}:${serialised}`, ...meta ? { meta } : {}, ...attribution ? { attribution } : {} }
2399
- });
2432
+ if (!placed) {
2433
+ saveCartExtras(id2, {
2434
+ attempt: { key, body: `${code}:${serialised}`, ...meta ? { meta } : {}, ...attribution ? { attribution } : {} }
2435
+ });
2436
+ }
2400
2437
  const metadata = {
2401
2438
  ...extra.metadata,
2402
2439
  ...meta ? { meta_capi: meta } : {},
2403
2440
  ...attribution ? { attribution } : {}
2404
2441
  };
2405
2442
  const expected = this.expected.get(id2);
2443
+ const sent = placed ?? {
2444
+ ...Object.keys(metadata).length ? { ...body, metadata } : body,
2445
+ ...expected ? { quoted_total_minor: toMinor(expected.total, currency) } : {}
2446
+ };
2406
2447
  const result = await this.data(`/api/checkout/${encodeURIComponent(code)}`, {
2407
2448
  method: "POST",
2408
- body: {
2409
- ...Object.keys(metadata).length ? { ...body, metadata } : body,
2410
- ...expected ? { quoted_total_minor: toMinor(expected.total, currency) } : {}
2411
- },
2449
+ body: sent,
2412
2450
  headers: { "Idempotency-Key": key }
2413
2451
  }).catch((err) => {
2414
2452
  if (keyAlreadyUsed(err)) {
@@ -2435,11 +2473,18 @@ var CheckoutService = class extends BaseService {
2435
2473
  }).catch(() => void 0);
2436
2474
  }
2437
2475
  }
2438
- saveCartExtras(id2, { attempt: null });
2476
+ const reopenable = result.payment.kind === "client_action";
2477
+ saveCartExtras(id2, { attempt: reopenable ? { key, body: `${code}:${serialised}`, placed: sent } : null });
2478
+ if (reopenable && result.order.payment_status === "paid") {
2479
+ throw new GoCommerceError(`Order ${result.order.number} is already paid, so there is nothing more to pay.`, 409, "already_paid");
2480
+ }
2439
2481
  return result;
2440
2482
  }
2441
2483
  };
2442
- var REDIRECT_GATEWAYS = /* @__PURE__ */ new Set(["adyen", "paddle", "lemonsqueezy", "creem", "hyperswitch", "revenuecat"]);
2484
+ var REDIRECT_GATEWAYS = /* @__PURE__ */ new Set(["paypal", "adyen", "paddle", "lemonsqueezy", "creem", "hyperswitch", "revenuecat"]);
2485
+ var cashfreeEnvironment;
2486
+ var cashfreeIsTest = () => cashfreeEnvironment === "sandbox";
2487
+ var successUrl = (origin, cartId) => `${origin.replace(/\/+$/, "")}/checkout/success?cart_id=${encodeURIComponent(cartId)}`;
2443
2488
  var unsupported = (method, why) => new NotSupportedError("CheckoutService", method, why);
2444
2489
  function keyAlreadyUsed(err) {
2445
2490
  return err instanceof GoCommerceError && err.status === 400 && err.message.includes("Idempotency-Key was already used");
@@ -2466,7 +2511,8 @@ var checkoutService = new CheckoutService();
2466
2511
  // src/services/payment-method-service.ts
2467
2512
  var DRIVABLE = {
2468
2513
  cod: { code: "COD", img: "/payment/cod.svg" },
2469
- razorpay: { code: "RAZORPAY" }
2514
+ razorpay: { code: "RAZORPAY" },
2515
+ cashfree: { code: "CASHFREE", img: "/payment/card.svg" }
2470
2516
  };
2471
2517
  var reported2 = /* @__PURE__ */ new Set();
2472
2518
  var PaymentMethodService = class extends BaseService {
@@ -2475,7 +2521,12 @@ var PaymentMethodService = class extends BaseService {
2475
2521
  const options = await this.facts.checkoutOptions();
2476
2522
  const methods = options.methods ?? [];
2477
2523
  const redirect = methods.find((m) => REDIRECT_GATEWAYS.has(m.code))?.code;
2478
- const drive = (code) => DRIVABLE[code] ?? (code === redirect ? { code: "PAYPAL", img: "/payment/card.svg" } : void 0);
2524
+ const drive = (code) => {
2525
+ if (code === "cashfree" && options.currency !== "INR") return void 0;
2526
+ if (DRIVABLE[code]) return DRIVABLE[code];
2527
+ if (code === redirect) return { code: "PAYPAL", img: "/payment/card.svg" };
2528
+ return REDIRECT_GATEWAYS.has(code) ? { code: code.toUpperCase(), img: "/payment/card.svg" } : void 0;
2529
+ };
2479
2530
  const data = methods.filter((m) => {
2480
2531
  if (drive(m.code)) return true;
2481
2532
  if (!reported2.has(m.code)) {
@@ -2485,19 +2536,24 @@ var PaymentMethodService = class extends BaseService {
2485
2536
  );
2486
2537
  }
2487
2538
  return false;
2488
- }).map((m, i) => ({
2489
- id: m.code,
2490
- code: drive(m.code).code,
2491
- engineCode: m.code,
2492
- name: m.name,
2493
- title: m.name,
2494
- description: "",
2495
- img: drive(m.code).img ?? null,
2496
- fee: toMajor(m.fee),
2497
- rank: i,
2498
- active: true,
2499
- isActive: true
2500
- }));
2539
+ }).map((m, i) => {
2540
+ const method = {
2541
+ id: m.code,
2542
+ code: drive(m.code).code,
2543
+ engineCode: m.code,
2544
+ name: m.name,
2545
+ title: m.name,
2546
+ description: "",
2547
+ img: drive(m.code).img ?? null,
2548
+ fee: toMajor(m.fee),
2549
+ rank: i,
2550
+ active: true,
2551
+ isActive: true
2552
+ };
2553
+ if (REDIRECT_GATEWAYS.has(m.code) && m.code !== redirect) Object.assign(method, { redirects: true });
2554
+ if (m.code === "cashfree") Object.defineProperty(method, "isTest", { get: cashfreeIsTest, enumerable: true });
2555
+ return method;
2556
+ });
2501
2557
  return { data, count: data.length, pageSize: data.length, noOfPage: 1, page: 1 };
2502
2558
  }
2503
2559
  };
@@ -4326,6 +4382,7 @@ exports.b2bService = b2bService;
4326
4382
  exports.bannerService = bannerService;
4327
4383
  exports.blogService = blogService;
4328
4384
  exports.cartService = cartService;
4385
+ exports.cashfreeIsTest = cashfreeIsTest;
4329
4386
  exports.categoryService = categoryService;
4330
4387
  exports.chatService = chatService;
4331
4388
  exports.checkoutService = checkoutService;