Skip to content

[Bug]: Order payment-collection workflow returns object or undefined despite array result type #16751

Description

@davidefalcone21

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:

  1. The workflow branches and final transform assert array types, then forward updatedPaymentCollections || createdPaymentCollection without normalization.
  2. The update step calls updatePaymentCollections(selector, update) and forwards its return value.
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions