Skip to content

fix(link-modules,core-flows): Carry over cart promotions to order promotions #12920

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/odd-apricots-marry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@medusajs/link-modules": patch
"@medusajs/core-flows": patch
---

fix(link-modules,core-flows): Carry over cart promotions to order promotions
54 changes: 54 additions & 0 deletions integration-tests/http/__tests__/cart/store/cart.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1252,6 +1252,60 @@ medusaIntegrationTestRunner({
)
})

it("should successfully complete cart with promotions", async () => {
const oldCart = (
await api.get(`/store/carts/${cart.id}`, storeHeaders)
).data.cart

createCartCreditLinesWorkflow.run({
input: [
{
cart_id: oldCart.id,
amount: oldCart.total,
currency_code: "usd",
reference: "test",
reference_id: "test",
},
],
container: appContainer,
})

const cartResponse = await api.post(
`/store/carts/${cart.id}/complete`,
{},
storeHeaders
)

const orderResponse = await api.get(
`/store/orders/${cartResponse.data.order.id}?fields=+promotions.*`,
storeHeaders
)

expect(cartResponse.status).toEqual(200)
expect(orderResponse.data.order).toEqual(
expect.objectContaining({
promotions: [
expect.objectContaining({
code: promotion.code,
}),
],
items: expect.arrayContaining([
expect.objectContaining({
unit_price: 1500,
compare_at_unit_price: null,
quantity: 1,
adjustments: expect.arrayContaining([
expect.objectContaining({
amount: 100,
code: promotion.code,
}),
]),
}),
]),
})
)
})

it("should successfully complete cart without shipping for digital products", async () => {
/**
* Product has a shipping profile so cart item should not require shipping
Expand Down
2 changes: 2 additions & 0 deletions packages/core/core-flows/src/cart/utils/fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const cartFieldsForRefreshSteps = [
"shipping_methods.tax_lines.*",
"customer.*",
"customer.groups.*",
"promotions.id",
"promotions.code",
"payment_collection.id",
"payment_collection.raw_amount",
Expand Down Expand Up @@ -105,6 +106,7 @@ export const completeCartFields = [
"credit_lines.*",
"payment_collection.*",
"payment_collection.payment_sessions.*",
"promotions.id",
"items.variant.id",
"items.variant.product.id",
"items.variant.product.is_giftcard",
Expand Down
13 changes: 12 additions & 1 deletion packages/core/core-flows/src/cart/workflows/complete-cart.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {
CartCreditLineDTO,
CartWorkflowDTO,
LinkDefinition,
PromotionDTO,
UsageComputedActions,
} from "@medusajs/framework/types"
import {
Expand Down Expand Up @@ -326,13 +328,22 @@ export const completeCartWorkflow = createWorkflow(
const linksToCreate = transform(
{ cart, createdOrder },
({ cart, createdOrder }) => {
const links: Record<string, any>[] = [
const links: LinkDefinition[] = [
{
[Modules.ORDER]: { order_id: createdOrder.id },
[Modules.CART]: { cart_id: cart.id },
},
]

if (cart.promotions?.length) {
cart.promotions.forEach((promotion: PromotionDTO) => {
links.push({
[Modules.ORDER]: { order_id: createdOrder.id },
[Modules.PROMOTION]: { promotion_id: promotion.id },
})
})
}

if (isDefined(cart.payment_collection?.id)) {
links.push({
[Modules.ORDER]: { order_id: createdOrder.id },
Expand Down
6 changes: 2 additions & 4 deletions packages/core/core-flows/src/draft-order/utils/fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,8 @@ export const draftOrderFieldsForRefreshSteps = [
"shipping_methods.tax_lines.*",
"customer.*",
"customer.groups.*",
"promotion_link.*",
"promotion_link.promotion",
"promotion_link.promotion.id",
"promotion_link.promotion.code",
"promotions.id",
"promotions.code",
"subtotal",
"item_total",
"total",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,38 +27,39 @@ export const addDraftOrderItemsWorkflowId = "add-draft-order-items"
/**
* This workflow adds items to a draft order. It's used by the
* [Add Item to Draft Order Admin API Route](https://docs.medusajs.com/api/admin#draft-orders_postdraftordersidedititems).
*
*
* You can use this workflow within your customizations or your own custom workflows, allowing you to wrap custom logic around adding items to
* a draft order.
*
*
* @example
* const { result } = await addDraftOrderItemsWorkflow(container)
* .run({
* input: {
* order_id: "order_123",
* items: [{
* variant_id: "variant_123",
* quantity: 1
* items: [{
* variant_id: "variant_123",
* quantity: 1
* }]
* }
* })
*
*
* @summary
*
*
* Add items to a draft order.
*/
export const addDraftOrderItemsWorkflow = createWorkflow(
addDraftOrderItemsWorkflowId,
function (
input: WorkflowData<OrderWorkflow.OrderEditAddNewItemWorkflowInput>
) {
const order: OrderDTO = useRemoteQueryStep({
entry_point: "orders",
fields: draftOrderFieldsForRefreshSteps,
variables: { id: input.order_id },
list: false,
throw_if_key_not_found: true,
}).config({ name: "order-query" })
const order: OrderDTO & { promotions: { code: string }[] } =
useRemoteQueryStep({
entry_point: "orders",
fields: draftOrderFieldsForRefreshSteps,
variables: { id: input.order_id },
list: false,
throw_if_key_not_found: true,
}).config({ name: "order-query" })

const orderChange: OrderChangeDTO = useRemoteQueryStep({
entry_point: "order_change",
Expand Down Expand Up @@ -92,19 +93,10 @@ export const addDraftOrderItemsWorkflow = createWorkflow(
},
})

const appliedPromoCodes: string[] = transform(order, (order) => {
const promotionLink = (order as any).promotion_link

if (!promotionLink) {
return []
}

if (Array.isArray(promotionLink)) {
return promotionLink.map((promo) => promo.promotion.code)
}

return [promotionLink.promotion.code]
})
const appliedPromoCodes: string[] = transform(
order,
(order) => order.promotions?.map((promotion) => promotion.code) ?? []
)

// If any the order has any promo codes, then we need to refresh the adjustments.
when(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ export interface AddDraftOrderShippingMethodsWorkflowInput {
/**
* This workflow adds shipping methods to a draft order. It's used by the
* [Add Shipping Method to Draft Order Admin API Route](https://docs.medusajs.com/api/admin#draft-orders_postdraftordersideditshippingmethods).
*
*
* You can use this workflow within your customizations or your own custom workflows, allowing you to wrap custom logic around adding shipping methods to
* a draft order.
*
*
* @example
* const { result } = await addDraftOrderShippingMethodsWorkflow(container)
* .run({
Expand All @@ -61,15 +61,19 @@ export interface AddDraftOrderShippingMethodsWorkflowInput {
* custom_amount: 10
* }
* })
*
*
* @summary
*
*
* Add shipping methods to a draft order.
*/
export const addDraftOrderShippingMethodsWorkflow = createWorkflow(
addDraftOrderShippingMethodsWorkflowId,
function (input: WorkflowData<AddDraftOrderShippingMethodsWorkflowInput>) {
const order: OrderDTO = useRemoteQueryStep({
const order: OrderDTO & {
promotions: {
code: string
}[]
} = useRemoteQueryStep({
entry_point: "orders",
fields: draftOrderFieldsForRefreshSteps,
variables: { id: input.order_id },
Expand Down Expand Up @@ -133,19 +137,10 @@ export const addDraftOrderShippingMethodsWorkflow = createWorkflow(
},
})

const appliedPromoCodes = transform(order, (order) => {
const promotionLink = (order as any).promotion_link

if (!promotionLink) {
return []
}

if (Array.isArray(promotionLink)) {
return promotionLink.map((promo) => promo.promotion.code)
}

return [promotionLink.promotion.code]
})
const appliedPromoCodes: string[] = transform(
order,
(order) => order.promotions?.map((promotion) => promotion.code) ?? []
)

// If any the order has any promo codes, then we need to refresh the adjustments.
when(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,26 +33,30 @@ export interface CancelDraftOrderEditWorkflowInput {
/**
* This workflow cancels a draft order edit. It's used by the
* [Cancel Draft Order Edit Admin API Route](https://docs.medusajs.com/api/admin#draft-orders_deletedraftordersidedit).
*
*
* You can use this workflow within your customizations or your own custom workflows, allowing you to wrap custom logic around
* cancelling a draft order edit.
*
*
* @example
* const { result } = await cancelDraftOrderEditWorkflow(container)
* .run({
* input: {
* order_id: "order_123",
* }
* })
*
*
* @summary
*
*
* Cancel a draft order edit.
*/
export const cancelDraftOrderEditWorkflow = createWorkflow(
cancelDraftOrderEditWorkflowId,
function (input: WorkflowData<CancelDraftOrderEditWorkflowInput>) {
const order: OrderDTO = useRemoteQueryStep({
const order: OrderDTO & {
promotions: {
code: string
}[]
} = useRemoteQueryStep({
entry_point: "orders",
fields: ["version", ...draftOrderFieldsForRefreshSteps],
variables: { id: input.order_id },
Expand Down Expand Up @@ -125,18 +129,12 @@ export const cancelDraftOrderEditWorkflow = createWorkflow(
const promotionsToRefresh = transform(
{ order, promotionsToRemove, promotionsToRestore },
({ order, promotionsToRemove, promotionsToRestore }) => {
const promotionLink = (order as any).promotion_link
const orderPromotions = order.promotions
const codes: Set<string> = new Set()

if (promotionLink) {
if (Array.isArray(promotionLink)) {
promotionLink.forEach((promo) => {
codes.add(promo.promotion.code)
})
} else {
codes.add(promotionLink.promotion.code)
}
}
orderPromotions?.forEach((promo) => {
codes.add(promo.code)
})

for (const code of promotionsToRemove) {
codes.delete(code)
Expand All @@ -163,9 +161,7 @@ export const cancelDraftOrderEditWorkflow = createWorkflow(
},
})

when(shippingToRestore, (methods) => {
return !!methods?.length
}).then(() => {
when(shippingToRestore, (methods) => !!methods?.length).then(() => {
restoreDraftOrderShippingMethodsStep({
shippingMethods: shippingToRestore as any,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ export const removeDraftOrderActionItemWorkflowId =
/**
* This workflow removes an item that was added or updated in a draft order edit. It's used by the
* [Remove Item from Draft Order Edit Admin API Route](https://docs.medusajs.com/api/admin#draft-orders_deletedraftordersidedititemsaction_id).
*
*
* You can use this workflow within your customizations or your own custom workflows, allowing you to wrap custom logic around
* removing an item from a draft order edit.
*
*
* @example
* const { result } = await removeDraftOrderActionItemWorkflow(container)
* .run({
Expand All @@ -40,17 +40,21 @@ export const removeDraftOrderActionItemWorkflowId =
* action_id: "action_123",
* }
* })
*
*
* @summary
*
*
* Remove an item from a draft order edit.
*/
export const removeDraftOrderActionItemWorkflow = createWorkflow(
removeDraftOrderActionItemWorkflowId,
function (
input: WorkflowData<OrderWorkflow.DeleteOrderEditItemActionWorkflowInput>
): WorkflowResponse<OrderPreviewDTO> {
const order: OrderDTO = useRemoteQueryStep({
const order: OrderDTO & {
promotions: {
code: string
}[]
} = useRemoteQueryStep({
entry_point: "orders",
fields: ["id", "status", "is_draft_order", "canceled_at", "items.*"],
variables: { id: input.order_id },
Expand Down Expand Up @@ -89,19 +93,8 @@ export const removeDraftOrderActionItemWorkflow = createWorkflow(

const appliedPromoCodes: string[] = transform(
refetchedOrder,
(refetchedOrder) => {
const promotionLink = (refetchedOrder as any).promotion_link

if (!promotionLink) {
return []
}

if (Array.isArray(promotionLink)) {
return promotionLink.map((promo) => promo.promotion.code)
}

return [promotionLink.promotion.code]
}
(refetchedOrder) =>
refetchedOrder.promotions?.map((promotion) => promotion.code) ?? []
)

// If any the order has any promo codes, then we need to refresh the adjustments.
Expand Down
Loading