#16807·medusa

Bug: deleteOrders deletes all order_address rows when address IDs are null (wipes completed orders via CASCADE)

Author: princeparmarCreated Sep 12, 2026Updated Sep 17, 2026
Labelstype: bugrequires-team

Bug report

Describe the bug

Calling DELETE /admin/draft-orders/:id (via deleteDraftOrdersWorkflowOrderModuleService.deleteOrders) can delete every row in order_address, not just addresses belonging to the draft being deleted.

When shipping_address_id / billing_address_id are null (common for POS/draft carts), deleteOrders builds an address ID list that includes null values and passes that array into orderAddressService_.delete(...).

Observed Postgres statement (with log_statement = mod):

sql
delete from "order_address" returning "id"

(no usable WHERE id IN (...) with real IDs)

Why this is catastrophic

Medusa's order schema defines:

sql
FOREIGN KEY (shipping_address_id) REFERENCES order_address(id) ON DELETE CASCADE
FOREIGN KEY (billing_address_id) REFERENCES order_address(id) ON DELETE CASCADE

So deleting all order_address rows cascade-deletes all orders that reference those addresses, including completed/paid orders.

In production we correlated:

  1. DELETE /admin/draft-orders/<draft_id> from POS/Admin
  2. Immediately followed by delete from "order_address" returning "id"
  3. Then all completed orders disappeared (0 soft-deletes; hard-deleted via CASCADE)
  4. order_payment_collection / payment rows remained as orphans

Affected version

  • @medusajs/medusa / @medusajs/order 2.12.3
  • Likely present on other 2.x versions with the same deleteOrders implementation

Steps to reproduce

  1. Create several completed orders that have shipping addresses.
  2. Create a draft order with shipping_address_id / billing_address_id = null (typical POS hold cart).
  3. Call DELETE /admin/draft-orders/:id for that draft.
  4. Observe:
    • order_address table is emptied (or mass-deleted)
    • completed orders referencing those addresses are gone (CASCADE)
    • only drafts without address FKs may remain

Expected behavior

Deleting one draft order should delete only that draft and its related rows (addresses for that order only, if any).

orderAddressService_.delete must never be called with null/empty IDs in a way that deletes the whole table.

Actual behavior

deleteOrders does:

javascript
const orderAddressIds = orders
  .map((order) => [order.shipping_address_id, order.billing_address_id])
  .flat(1)

await this.orderAddressService_.delete(orderAddressIds, sharedContext)

When address IDs are null, this becomes effectively a wipe of order_address, which then CASCADE-deletes completed orders.

Same pattern is risky for empty/null entries in lineItemIds / orderShippingMethodIds / orderChangeIds.

Suggested fix

Filter falsy IDs and skip delete when empty:

javascript
const orderAddressIds = orders
  .map((order) => [order.shipping_address_id, order.billing_address_id])
  .flat(1)
  .filter((id) => !!id)

if (orderAddressIds.length) {
  await this.orderAddressService_.delete(orderAddressIds, sharedContext)
}

Also consider changing address FKs from ON DELETE CASCADE to ON DELETE SET NULL (address is owned by the order; deleting an address should not delete the order).

Additional context

  • Triggered from Medusa Admin / POS Electron clients hitting core DELETE /admin/draft-orders/:id (not a custom route).
  • Workflow path: deleteDraftOrdersWorkflowdeleteDraftOrdersStepservice.deleteOrders(orderIds).
  • This caused repeated production data loss of completed POS sales until mitigated at the DB/app layer.

Environment

  • Medusa: 2.12.3
  • Database: PostgreSQL 16
  • Node: 22.x