Skip to content

[Bug]: core stripe payment integration - double /capture api #13301

@420coupe

Description

@420coupe

Package.json file

{
  "name": "medusa-starter-default",
  "version": "0.0.1",
  "description": "A starter for Medusa projects.",
  "author": "Medusa (https://medusajs.com)",
  "license": "MIT",
  "keywords": [
    "sqlite",
    "postgres",
    "typescript",
    "ecommerce",
    "headless",
    "medusa"
  ],
  "scripts": {
    "build": "medusa build",
    "seed": "medusa exec ./src/scripts/seed.ts",
    "start": "medusa start -p 9777",
    "dev": "medusa develop -p 9777",
    "test:integration:http": "TEST_TYPE=integration:http NODE_OPTIONS=--experimental-vm-modules jest --silent=false --runInBand --forceExit",
    "test:integration:modules": "TEST_TYPE=integration:modules NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit",
    "test:unit": "TEST_TYPE=unit NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit",
    "full-cost-reconcile": "node -r dotenv/config -r ts-node/register src/scripts/full-cost-reconciliation.ts",
    "per-message-costs": "node -r dotenv/config -r ts-node/register src/scripts/per-message-cost-tracker.ts",
    "monitor-db-pool": "node -r dotenv/config -r ts-node/register src/scripts/monitor-db-pool.ts",
    "backfill-tiers": "medusa exec ./src/scripts/backfill-customer-tiers-merged.ts"
  },
  "dependencies": {
    "@medusajs/admin-sdk": "latest",
    "@medusajs/cli": "latest",
    "@medusajs/framework": "latest",
    "@medusajs/medusa": "latest",
    "@mikro-orm/core": "6.4.3",
    "@mikro-orm/knex": "6.4.3",
    "@mikro-orm/migrations": "6.4.3",
    "@mikro-orm/postgresql": "6.4.3",
    "@sendgrid/eventwebhook": "^8.0.0",
    "awilix": "^8.0.1",
    "multer": "^1.4.5-lts.1",
    "pg": "^8.13.0",
    "twilio": "^5.3.3"
  },
  "devDependencies": {
    "@medusajs/test-utils": "latest",
    "@mikro-orm/cli": "6.4.3",
    "@swc/core": "1.5.7",
    "@swc/jest": "^0.2.36",
    "@types/jest": "^29.5.13",
    "@types/multer": "^1.4.12",
    "@types/node": "^20.0.0",
    "@types/react": "^18.3.2",
    "@types/react-dom": "^18.2.25",
    "jest": "^29.7.0",
    "prop-types": "^15.8.1",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "ts-node": "^10.9.2",
    "typescript": "^5.6.2",
    "vite": "^5.2.11",
    "yalc": "^1.0.0-pre.53"
  },
  "engines": {
    "node": ">=20"
  },
  "packageManager": "[email protected]+sha512.f26f951f67de0c6a33ee381e5ff364709c87e70eb5e65c694e4facde3512f1fa80b8679e6ba31ce7d340fbb46f08dd683af9457e240f25a204be7427940d767e"
}

Node.js version

v20.18.1

Database and its version

PostgreSQL 15.6

Operating system name and version

Ubuntu 25.04

Browser name

Brave

What happended?

The stripe endpoint /capture is called twice on a processPaymentWorkflow event whether manual or automatic capture from medusa-config.ts. I even tested with two different version frontend package versions and see the results below:

Default versions used by medusajs packages (core medusa stripe integration and nextjs-starter)

backend medusajs: 
@medusajs/[email protected] invalid: "latest" from the root project
  └─┬ @medusajs/[email protected]
    └── [email protected]
frontend nextjs:
├── @stripe/[email protected]
├── @stripe/[email protected]

Here find the following: Multiple calls to stripe /capture api endpoint

  1. top manual capture
    "capture_method": "manual"
  2. bottom automatic
    "capture_method": "automatic"
Image

Different package version frontend

backend medusajs: 
@medusajs/[email protected] invalid: "latest" from the root project
  └─┬ @medusajs/[email protected]
    └── [email protected]
frontend nextjs:
├── @stripe/[email protected]
├── @stripe/[email protected]

Here find the following: mutiple calls to strpipe /capture api endpoint on manual, 1 /capture and 1 /cancel on auto

  1. top manual capture
    "capture_method": "manual"
  2. bottom automatic
    "capture_method": "automatic"
Image

Payment is successfully captured in both scenarios with both package versions. However at times there can occur a race condition between the capture events and the .confirmPayment on the nextjs frontend leading to a refund after an order is successfully processed and fulfilled in the case of digital products.

Expected behavior

  1. payment intent created
  2. .confirmPayment - if capture: true this is where payment is collected as "capture_method": "automatic" is passed due to the config below in medusa-config.ts
{
      resolve: "@medusajs/medusa/payment",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/payment-stripe",
            id: "stripe",
            options: {
              apiKey: process.env.STRIPE_API_KEY,
              webhookSecret: process.env.STRIPE_WEBHOOK,
              automatic_payment_methods: true,
              capture: true,
            },
          },
        ],
      },
    },
  1. order created after success response from .confirmPayment
  2. There would be no need for the /capture api calls if capture: true

Actual behavior

  1. payment intent created
  2. .confirmPayment - payment is captured - race conditions occur 1/1000
  3. order created after success response from .confirmPayment
  4. processPaymentWorkflow - /capture api call
  5. processPaymentWorkflow - /capture api call (from webhook response)

Link to reproduction repo

https://github.com/medusajs/medusa-starter-default/

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions