Package.json file
Minimal dependency example for a disposable reproduction application, using public upstream packages:
{
"dependencies": {
"@medusajs/framework": "2.20.1",
"@medusajs/medusa": "2.20.1"
}
}
I inspected the publicly published @medusajs/core-flows and @medusajs/payment packages at 2.18.0 and 2.20.1, the latest stable release on 2026-09-07. Their npm SHA-512 integrity values were verified before inspection. The workflow's JavaScript, its declaration, and the update step are identical between these versions; the Payment Module's result-selection expression is also unchanged. This report is a published-package/source audit, not a database-backed execution. It contains only public upstream implementation details and a synthetic reproduction example.
Node.js version
Not applicable to the source audit; no Medusa application was executed.
Database and its version
Not applicable; no database was used for the source audit.
Operating system name and version
Not applicable; the reported mismatch is in the publicly distributed JavaScript and declarations.
Browser name
Not applicable; server-side workflow.
What happened?
createOrUpdateOrderPaymentCollectionWorkflow declares PaymentCollectionDTO[] as its result, but its update branch returns one DTO and its no-op path returns undefined.
This breaks a caller that uses the declared array contract, for example result[0].id, when retrying an order with an existing not_paid collection. Fakes that always return arrays conceal the mismatch.
Expected behavior
The runtime result and published type should agree in every branch. One option is to preserve the array contract: return an array from the update path and [] from the no-op path. Another is to declare and document the actual union throughout the workflow and step APIs. Normalizing only the update branch leaves the no-op mismatch unresolved.
The Payment Module's selector overload independently declares an array return and should preserve the records it updates, while the ID overload should retain its single-record contract.
Actual behavior
Assuming the input amount passes validation:
| Selected collection |
Order pending amount |
Branch |
Result |
| None |
Positive |
Create |
PaymentCollectionDTO[] |
not_paid / awaiting |
Positive or zero |
Update |
PaymentCollectionDTO |
| None |
Zero |
Neither |
undefined |
authorized / partially_authorized |
Positive |
Recreate and cancel previous |
PaymentCollectionDTO[] |
authorized / partially_authorized |
Zero |
Neither |
undefined |
| Any of the above |
Negative, with omitted/zero input amount |
Neither |
undefined |
Zero pending does not always mean no-op: the update predicate uses gte, whereas creation uses gt.
Source at the immutable 2.20.1 commit 0ed927bdb7a396a8fd5f6447ed69b7b354b150d3:
- The workflow branches and final transform assert array types, then forward
updatedPaymentCollections || createdPaymentCollection without normalization.
- The update step calls
updatePaymentCollections(selector, update) and forwards its return value.
- The Payment Module overloads and implementation choose
Array.isArray(data) ? result : result[0]. Here data is the update payload, an object, regardless of whether the first argument is a selector. The selector overload therefore returns only the first record instead of the declared array.
The published @medusajs/core-flows@2.20.1 file dist/order/workflows/create-or-update-order-payment-collection.d.ts still declares ReturnWorkflow<{ order_id: string; amount?: number }, PaymentCollectionDTO[], []>.
Reproduction
Use a disposable Medusa application/database with the standard Order, Payment, and link modules, first on 2.18.0 and then with all Medusa packages aligned to 2.20.1. The following diagnostic is supplied for a database-backed reproduction; it was not executed as part of the newer-version source audit. Save it as src/scripts/reproduce-payment-collection-shape.ts and run pnpm exec medusa exec ./src/scripts/reproduce-payment-collection-shape.ts.
The script creates two synthetic orders and one payment collection. It does not create payment sessions or invoke a payment provider. Dispose of the test database afterward.
import type { ExecArgs, IOrderModuleService } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
import { createOrUpdateOrderPaymentCollectionWorkflow } from "@medusajs/medusa/core-flows"
function describe(result: unknown) {
if (Array.isArray(result)) return "array"
if (result === undefined) return "undefined"
if (result === null) return "null"
return typeof result
}
export default async function reproduce({ container }: ExecArgs) {
const orders = container.resolve<IOrderModuleService>(Modules.ORDER)
const workflow = createOrUpdateOrderPaymentCollectionWorkflow(container)
const order = await orders.createOrders({
currency_code: "usd",
items: [{ title: "Shape reproduction", quantity: 1, unit_price: 100 }],
})
const created = await workflow.run({ input: { order_id: order.id, amount: 100 } })
const updated = await workflow.run({ input: { order_id: order.id, amount: 20 } })
const omitted = await workflow.run({ input: { order_id: order.id } })
const freeOrder = await orders.createOrders({
currency_code: "usd",
items: [{ title: "No-op reproduction", quantity: 1, unit_price: 0 }],
})
const absent = await workflow.run({ input: { order_id: freeOrder.id, amount: 0 } })
console.log({
create: describe(created.result),
update: describe(updated.result),
omittedAmount: describe(omitted.result),
noCollectionAndZeroPending: describe(absent.result),
})
}
The audited source predicts array, object, object, and undefined, respectively. A regression should use the real Payment Module, rather than a fake returning the workflow's declared array shape. It should also cover an existing not_paid / awaiting collection at zero pending and selector updates matching multiple collections.
Related amount documentation mismatch
This is secondary to the result-shape report. The input documentation and pending-amount step disagree: the step rejects a positive requested amount above pending, then returns the entire pending amount. With pending 100, passing 20 still sets the collection to 100; omitting the amount also uses 100, rather than the documented zero. The caller's amount is therefore neither the target collection amount nor a cap on it. This report does not request an unreviewed change to charging behavior, but the documented contract should make the current behavior explicit.
Link to reproduction repo
Self-contained diagnostic above; no separate reproduction repository. All implementation evidence links to public Medusa source. Searches for the workflow name and payment-collection array issues did not find an existing matching report.
Package.json file
Minimal dependency example for a disposable reproduction application, using public upstream packages:
{ "dependencies": { "@medusajs/framework": "2.20.1", "@medusajs/medusa": "2.20.1" } }I inspected the publicly published
@medusajs/core-flowsand@medusajs/paymentpackages at 2.18.0 and 2.20.1, the latest stable release on 2026-09-07. Their npm SHA-512 integrity values were verified before inspection. The workflow's JavaScript, its declaration, and the update step are identical between these versions; the Payment Module's result-selection expression is also unchanged. This report is a published-package/source audit, not a database-backed execution. It contains only public upstream implementation details and a synthetic reproduction example.Node.js version
Not applicable to the source audit; no Medusa application was executed.
Database and its version
Not applicable; no database was used for the source audit.
Operating system name and version
Not applicable; the reported mismatch is in the publicly distributed JavaScript and declarations.
Browser name
Not applicable; server-side workflow.
What happened?
createOrUpdateOrderPaymentCollectionWorkflowdeclaresPaymentCollectionDTO[]as its result, but its update branch returns one DTO and its no-op path returnsundefined.This breaks a caller that uses the declared array contract, for example
result[0].id, when retrying an order with an existingnot_paidcollection. Fakes that always return arrays conceal the mismatch.Expected behavior
The runtime result and published type should agree in every branch. One option is to preserve the array contract: return an array from the update path and
[]from the no-op path. Another is to declare and document the actual union throughout the workflow and step APIs. Normalizing only the update branch leaves the no-op mismatch unresolved.The Payment Module's selector overload independently declares an array return and should preserve the records it updates, while the ID overload should retain its single-record contract.
Actual behavior
Assuming the input amount passes validation:
PaymentCollectionDTO[]not_paid/awaitingPaymentCollectionDTOundefinedauthorized/partially_authorizedPaymentCollectionDTO[]authorized/partially_authorizedundefinedundefinedZero pending does not always mean no-op: the update predicate uses
gte, whereas creation usesgt.Source at the immutable 2.20.1 commit
0ed927bdb7a396a8fd5f6447ed69b7b354b150d3:updatedPaymentCollections || createdPaymentCollectionwithout normalization.updatePaymentCollections(selector, update)and forwards its return value.Array.isArray(data) ? result : result[0]. Heredatais the update payload, an object, regardless of whether the first argument is a selector. The selector overload therefore returns only the first record instead of the declared array.The published
@medusajs/core-flows@2.20.1filedist/order/workflows/create-or-update-order-payment-collection.d.tsstill declaresReturnWorkflow<{ order_id: string; amount?: number }, PaymentCollectionDTO[], []>.Reproduction
Use a disposable Medusa application/database with the standard Order, Payment, and link modules, first on 2.18.0 and then with all Medusa packages aligned to 2.20.1. The following diagnostic is supplied for a database-backed reproduction; it was not executed as part of the newer-version source audit. Save it as
src/scripts/reproduce-payment-collection-shape.tsand runpnpm exec medusa exec ./src/scripts/reproduce-payment-collection-shape.ts.The script creates two synthetic orders and one payment collection. It does not create payment sessions or invoke a payment provider. Dispose of the test database afterward.
The audited source predicts
array,object,object, andundefined, respectively. A regression should use the real Payment Module, rather than a fake returning the workflow's declared array shape. It should also cover an existingnot_paid/awaitingcollection at zero pending and selector updates matching multiple collections.Related amount documentation mismatch
This is secondary to the result-shape report. The input documentation and pending-amount step disagree: the step rejects a positive requested amount above pending, then returns the entire pending amount. With pending
100, passing20still sets the collection to100; omitting the amount also uses100, rather than the documented zero. The caller's amount is therefore neither the target collection amount nor a cap on it. This report does not request an unreviewed change to charging behavior, but the documented contract should make the current behavior explicit.Link to reproduction repo
Self-contained diagnostic above; no separate reproduction repository. All implementation evidence links to public Medusa source. Searches for the workflow name and payment-collection array issues did not find an existing matching report.