You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
N/A (backend behaviour; reproduced via the admin dashboard and via the API directly)
What happended?
Since v2.17.0 (#15085, async payment methods) a payment provider can return pending_authorization from authorizePayment, the cart completes with an "awaiting" order, and an admin can later click Check status
on the order page. That button calls POST /admin/orders/:id/payment-sessions/authorize, which runs authorizePaymentSessionForOrderWorkflow.
When the provider now reports the payment as captured (which is what @medusajs/payment-stripe does: getStatus() maps a succeeded PaymentIntent to PaymentSessionStatus.CAPTURED, and authorizePayment
delegates to getPaymentStatus), the workflow:
The order's payment_status becomes captured and the payment collection is completed, but payment.captured is never emitted. In core, that event is emitted only by emitEventStep inside capturePaymentWorkflow (packages/core/core-flows/src/payment/workflows/capture-payment.ts), and authorizePaymentSessionForOrderWorkflow
(packages/core/core-flows/src/payment/workflows/authorize-payment-session-for-order.ts) never invokes it.
Every other place where core materialises a capture does go through capturePaymentWorkflow, so the event
fires even when the provider auto-captured and the provider call is skipped:
processPaymentWorkflow, capture-payment-autocapture branch (authorizePaymentSessionStep immediately
followed by capturePaymentWorkflow.runAsStep);
processPaymentWorkflow, capture-payment branch;
markPaymentCollectionAsPaidWorkflow;
POST /admin/payments/:id/capture.
The admin "Check status" path is the only one that creates a capture silently. That matters because the
button exists precisely for the case where the provider's webhook did not arrive, so there is no later processPaymentWorkflow run to emit the event. Anything subscribed to payment.captured (fulfilment of
digital goods, invoicing, notifications, tax reporting, etc.) never runs for such orders.
This is the same class of problem as #9252 (webhook route not emitting payment.captured), which was fixed
in core rather than delegated to providers. Providers cannot work around it: AbstractPaymentProvider
returns only { status, data } and has no access to the event bus.
Related: #15085 introduced the workflow; #14073 made authorizePayment → CAPTURED an officially supported
provider pattern; #16722 is currently hardening the neighbouring autocapture branch of processPaymentWorkflow.
Expected behavior
POST /admin/orders/:id/payment-sessions/authorize should emit payment.captured whenever the
authorization results in a captured payment, exactly as the webhook path does. The simplest fix mirrors the capture-payment-autocapture branch of process-payment.ts: after authorizePaymentSessionStep, run capturePaymentWorkflow.runAsStep({ input: { payment_id: payment.id } }) when the returned payment has captured_at set (or captures). The module already short-circuits in capturePayment_ when captured_at
is set and skips the provider call, and addOrderTransactionStep dedupes by reference_id, so this is safe
to run after the existing transaction step. Alternatively, emit PaymentEvents.CAPTURED with { id: payment.id } directly.
Actual behavior
After the button click the order shows payment_status: "captured", payment_collections[0].payments[0].captures has one row, payment_collections[0].status is completed,
and no payment.captured event is published. If the provider's payment_intent.succeeded / checkout.session.completed webhook is later redelivered, processPaymentWorkflow runs capturePaymentWorkflow and the event fires at that point, which confirms that only the admin path is
missing it.
Verified against the installed @medusajs/core-flows@2.17.2 dist (no emitEventStep or capturePaymentWorkflow reference in authorize-payment-session-for-order.js) and against the current develop source; the file has not changed since #15085.
Package.json file
{ "name": "medusa-payment-captured-repro", "version": "0.0.1", "private": true, "license": "MIT", "scripts": { "build": "medusa build", "start": "medusa start", "dev": "medusa develop", "test:integration:http": "TEST_TYPE=integration:http NODE_OPTIONS=--experimental-vm-modules jest --silent=false --runInBand --forceExit" }, "dependencies": { "@medusajs/admin-sdk": "2.20.1", "@medusajs/cli": "2.20.1", "@medusajs/framework": "2.20.1", "@medusajs/medusa": "2.20.1" }, "devDependencies": { "@medusajs/test-utils": "2.20.1", "@swc/core": "^1.7.28", "@swc/jest": "^0.2.36", "@types/jest": "^29.5.14", "@types/node": "^20.12.11", "jest": "^29.7.0", "pg-god": "^1.0.12", "ts-node": "^10.9.2", "typescript": "^5.6.2" }, "engines": { "node": ">=20" } }Node.js version
v24.13.1
Database and its version
PostgreSQL 18
Operating system name and version
macOS Tahoe 26.5
Browser name
N/A (backend behaviour; reproduced via the admin dashboard and via the API directly)
What happended?
Since v2.17.0 (#15085, async payment methods) a payment provider can return
pending_authorizationfromauthorizePayment, the cart completes with an "awaiting" order, and an admin can later click Check statuson the order page. That button calls
POST /admin/orders/:id/payment-sessions/authorize, which runsauthorizePaymentSessionForOrderWorkflow.When the provider now reports the payment as captured (which is what
@medusajs/payment-stripedoes:getStatus()maps asucceededPaymentIntent toPaymentSessionStatus.CAPTURED, andauthorizePaymentdelegates to
getPaymentStatus), the workflow:authorizePaymentSessionStep→PaymentModuleService.authorizePaymentSessiontreatsCAPTUREDasauthorized + auto-captured (the
is_capturedpath from fix(payment): Double idempotent capture called with auto capture beha… #14073), creates thePaymentand aCapturerow,sets
captured_at;addOrderTransactionStepfor those captures;The order's
payment_statusbecomescapturedand the payment collection is completed, butpayment.capturedis never emitted. In core, that event is emitted only byemitEventStepinsidecapturePaymentWorkflow(packages/core/core-flows/src/payment/workflows/capture-payment.ts), andauthorizePaymentSessionForOrderWorkflow(
packages/core/core-flows/src/payment/workflows/authorize-payment-session-for-order.ts) never invokes it.Every other place where core materialises a capture does go through
capturePaymentWorkflow, so the eventfires even when the provider auto-captured and the provider call is skipped:
processPaymentWorkflow,capture-payment-autocapturebranch (authorizePaymentSessionStepimmediatelyfollowed by
capturePaymentWorkflow.runAsStep);processPaymentWorkflow,capture-paymentbranch;markPaymentCollectionAsPaidWorkflow;POST /admin/payments/:id/capture.The admin "Check status" path is the only one that creates a capture silently. That matters because the
button exists precisely for the case where the provider's webhook did not arrive, so there is no later
processPaymentWorkflowrun to emit the event. Anything subscribed topayment.captured(fulfilment ofdigital goods, invoicing, notifications, tax reporting, etc.) never runs for such orders.
This is the same class of problem as #9252 (webhook route not emitting
payment.captured), which was fixedin core rather than delegated to providers. Providers cannot work around it:
AbstractPaymentProviderreturns only
{ status, data }and has no access to the event bus.Related: #15085 introduced the workflow; #14073 made
authorizePayment → CAPTUREDan officially supportedprovider pattern; #16722 is currently hardening the neighbouring autocapture branch of
processPaymentWorkflow.Expected behavior
POST /admin/orders/:id/payment-sessions/authorizeshould emitpayment.capturedwhenever theauthorization results in a captured payment, exactly as the webhook path does. The simplest fix mirrors the
capture-payment-autocapturebranch ofprocess-payment.ts: afterauthorizePaymentSessionStep, runcapturePaymentWorkflow.runAsStep({ input: { payment_id: payment.id } })when the returned payment hascaptured_atset (or captures). The module already short-circuits incapturePayment_whencaptured_atis set and skips the provider call, and
addOrderTransactionStepdedupes byreference_id, so this is safeto run after the existing transaction step. Alternatively, emit
PaymentEvents.CAPTUREDwith{ id: payment.id }directly.Actual behavior
After the button click the order shows
payment_status: "captured",payment_collections[0].payments[0].captureshas one row,payment_collections[0].statusiscompleted,and no
payment.capturedevent is published. If the provider'spayment_intent.succeeded/checkout.session.completedwebhook is later redelivered,processPaymentWorkflowrunscapturePaymentWorkflowand the event fires at that point, which confirms that only the admin path ismissing it.
Verified against the installed
@medusajs/core-flows@2.17.2dist (noemitEventSteporcapturePaymentWorkflowreference inauthorize-payment-session-for-order.js) and against the currentdevelopsource; the file has not changed since #15085.Link to reproduction repo
https://github.com/mczech-tsh/medusa-payment-captured-repro/