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

what:

- Carry over cart promotions to order promotions
This commit is contained in:
Riqwan Thamir
2025-07-11 10:05:20 +02:00
committed by GitHub
parent 541e791b9b
commit 8c4228fc42
16 changed files with 200 additions and 203 deletions

View File

@@ -0,0 +1,6 @@
---
"@medusajs/link-modules": patch
"@medusajs/core-flows": patch
---
fix(link-modules,core-flows): Carry over cart promotions to order promotions

View File

@@ -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

View File

@@ -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",
@@ -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",

View File

@@ -1,6 +1,8 @@
import {
CartCreditLineDTO,
CartWorkflowDTO,
LinkDefinition,
PromotionDTO,
UsageComputedActions,
} from "@medusajs/framework/types"
import {
@@ -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 },

View File

@@ -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",

View File

@@ -27,24 +27,24 @@ 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(
@@ -52,13 +52,14 @@ export const addDraftOrderItemsWorkflow = createWorkflow(
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",
@@ -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(

View File

@@ -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({
@@ -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 },
@@ -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(

View File

@@ -33,10 +33,10 @@ 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({
@@ -44,15 +44,19 @@ export interface CancelDraftOrderEditWorkflowInput {
* 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 },
@@ -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)
@@ -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,
})

View File

@@ -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({
@@ -40,9 +40,9 @@ export const removeDraftOrderActionItemWorkflowId =
* action_id: "action_123",
* }
* })
*
*
* @summary
*
*
* Remove an item from a draft order edit.
*/
export const removeDraftOrderActionItemWorkflow = createWorkflow(
@@ -50,7 +50,11 @@ export const removeDraftOrderActionItemWorkflow = createWorkflow(
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 },
@@ -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.

View File

@@ -32,10 +32,10 @@ export const removeDraftOrderActionShippingMethodWorkflowId =
/**
* This workflow removes a shipping method that was added to an edited draft order. It's used by the
* [Remove Shipping Method from Draft Order Edit Admin API Route](https://docs.medusajs.com/api/admin#draft-orders_deletedraftordersideditshippingmethodsaction_id).
*
*
* You can use this workflow within your customizations or your own custom workflows, allowing you to wrap custom logic around
* removing a shipping method from an edited draft order.
*
*
* @example
* const { result } = await removeDraftOrderActionShippingMethodWorkflow(container)
* .run({
@@ -44,9 +44,9 @@ export const removeDraftOrderActionShippingMethodWorkflowId =
* action_id: "action_123",
* }
* })
*
*
* @summary
*
*
* Remove a shipping method from an edited draft order.
*/
export const removeDraftOrderActionShippingMethodWorkflow = createWorkflow(
@@ -100,19 +100,11 @@ export const removeDraftOrderActionShippingMethodWorkflow = createWorkflow(
order,
})
const appliedPromoCodes = transform(context, (context) => {
const promotionLink = (context 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(
context,
(context) =>
(context as any).promotions?.map((promotion) => promotion.code) ?? []
)
when(
appliedPromoCodes,

View File

@@ -41,10 +41,10 @@ export interface RemoveDraftOrderShippingMethodWorkflowInput {
/**
* This workflow removes an existing shipping method from a draft order edit. It's used by the
* [Remove Shipping Method from Draft Order Edit Admin API Route](https://docs.medusajs.com/api/admin#draft-orders_deletedraftordersideditshippingmethodsmethodmethod_id).
*
*
* You can use this workflow within your customizations or your own custom workflows, allowing you to wrap custom logic around
* removing a shipping method from a draft order edit.
*
*
* @example
* const { result } = await removeDraftOrderShippingMethodWorkflow(container)
* .run({
@@ -53,15 +53,19 @@ export interface RemoveDraftOrderShippingMethodWorkflowInput {
* shipping_method_id: "sm_123",
* }
* })
*
*
* @summary
*
*
* Remove an existing shipping method from a draft order edit.
*/
export const removeDraftOrderShippingMethodWorkflow = createWorkflow(
removeDraftOrderShippingMethodWorkflowId,
function (input: WorkflowData<RemoveDraftOrderShippingMethodWorkflowInput>) {
const order: OrderDTO = useRemoteQueryStep({
const order: OrderDTO & {
promotions: {
code: string
}[]
} = useRemoteQueryStep({
entry_point: "orders",
fields: draftOrderFieldsForRefreshSteps,
variables: { id: input.order_id },
@@ -89,19 +93,10 @@ export const removeDraftOrderShippingMethodWorkflow = 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(

View File

@@ -28,10 +28,10 @@ export const updateDraftOrderActionItemId = "update-draft-order-action-item"
/**
* This workflow updates a new item that was added to a draft order edit. It's used by the
* [Update New Item in Draft Order Edit Admin API Route](https://docs.medusajs.com/api/admin#draft-orders_postdraftordersidedititemsaction_id).
*
*
* You can use this workflow within your customizations or your own custom workflows, allowing you to wrap custom logic around
* updating a new item in a draft order edit.
*
*
* @example
* const { result } = await updateDraftOrderActionItemWorkflow(container)
* .run({
@@ -43,9 +43,9 @@ export const updateDraftOrderActionItemId = "update-draft-order-action-item"
* }
* }
* })
*
*
* @summary
*
*
* Update a new item in a draft order edit.
*/
export const updateDraftOrderActionItemWorkflow = createWorkflow(
@@ -53,7 +53,11 @@ export const updateDraftOrderActionItemWorkflow = createWorkflow(
function (
input: WorkflowData<OrderWorkflow.UpdateOrderEditAddNewItemWorkflowInput>
) {
const order: OrderDTO = useRemoteQueryStep({
const order: OrderDTO & {
promotions: {
code: string
}[]
} = useRemoteQueryStep({
entry_point: "orders",
fields: draftOrderFieldsForRefreshSteps,
variables: { id: input.order_id },
@@ -112,19 +116,11 @@ export const updateDraftOrderActionItemWorkflow = createWorkflow(
order,
})
const appliedPromoCodes: string[] = transform(context, (context) => {
const promotionLink = (context 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(
context,
(context) =>
(context as any).promotions?.map((promotion) => promotion.code) ?? []
)
// If any the order has any promo codes, then we need to refresh the adjustments.
when(

View File

@@ -33,10 +33,10 @@ export const updateDraftOrderActionShippingMethodWorkflowId =
/**
* This workflow updates a new shipping method that was added to a draft order edit. It's used by the
* [Update New Shipping Method in Draft Order Edit Admin API Route](https://docs.medusajs.com/api/admin#draft-orders_postdraftordersideditshippingmethodsaction_id).
*
*
* You can use this workflow within your customizations or your own custom workflows, allowing you to wrap custom logic around
* updating a new shipping method in a draft order edit.
*
*
* @example
* const { result } = await updateDraftOrderActionShippingMethodWorkflow(container)
* .run({
@@ -48,9 +48,9 @@ export const updateDraftOrderActionShippingMethodWorkflowId =
* }
* }
* })
*
*
* @summary
*
*
* Update a new shipping method in a draft order edit.
*/
export const updateDraftOrderActionShippingMethodWorkflow = createWorkflow(
@@ -142,19 +142,11 @@ export const updateDraftOrderActionShippingMethodWorkflow = createWorkflow(
order,
})
const appliedPromoCodes = transform(context, (context) => {
const promotionLink = (context 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(
context,
(context) =>
(context as any).promotions?.map((promotion) => promotion.code) ?? []
)
when(
appliedPromoCodes,

View File

@@ -33,10 +33,10 @@ export const updateDraftOrderItemWorkflowId = "update-draft-order-item"
/**
* This workflow updates an item in a draft order edit. It's used by the
* [Update Item in Draft Order Edit Admin API Route](https://docs.medusajs.com/api/admin#draft-orders_postdraftordersidedititemsitemitem_id).
*
*
* You can use this workflow within your customizations or your own custom workflows, allowing you to wrap custom logic around
* updating an item in a draft order edit.
*
*
* @example
* const { result } = await updateDraftOrderItemWorkflow(container)
* .run({
@@ -45,9 +45,9 @@ export const updateDraftOrderItemWorkflowId = "update-draft-order-item"
* items: [{ id: "orli_123", quantity: 2 }],
* }
* })
*
*
* @summary
*
*
* Update an item in a draft order edit.
*/
export const updateDraftOrderItemWorkflow = createWorkflow(
@@ -115,19 +115,11 @@ export const updateDraftOrderItemWorkflow = createWorkflow(
order,
})
const appliedPromoCodes: string[] = transform(context, (context) => {
const promotionLink = (context 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(
context,
(context) =>
(context as any).promotions?.map((promotion) => promotion.code) ?? []
)
// If any the order has any promo codes, then we need to refresh the adjustments.
when(

View File

@@ -56,10 +56,10 @@ export interface UpdateDraftOrderShippingMethodWorkflowInput {
/**
* This workflow updates an existing shipping method in a draft order edit. It's used by the
* [Update Shipping Method in Draft Order Edit Admin API Route](https://docs.medusajs.com/api/admin#draft-orders_postdraftordersideditshippingmethodsmethodmethod_id).
*
*
* You can use this workflow within your customizations or your own custom workflows, allowing you to wrap custom logic around
* updating an existing shipping method in a draft order edit.
*
*
* @example
* const { result } = await updateDraftOrderShippingMethodWorkflow(container)
* .run({
@@ -71,9 +71,9 @@ export interface UpdateDraftOrderShippingMethodWorkflowInput {
* }
* }
* })
*
*
* @summary
*
*
* Update an existing shipping method in a draft order edit.
*/
export const updateDraftOrderShippingMethodWorkflow = createWorkflow(
@@ -123,19 +123,11 @@ export const updateDraftOrderShippingMethodWorkflow = createWorkflow(
throw_if_key_not_found: true,
}).config({ name: "refetched-order-query" })
const appliedPromoCodes = 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]
})
const appliedPromoCodes = transform(
refetchedOrder,
(refetchedOrder) =>
refetchedOrder.promotions?.map((promotion) => promotion.code) ?? []
)
when(
appliedPromoCodes,

View File

@@ -32,7 +32,7 @@ export const OrderPromotion: ModuleJoinerConfig = {
entity: "Promotion",
primaryKey: "id",
foreignKey: "promotion_id",
alias: "promotion",
alias: "promotions",
args: {
methodSuffix: "Promotions",
},
@@ -44,8 +44,8 @@ export const OrderPromotion: ModuleJoinerConfig = {
serviceName: Modules.ORDER,
entity: "Order",
fieldAlias: {
promotion: {
path: "promotion_link.promotion",
promotions: {
path: "promotion_link.promotions",
isList: true,
},
},
@@ -54,16 +54,7 @@ export const OrderPromotion: ModuleJoinerConfig = {
primaryKey: "order_id",
foreignKey: "id",
alias: "promotion_link",
},
},
{
serviceName: Modules.PROMOTION,
entity: "Promotion",
relationship: {
serviceName: LINKS.OrderPromotion,
primaryKey: "promotion_id",
foreignKey: "id",
alias: "order_link",
isList: true,
},
},
],