diff --git a/.changeset/public-brooms-give.md b/.changeset/public-brooms-give.md
new file mode 100644
index 0000000000..ac4729372d
--- /dev/null
+++ b/.changeset/public-brooms-give.md
@@ -0,0 +1,10 @@
+---
+"@medusajs/dashboard": patch
+"@medusajs/core-flows": patch
+"@medusajs/order": patch
+"@medusajs/js-sdk": patch
+"@medusajs/types": patch
+"@medusajs/medusa": patch
+---
+
+feat: carry over promotions toggle on exchanges
diff --git a/integration-tests/http/__tests__/exchanges/exchanges.spec.ts b/integration-tests/http/__tests__/exchanges/exchanges.spec.ts
index 0b1d0f14eb..53978b4e25 100644
--- a/integration-tests/http/__tests__/exchanges/exchanges.spec.ts
+++ b/integration-tests/http/__tests__/exchanges/exchanges.spec.ts
@@ -1089,6 +1089,19 @@ medusaIntegrationTestRunner({
expect(result.original_total).toEqual(11) // $10 + 10% tax
expect(result.total).toEqual(10 * 0.9 * 1.1) // ($10 - 10% discount) + 10% tax
+ const orderChange = (
+ await api.get(`/admin/orders/${orderId}/preview`, adminHeaders)
+ ).data.order.order_change
+
+ // opt in for carry over promotions
+ await api.post(
+ `/admin/order-changes/${orderChange.id}`,
+ {
+ carry_over_promotions: true,
+ },
+ adminHeaders
+ )
+
// Add outbound item with price $12, 10% discount and 10% tax
result = (
await api
@@ -1214,6 +1227,363 @@ medusaIntegrationTestRunner({
expect(orderResult2.total).toEqual(11.016)
expect(orderResult2.original_total).toEqual(12.24)
})
+
+ it("should enable carry_over_promotions flag and apply promotions to outbound items (flag disabled before request)", async () => {
+ // fulfill item so it can be returned
+ await api.post(
+ `/admin/orders/${orderWithPromotion.id}/fulfillments`,
+ {
+ items: [
+ {
+ id: orderWithPromotion.items[0].id,
+ quantity: 1,
+ },
+ ],
+ },
+ adminHeaders
+ )
+
+ let result = await api.post(
+ "/admin/exchanges",
+ {
+ order_id: orderWithPromotion.id,
+ description: "Test",
+ },
+ adminHeaders
+ )
+
+ const exchangeId = result.data.exchange.id
+
+ // Query order change for the exchange
+ const orderChange = (
+ await api.get(
+ `/admin/orders/${orderWithPromotion.id}/preview`,
+ adminHeaders
+ )
+ ).data.order.order_change
+
+ const orderChangeId = orderChange.id
+
+ // return original item
+ await api.post(
+ `/admin/exchanges/${exchangeId}/inbound/items`,
+ {
+ items: [
+ {
+ id: orderWithPromotion.items[0].id,
+ quantity: 1,
+ },
+ ],
+ },
+ adminHeaders
+ )
+
+ // add outbound item
+ await api.post(
+ `/admin/exchanges/${exchangeId}/outbound/items`,
+ {
+ items: [
+ {
+ variant_id: productForAdjustmentTest.variants[0].id,
+ quantity: 1,
+ },
+ ],
+ },
+ adminHeaders
+ )
+
+ // Initially, promotions should be disabled by default when adding outbound items
+ let orderPreview = (
+ await api.get(
+ `/admin/orders/${orderWithPromotion.id}/preview`,
+ adminHeaders
+ )
+ ).data.order
+
+ expect(orderPreview.items).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ id: "item-1", // original item
+ adjustments: [
+ expect.objectContaining({
+ amount: 1,
+ }),
+ ],
+ }),
+ expect.objectContaining({
+ variant_id: productForAdjustmentTest.variants[0].id,
+ adjustments: [], // outbound item has no adjustments initially
+ }),
+ ])
+ )
+
+ // Enable carry_over_promotions
+ await api.post(
+ `/admin/order-changes/${orderChangeId}`,
+ {
+ carry_over_promotions: true,
+ },
+ adminHeaders
+ )
+
+ // Verify adjustments are added
+ orderPreview = (
+ await api.get(
+ `/admin/orders/${orderWithPromotion.id}/preview`,
+ adminHeaders
+ )
+ ).data.order
+
+ expect(orderPreview.items).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ id: "item-1", // original item
+ adjustments: [
+ expect.objectContaining({
+ amount: 1,
+ }),
+ ],
+ }),
+ expect.objectContaining({
+ variant_id: productForAdjustmentTest.variants[0].id,
+ adjustments: [
+ // outbound item has adjustments after carry_over_promotions is enabled
+ expect.objectContaining({
+ amount: 1.2,
+ }),
+ ],
+ }),
+ ])
+ )
+
+ // Disable carry_over_promotions
+ await api.post(
+ `/admin/order-changes/${orderChangeId}`,
+ {
+ carry_over_promotions: false,
+ },
+ adminHeaders
+ )
+
+ // Verify adjustments are removed again
+ orderPreview = (
+ await api.get(
+ `/admin/orders/${orderWithPromotion.id}/preview`,
+ adminHeaders
+ )
+ ).data.order
+
+ expect(orderPreview.items).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ id: "item-1", // original item
+ adjustments: [
+ expect.objectContaining({
+ amount: 1,
+ }),
+ ],
+ }),
+ expect.objectContaining({
+ variant_id: productForAdjustmentTest.variants[0].id,
+ adjustments: [], // outbound item has no adjustments
+ }),
+ ])
+ )
+
+ await api.post(
+ `/admin/exchanges/${exchangeId}/request`,
+ {},
+ adminHeaders
+ )
+
+ const finalOrder = (
+ await api.get(
+ `/admin/orders/${orderWithPromotion.id}`,
+ adminHeaders
+ )
+ ).data.order
+
+ // items adjustment state is equal to the last state of the order preview (flag disabled)
+ expect(finalOrder.items).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ id: "item-1", // original item
+ adjustments: [
+ expect.objectContaining({
+ amount: 1,
+ }),
+ ],
+ }),
+ expect.objectContaining({
+ variant_id: productForAdjustmentTest.variants[0].id,
+ adjustments: [],
+ }),
+ ])
+ )
+ })
+
+ it("should enable carry_over_promotions flag and apply promotions to outbound items (flag enabled before request)", async () => {
+ // fulfill item so it can be returned
+ await api.post(
+ `/admin/orders/${orderWithPromotion.id}/fulfillments`,
+ {
+ items: [
+ {
+ id: orderWithPromotion.items[0].id,
+ quantity: 1,
+ },
+ ],
+ },
+ adminHeaders
+ )
+
+ let result = await api.post(
+ "/admin/exchanges",
+ {
+ order_id: orderWithPromotion.id,
+ description: "Test",
+ },
+ adminHeaders
+ )
+
+ const exchangeId = result.data.exchange.id
+
+ // Query order change for the exchange
+ const orderChange = (
+ await api.get(
+ `/admin/orders/${orderWithPromotion.id}/preview`,
+ adminHeaders
+ )
+ ).data.order.order_change
+
+ const orderChangeId = orderChange.id
+
+ // return original item
+ await api.post(
+ `/admin/exchanges/${exchangeId}/inbound/items`,
+ {
+ items: [
+ {
+ id: orderWithPromotion.items[0].id,
+ quantity: 1,
+ },
+ ],
+ },
+ adminHeaders
+ )
+
+ // add outbound item
+ await api.post(
+ `/admin/exchanges/${exchangeId}/outbound/items`,
+ {
+ items: [
+ {
+ variant_id: productForAdjustmentTest.variants[0].id,
+ quantity: 1,
+ },
+ ],
+ },
+ adminHeaders
+ )
+
+ let orderPreview = (
+ await api.get(
+ `/admin/orders/${orderWithPromotion.id}/preview`,
+ adminHeaders
+ )
+ ).data.order
+
+ expect(orderPreview.items).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ id: "item-1", // original item
+ adjustments: [
+ expect.objectContaining({
+ amount: 1,
+ }),
+ ],
+ }),
+ expect.objectContaining({
+ variant_id: productForAdjustmentTest.variants[0].id,
+ adjustments: [], // outbound item has no adjustments initially
+ }),
+ ])
+ )
+
+ // Enable carry_over_promotions
+ await api.post(
+ `/admin/order-changes/${orderChangeId}`,
+ {
+ carry_over_promotions: true,
+ },
+ adminHeaders
+ )
+
+ // Verify adjustments are added
+ orderPreview = (
+ await api.get(
+ `/admin/orders/${orderWithPromotion.id}/preview`,
+ adminHeaders
+ )
+ ).data.order
+
+ expect(orderPreview.items).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ id: "item-1", // original item
+ adjustments: [
+ expect.objectContaining({
+ amount: 1,
+ }),
+ ],
+ }),
+ expect.objectContaining({
+ variant_id: productForAdjustmentTest.variants[0].id,
+ adjustments: [
+ // outbound item has adjustments after carry_over_promotions is enabled
+ expect.objectContaining({
+ amount: 1.2,
+ }),
+ ],
+ }),
+ ])
+ )
+
+ await api.post(
+ `/admin/exchanges/${exchangeId}/request`,
+ {},
+ adminHeaders
+ )
+
+ const finalOrder = (
+ await api.get(
+ `/admin/orders/${orderWithPromotion.id}`,
+ adminHeaders
+ )
+ ).data.order
+
+ // items adjustment state is equal to the last state of the order preview (flag enabled)
+ expect(finalOrder.items).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ id: "item-1", // original item
+ adjustments: [
+ expect.objectContaining({
+ amount: 1,
+ }),
+ ],
+ }),
+ expect.objectContaining({
+ variant_id: productForAdjustmentTest.variants[0].id,
+ adjustments: [
+ expect.objectContaining({
+ amount: 1.2,
+ }),
+ ],
+ }),
+ ])
+ )
+ })
})
})
},
diff --git a/integration-tests/http/__tests__/order-edits/order-edits.spec.ts b/integration-tests/http/__tests__/order-edits/order-edits.spec.ts
index c97e36dda6..a8d0e29590 100644
--- a/integration-tests/http/__tests__/order-edits/order-edits.spec.ts
+++ b/integration-tests/http/__tests__/order-edits/order-edits.spec.ts
@@ -1297,6 +1297,16 @@ medusaIntegrationTestRunner({
adminHeaders
)
+ // allow carry over promotions flag on the edit
+ const orderChangeId = result.data.order_change.id
+ await api.post(
+ `/admin/order-changes/${orderChangeId}`,
+ {
+ carry_over_promotions: true,
+ },
+ adminHeaders
+ )
+
const orderId = result.data.order_change.order_id
result = (await api.get(`/admin/orders/${orderId}`, adminHeaders)).data
@@ -1375,6 +1385,16 @@ medusaIntegrationTestRunner({
const orderId = result.data.order_change.order_id
+ // allow carry over promotions flag on the edit
+ const orderChangeId = result.data.order_change.id
+ await api.post(
+ `/admin/order-changes/${orderChangeId}`,
+ {
+ carry_over_promotions: true,
+ },
+ adminHeaders
+ )
+
const item = order.items[0]
result = (await api.get(`/admin/orders/${orderId}`, adminHeaders)).data
@@ -1451,6 +1471,16 @@ medusaIntegrationTestRunner({
const orderId = result.data.order_change.order_id
+ // allow carry over promotions flag on the edit
+ const orderChangeId = result.data.order_change.id
+ await api.post(
+ `/admin/order-changes/${orderChangeId}`,
+ {
+ carry_over_promotions: true,
+ },
+ adminHeaders
+ )
+
const item = order.items[0]
result = (await api.get(`/admin/orders/${orderId}`, adminHeaders)).data
@@ -1559,6 +1589,16 @@ medusaIntegrationTestRunner({
)
const orderChange1 = response.data.order_change
+ // allow carry over promotions flag on the edit
+ const orderChangeId = response.data.order_change.id
+ await api.post(
+ `/admin/order-changes/${orderChangeId}`,
+ {
+ carry_over_promotions: true,
+ },
+ adminHeaders
+ )
+
// 2. Add a new item in the first edit
response = await api.post(
`/admin/order-edits/${orderChange1.order_id}/items`,
@@ -1648,6 +1688,13 @@ medusaIntegrationTestRunner({
adminHeaders
)
const orderChange2 = response.data.order_change
+ await api.post(
+ `/admin/order-changes/${orderChange2.id}`,
+ {
+ carry_over_promotions: true,
+ },
+ adminHeaders
+ )
// 5. Add another productExtra item
response = await api.post(
@@ -1943,6 +1990,17 @@ medusaIntegrationTestRunner({
},
adminHeaders
)
+
+ // allow carry over promotions flag on the edit
+ const orderChangeId = editRes.data.order_change.id
+ await api.post(
+ `/admin/order-changes/${orderChangeId}`,
+ {
+ carry_over_promotions: true,
+ },
+ adminHeaders
+ )
+
const editOrderId = editRes.data.order_change.order_id
const extraVariantId = productExtra.variants[0].id
@@ -2038,6 +2096,16 @@ medusaIntegrationTestRunner({
const orderId = result.data.order_change.order_id
const originalItem = order.items[0]
+ // allow carry over promotions flag on the edit
+ const orderChangeId = result.data.order_change.id
+ await api.post(
+ `/admin/order-changes/${orderChangeId}`,
+ {
+ carry_over_promotions: true,
+ },
+ adminHeaders
+ )
+
// Add a new item
result = (
await api.post(
@@ -2154,6 +2222,16 @@ medusaIntegrationTestRunner({
adminHeaders
)
+ // allow carry over promotions flag on the edit
+ const orderChangeId = result.data.order_change.id
+ await api.post(
+ `/admin/order-changes/${orderChangeId}`,
+ {
+ carry_over_promotions: true,
+ },
+ adminHeaders
+ )
+
const orderId = result.data.order_change.order_id
result = (await api.get(`/admin/orders/${orderId}`, adminHeaders)).data
@@ -2332,6 +2410,16 @@ medusaIntegrationTestRunner({
adminHeaders
)
+ // allow carry over promotions flag on the edit
+ const orderChangeId = result.data.order_change.id
+ await api.post(
+ `/admin/order-changes/${orderChangeId}`,
+ {
+ carry_over_promotions: true,
+ },
+ adminHeaders
+ )
+
const orderId = result.data.order_change.order_id
result = (await api.get(`/admin/orders/${orderId}`, adminHeaders)).data
diff --git a/packages/admin/dashboard/src/hooks/api/orders.tsx b/packages/admin/dashboard/src/hooks/api/orders.tsx
index aaa0684943..d7bfffe7ac 100644
--- a/packages/admin/dashboard/src/hooks/api/orders.tsx
+++ b/packages/admin/dashboard/src/hooks/api/orders.tsx
@@ -10,8 +10,8 @@ import {
import { sdk } from "../../lib/client"
import { queryClient } from "../../lib/query-client"
import { queryKeysFactory, TQueryKey } from "../../lib/query-key-factory"
-import { inventoryItemsQueryKeys } from "./inventory"
import { reservationItemsQueryKeys } from "./reservations"
+import { inventoryItemsQueryKeys } from "./inventory"
const ORDERS_QUERY_KEY = "orders" as const
const _orderKeys = queryKeysFactory(ORDERS_QUERY_KEY) as TQueryKey<"orders"> & {
@@ -406,3 +406,35 @@ export const useCreateOrderCreditLine = (
...options,
})
}
+
+export const useUpdateOrderChange = (
+ orderChangeId: string,
+ options?: UseMutationOptions<
+ HttpTypes.AdminOrderChangeResponse,
+ FetchError,
+ { carry_over_promotions: boolean }
+ >
+) => {
+ return useMutation({
+ mutationFn: (payload: { carry_over_promotions: boolean }) =>
+ sdk.admin.order.updateOrderChange(orderChangeId, payload),
+ onSuccess: (data, variables, context) => {
+ const orderId = data.order_change.order_id
+
+ queryClient.invalidateQueries({
+ queryKey: ordersQueryKeys.details(),
+ })
+
+ queryClient.invalidateQueries({
+ queryKey: ordersQueryKeys.preview(orderId),
+ })
+
+ queryClient.invalidateQueries({
+ queryKey: ordersQueryKeys.changes(orderId),
+ })
+
+ options?.onSuccess?.(data, variables, context)
+ },
+ ...options,
+ })
+}
diff --git a/packages/admin/dashboard/src/i18n/translations/$schema.json b/packages/admin/dashboard/src/i18n/translations/$schema.json
index f413a59c52..9b6147c0f7 100644
--- a/packages/admin/dashboard/src/i18n/translations/$schema.json
+++ b/packages/admin/dashboard/src/i18n/translations/$schema.json
@@ -2008,6 +2008,7 @@
"uploadImagesLabel",
"uploadImagesHint",
"invalidFileType",
+ "fileTooLarge",
"failedToUpload",
"deleteWarning_one",
"deleteWarning_other",
@@ -4907,6 +4908,15 @@
"refundAmount": {
"type": "string"
},
+ "carryOverPromotion": {
+ "type": "string"
+ },
+ "carryOverPromotionHint": {
+ "type": "string"
+ },
+ "carryOverPromotionTooltip": {
+ "type": "string"
+ },
"activeChangeError": {
"type": "string"
},
@@ -4988,6 +4998,9 @@
"outboundShipping",
"outboundShippingHint",
"refundAmount",
+ "carryOverPromotion",
+ "carryOverPromotionHint",
+ "carryOverPromotionTooltip",
"activeChangeError",
"actions",
"cancel",
diff --git a/packages/admin/dashboard/src/i18n/translations/en.json b/packages/admin/dashboard/src/i18n/translations/en.json
index 945ba1ec01..1bfd787c58 100644
--- a/packages/admin/dashboard/src/i18n/translations/en.json
+++ b/packages/admin/dashboard/src/i18n/translations/en.json
@@ -1305,6 +1305,9 @@
"outboundShipping": "Outbound shipping",
"outboundShippingHint": "Choose which method you want to use.",
"refundAmount": "Estimated difference",
+ "carryOverPromotion": "Carry over promotions",
+ "carryOverPromotionHint": "Apply the order's promotions to the exchange items",
+ "carryOverPromotionTooltip": "Only fixed type promotions with EACH allocation and percentage type promotions with EACH or ACROSS allocation can be carried over to outbound exchange items.",
"activeChangeError": "There is an active order change on this order. Please finish or discard the previous change.",
"actions": {
"cancelExchange": {
diff --git a/packages/admin/dashboard/src/lib/client/client.ts b/packages/admin/dashboard/src/lib/client/client.ts
index 382aa9e6c5..19b8b7d649 100644
--- a/packages/admin/dashboard/src/lib/client/client.ts
+++ b/packages/admin/dashboard/src/lib/client/client.ts
@@ -8,7 +8,7 @@ export const sdk = new Medusa({
baseUrl: backendUrl,
auth: {
type: authType,
- jwtTokenStorageKey
+ jwtTokenStorageKey,
},
})
diff --git a/packages/admin/dashboard/src/routes/orders/order-create-exchange/components/exchange-create-form/exchange-create-form.tsx b/packages/admin/dashboard/src/routes/orders/order-create-exchange/components/exchange-create-form/exchange-create-form.tsx
index ab419e0c6c..320d7290b6 100644
--- a/packages/admin/dashboard/src/routes/orders/order-create-exchange/components/exchange-create-form/exchange-create-form.tsx
+++ b/packages/admin/dashboard/src/routes/orders/order-create-exchange/components/exchange-create-form/exchange-create-form.tsx
@@ -1,5 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod"
-import { PencilSquare } from "@medusajs/icons"
+import { InformationCircleSolid, PencilSquare } from "@medusajs/icons"
import { AdminExchange, AdminOrder, AdminOrderPreview } from "@medusajs/types"
import {
Button,
@@ -8,6 +8,7 @@ import {
IconButton,
Switch,
toast,
+ Tooltip,
usePrompt,
} from "@medusajs/ui"
import { useEffect, useMemo, useState } from "react"
@@ -31,6 +32,7 @@ import {
useUpdateExchangeInboundShipping,
useUpdateExchangeOutboundShipping,
} from "../../../../../hooks/api/exchanges"
+import { useUpdateOrderChange } from "../../../../../hooks/api/orders"
import { currencies } from "../../../../../lib/data/currencies"
import { ExchangeInboundSection } from "./exchange-inbound-section.tsx"
import { ExchangeOutboundSection } from "./exchange-outbound-section"
@@ -92,6 +94,15 @@ export const ExchangeCreateForm = ({
isPending: isUpdatingInboundShipping,
} = useUpdateExchangeOutboundShipping(exchange.id, order.id)
+ const { mutateAsync: updateOrderChange } = useUpdateOrderChange(
+ preview?.order_change?.id!,
+ {
+ onError: (error) => {
+ toast.error(error.message)
+ },
+ }
+ )
+
const isRequestLoading =
isConfirming ||
isCanceling ||
@@ -117,6 +128,14 @@ export const ExchangeCreateForm = ({
(item) => !!item.actions?.find((a) => a.action === "ITEM_ADD")
)
+ const hasPromotions = useMemo(() => {
+ return (
+ (order as any).promotions &&
+ Array.isArray((order as any).promotions) &&
+ (order as any).promotions.length > 0
+ )
+ }, [order])
+
/**
* FORM
*/
@@ -161,6 +180,8 @@ export const ExchangeCreateForm = ({
: "",
location_id: orderReturn?.location_id,
send_notification: false,
+ carry_over_promotions:
+ preview?.order_change?.carry_over_promotions ?? false,
})
},
resolver: zodResolver(ExchangeCreateSchema),
@@ -306,7 +327,14 @@ export const ExchangeCreateForm = ({
const action = item.actions?.find(
(act) => act.action === "RETURN_ITEM"
)
- acc = acc + (action?.amount || 0)
+ /**
+ * TODO: update this when the change actions return amounts are revamped
+ * it is might not cover all the cases but is more accurate then just using `unit_price` which does't consider adjustments
+ */
+ acc =
+ acc +
+ ((action?.details.quantity || 0) / item.quantity) *
+ item.total
return acc
}, 0) * -1,
@@ -323,10 +351,7 @@ export const ExchangeCreateForm = ({
{getStylizedAmount(
outboundPreviewItems.reduce((acc, item) => {
- const action = item.actions?.find(
- (act) => act.action === "ITEM_ADD"
- )
- acc = acc + (action?.amount || 0)
+ acc = acc + (item.total || 0) // outbound items entire quantity is used for calculating outbound total
return acc
}, 0),
@@ -498,6 +523,59 @@ export const ExchangeCreateForm = ({
+
+ {/* CARRY OVER PROMOTION */}
+ {hasPromotions && (
+
+
{
+ return (
+
+
+
+ {
+ onChange(checked)
+ if (preview?.order_change?.id) {
+ await updateOrderChange({
+ carry_over_promotions: checked,
+ })
+ }
+ }}
+ {...field}
+ />
+
+
+
+ {t("orders.exchanges.carryOverPromotion")}
+
+
+
+
+
+
+
+ {t("orders.exchanges.carryOverPromotionHint")}
+
+
+
+
+
+ )
+ }}
+ />
+
+ )}
+
{/* SEND NOTIFICATION*/}
diff --git a/packages/admin/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx b/packages/admin/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx
index 0d588127e1..09f89f2068 100644
--- a/packages/admin/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx
+++ b/packages/admin/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx
@@ -10,6 +10,7 @@ import {
DocumentText,
ExclamationCircle,
PencilSquare,
+ ReceiptPercent,
TriangleDownMini,
} from "@medusajs/icons"
import {
@@ -402,29 +403,49 @@ const Item = ({
item.variant?.inventory_items?.some((i) => i.required_quantity > 1))
const hasUnfulfilledItems = item.quantity - item.detail.fulfilled_quantity > 0
+ const appliedPromoCodes = (item.adjustments || []).map((a) => a.code)
+
return (
<>
-
-
-
-
- {item.title}
-
+
+
+
+
+
+ {item.title}
+
- {item.variant_sku && (
-
- {item.variant_sku}
-
-
- )}
-
- {item.variant?.options?.map((o) => o.value).join(" · ")}
-
+ {item.variant_sku && (
+
+ {item.variant_sku}
+
+
+ )}
+
+ {item.variant?.options?.map((o) => o.value).join(" · ")}
+
+
+ {appliedPromoCodes.length > 0 && (
+
+ {appliedPromoCodes.map((code) => (
+ {code}
+ ))}
+
+ }
+ >
+
+
+ )}
diff --git a/packages/core/core-flows/src/order/steps/index.ts b/packages/core/core-flows/src/order/steps/index.ts
index 4196d783c9..026948b95d 100644
--- a/packages/core/core-flows/src/order/steps/index.ts
+++ b/packages/core/core-flows/src/order/steps/index.ts
@@ -17,6 +17,7 @@ export * from "./delete-order-change-actions"
export * from "./delete-order-changes"
export * from "./delete-order-shipping-methods"
export * from "./exchange/cancel-exchange"
+export * from "./list-order-change-actions-by-type"
export * from "./exchange/create-exchange"
export * from "./exchange/create-exchange-items-from-actions"
export * from "./exchange/delete-exchanges"
diff --git a/packages/core/core-flows/src/order/steps/list-order-change-actions-by-type.ts b/packages/core/core-flows/src/order/steps/list-order-change-actions-by-type.ts
new file mode 100644
index 0000000000..c58bb81c3e
--- /dev/null
+++ b/packages/core/core-flows/src/order/steps/list-order-change-actions-by-type.ts
@@ -0,0 +1,37 @@
+import { IOrderModuleService } from "@medusajs/framework/types"
+import { ChangeActionType, Modules } from "@medusajs/framework/utils"
+import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
+
+/**
+ * This step lists order change actions filtered by action type.
+ */
+export const listOrderChangeActionsByTypeStep = createStep(
+ "list-order-change-actions-by-type",
+ async function (
+ {
+ order_change_id,
+ action_type,
+ }: {
+ order_change_id: string
+ action_type: ChangeActionType
+ },
+ { container }
+ ) {
+ const service = container.resolve
(Modules.ORDER)
+
+ const actions = await service.listOrderChangeActions(
+ {
+ order_change_id,
+ },
+ {
+ select: ["id", "action"],
+ }
+ )
+
+ const filteredActions = actions.filter(
+ (action) => action.action === action_type
+ )
+
+ return new StepResponse(filteredActions.map((action) => action.id))
+ }
+)
diff --git a/packages/core/core-flows/src/order/workflows/compute-adjustments-for-preview.ts b/packages/core/core-flows/src/order/workflows/compute-adjustments-for-preview.ts
new file mode 100644
index 0000000000..c8b11a77cb
--- /dev/null
+++ b/packages/core/core-flows/src/order/workflows/compute-adjustments-for-preview.ts
@@ -0,0 +1,166 @@
+import {
+ ComputeActionContext,
+ OrderChangeDTO,
+ OrderDTO,
+ PromotionDTO,
+} from "@medusajs/framework/types"
+import { ChangeActionType } from "@medusajs/framework/utils"
+import {
+ createWorkflow,
+ transform,
+ when,
+ WorkflowData,
+} from "@medusajs/framework/workflows-sdk"
+import {
+ getActionsToComputeFromPromotionsStep,
+ prepareAdjustmentsFromPromotionActionsStep,
+} from "../../cart"
+import { previewOrderChangeStep } from "../steps/preview-order-change"
+import { createOrderChangeActionsWorkflow } from "./create-order-change-actions"
+import {
+ deleteOrderChangeActionsStep,
+ listOrderChangeActionsByTypeStep,
+} from "../steps"
+
+/**
+ * The data to compute adjustments for an order edit, exchange, claim, or return.
+ */
+export type ComputeAdjustmentsForPreviewWorkflowInput = {
+ /**
+ * The order's details.
+ */
+ order: OrderDTO & { promotions: PromotionDTO[] }
+ /**
+ * The order change's details.
+ */
+ orderChange: OrderChangeDTO
+}
+
+export const computeAdjustmentsForPreviewWorkflowId =
+ "compute-adjustments-for-preview"
+/**
+ * This workflow computes adjustments for an order change if the carry over promotions flag is true on the order change.
+ * If the flag is false, it deletes the existing adjustments replacement actions.
+ *
+ * It is currently used as a part of the order edit and exchange flows.
+ * It's used by the [Add Items to Order Edit Admin API Route](https://docs.medusajs.com/api/admin#order-edits_postordereditsiditems),
+ * [Add Outbound Items Admin API Route](https://docs.medusajs.com/api/admin#exchanges_postexchangesidoutbounditems),
+ * and [Add Inbound Items Admin API Route](https://docs.medusajs.com/api/admin#exchanges_postexchangesidinbounditems).
+ *
+ * You can use this workflow within your customizations or your own custom workflows, allowing you to compute adjustments
+ * in your custom flows.
+ *
+ * @example
+ * const { result } = await computeAdjustmentsForPreviewWorkflow(container)
+ * .run({
+ * input: {
+ * order: {
+ * id: "order_123",
+ * // other order details...
+ * },
+ * orderChange: {
+ * id: "orch_123",
+ * // other order change details...
+ * },
+ * exchange_id: "exchange_123", // optional, for exchanges
+ * }
+ * })
+ *
+ * @summary
+ *
+ * Compute adjustments for an order edit, exchange, claim, or return.
+ */
+export const computeAdjustmentsForPreviewWorkflow = createWorkflow(
+ computeAdjustmentsForPreviewWorkflowId,
+ function (input: WorkflowData) {
+ const previewedOrder = previewOrderChangeStep(input.order.id)
+
+ when(
+ { order: input.order },
+ ({ order }) =>
+ /**
+ * Compute adjustments only if the flag on the order change is true
+ */
+ !!order.promotions.length && !!input.orderChange.carry_over_promotions
+ ).then(() => {
+ const actionsToComputeItemsInput = transform(
+ { previewedOrder, order: input.order },
+ ({ previewedOrder, order }) => {
+ return {
+ currency_code: order.currency_code,
+ items: previewedOrder.items.map((item) => ({
+ ...item,
+ // Buy-Get promotions rely on the product ID, so we need to manually set it before refreshing adjustments
+ product: { id: item.product_id },
+ })),
+ } as ComputeActionContext
+ }
+ )
+
+ const orderPromotions = transform({ order: input.order }, ({ order }) => {
+ return order.promotions
+ .map((p) => p.code)
+ .filter((p) => p !== undefined)
+ })
+
+ const actions = getActionsToComputeFromPromotionsStep({
+ computeActionContext: actionsToComputeItemsInput,
+ promotionCodesToApply: orderPromotions,
+ })
+
+ const { lineItemAdjustmentsToCreate } =
+ prepareAdjustmentsFromPromotionActionsStep({ actions })
+
+ const orderChangeActionAdjustmentsInput = transform(
+ {
+ order: input.order,
+ previewedOrder,
+ orderChange: input.orderChange,
+ lineItemAdjustmentsToCreate,
+ },
+ ({
+ order,
+ previewedOrder,
+ orderChange,
+ lineItemAdjustmentsToCreate,
+ }) => {
+ return previewedOrder.items.map((item) => {
+ const itemAdjustments = lineItemAdjustmentsToCreate.filter(
+ (adjustment) => adjustment.item_id === item.id
+ )
+
+ return {
+ order_change_id: orderChange.id,
+ order_id: order.id,
+ exchange_id: orderChange.exchange_id ?? undefined,
+ claim_id: orderChange.claim_id ?? undefined,
+ return_id: orderChange.return_id ?? undefined,
+ version: orderChange.version,
+ action: ChangeActionType.ITEM_ADJUSTMENTS_REPLACE,
+ details: {
+ reference_id: item.id,
+ adjustments: itemAdjustments,
+ },
+ }
+ })
+ }
+ )
+
+ createOrderChangeActionsWorkflow
+ .runAsStep({ input: orderChangeActionAdjustmentsInput })
+ .config({ name: "order-change-action-adjustments-input" })
+ })
+
+ when(
+ { order: previewedOrder },
+ ({ order }) => !order.order_change.carry_over_promotions
+ ).then(() => {
+ const actionIds = listOrderChangeActionsByTypeStep({
+ order_change_id: input.orderChange.id,
+ action_type: ChangeActionType.ITEM_ADJUSTMENTS_REPLACE,
+ })
+
+ deleteOrderChangeActionsStep({ ids: actionIds })
+ })
+ }
+)
diff --git a/packages/core/core-flows/src/order/workflows/exchange/exchange-add-new-item.ts b/packages/core/core-flows/src/order/workflows/exchange/exchange-add-new-item.ts
index 15da5854b5..d5072fd9c1 100644
--- a/packages/core/core-flows/src/order/workflows/exchange/exchange-add-new-item.ts
+++ b/packages/core/core-flows/src/order/workflows/exchange/exchange-add-new-item.ts
@@ -22,7 +22,7 @@ import {
} from "../../utils/order-validation"
import { addOrderLineItemsWorkflow } from "../add-line-items"
import { createOrderChangeActionsWorkflow } from "../create-order-change-actions"
-import { computeAdjustmentsForPreviewWorkflow } from "../order-edit/compute-adjustments-for-preview"
+import { computeAdjustmentsForPreviewWorkflow } from "../compute-adjustments-for-preview"
import { updateOrderTaxLinesWorkflow } from "../update-tax-lines"
import { refreshExchangeShippingWorkflow } from "./refresh-shipping"
@@ -140,7 +140,13 @@ export const orderExchangeAddNewItemWorkflow = createWorkflow(
const orderChange: OrderChangeDTO = useRemoteQueryStep({
entry_point: "order_change",
- fields: ["id", "status", "version"],
+ fields: [
+ "id",
+ "status",
+ "version",
+ "exchange_id",
+ "carry_over_promotions",
+ ],
variables: {
filters: {
order_id: orderExchange.order_id,
@@ -212,7 +218,6 @@ export const orderExchangeAddNewItemWorkflow = createWorkflow(
input: {
order: orderWithPromotions,
orderChange,
- exchange_id: orderExchange.id,
},
})
diff --git a/packages/core/core-flows/src/order/workflows/exchange/exchange-request-item-return.ts b/packages/core/core-flows/src/order/workflows/exchange/exchange-request-item-return.ts
index 118d3c648f..999950de0f 100644
--- a/packages/core/core-flows/src/order/workflows/exchange/exchange-request-item-return.ts
+++ b/packages/core/core-flows/src/order/workflows/exchange/exchange-request-item-return.ts
@@ -33,7 +33,7 @@ import {
throwIfOrderChangeIsNotActive,
} from "../../utils/order-validation"
import { createOrderChangeActionsWorkflow } from "../create-order-change-actions"
-import { computeAdjustmentsForPreviewWorkflow } from "../order-edit/compute-adjustments-for-preview"
+import { computeAdjustmentsForPreviewWorkflow } from "../compute-adjustments-for-preview"
import { refreshExchangeShippingWorkflow } from "./refresh-shipping"
/**
@@ -194,7 +194,13 @@ export const orderExchangeRequestItemReturnWorkflow = createWorkflow(
const orderChange: OrderChangeDTO = useRemoteQueryStep({
entry_point: "order_change",
- fields: ["id", "status", "version"],
+ fields: [
+ "id",
+ "status",
+ "version",
+ "exchange_id",
+ "carry_over_promotions",
+ ],
variables: {
filters: {
order_id: orderExchange.order_id,
@@ -321,7 +327,6 @@ export const orderExchangeRequestItemReturnWorkflow = createWorkflow(
input: {
order: orderWithPromotions,
orderChange,
- exchange_id: orderExchange.id,
},
})
diff --git a/packages/core/core-flows/src/order/workflows/exchange/update-exchange-add-item.ts b/packages/core/core-flows/src/order/workflows/exchange/update-exchange-add-item.ts
index 87579a356c..a0e4b865d9 100644
--- a/packages/core/core-flows/src/order/workflows/exchange/update-exchange-add-item.ts
+++ b/packages/core/core-flows/src/order/workflows/exchange/update-exchange-add-item.ts
@@ -3,6 +3,7 @@ import {
OrderChangeDTO,
OrderDTO,
OrderExchangeDTO,
+ PromotionDTO,
OrderPreviewDTO,
OrderWorkflow,
} from "@medusajs/framework/types"
@@ -24,6 +25,7 @@ import {
throwIfOrderChangeIsNotActive,
} from "../../utils/order-validation"
import { refreshExchangeShippingWorkflow } from "./refresh-shipping"
+import { computeAdjustmentsForPreviewWorkflow } from "../compute-adjustments-for-preview"
/**
* The data to validate that an outbound or new item in an exchange can be updated.
@@ -150,7 +152,7 @@ export const updateExchangeAddItemWorkflow = createWorkflow(
const order: OrderDTO = useRemoteQueryStep({
entry_point: "orders",
- fields: ["id", "status", "canceled_at", "items.*"],
+ fields: ["id", "status", "canceled_at", "items.*", "promotions.*"],
variables: { id: orderExchange.order_id },
list: false,
throw_if_key_not_found: true,
@@ -158,7 +160,14 @@ export const updateExchangeAddItemWorkflow = createWorkflow(
const orderChange: OrderChangeDTO = useRemoteQueryStep({
entry_point: "order_change",
- fields: ["id", "status", "version", "actions.*"],
+ fields: [
+ "id",
+ "status",
+ "version",
+ "exchange_id",
+ "actions.*",
+ "carry_over_promotions",
+ ],
variables: {
filters: {
order_id: orderExchange.order_id,
@@ -207,6 +216,20 @@ export const updateExchangeAddItemWorkflow = createWorkflow(
}
)
+ const orderWithPromotions = transform({ order }, ({ order }) => {
+ return {
+ ...order,
+ promotions: (order as any).promotions ?? [],
+ } as OrderDTO & { promotions: PromotionDTO[] }
+ })
+
+ computeAdjustmentsForPreviewWorkflow.runAsStep({
+ input: {
+ order: orderWithPromotions,
+ orderChange,
+ },
+ })
+
refreshExchangeShippingWorkflow.runAsStep({
input: refreshArgs,
})
diff --git a/packages/core/core-flows/src/order/workflows/index.ts b/packages/core/core-flows/src/order/workflows/index.ts
index 7da3479153..a972ad1905 100644
--- a/packages/core/core-flows/src/order/workflows/index.ts
+++ b/packages/core/core-flows/src/order/workflows/index.ts
@@ -50,7 +50,7 @@ export * from "./mark-payment-collection-as-paid"
export * from "./maybe-refresh-shipping-methods"
export * from "./order-edit/begin-order-edit"
export * from "./order-edit/cancel-begin-order-edit"
-export * from "./order-edit/compute-adjustments-for-preview"
+export * from "./compute-adjustments-for-preview"
export * from "./order-edit/confirm-order-edit-request"
export * from "./order-edit/create-order-edit-shipping-method"
export * from "./order-edit/order-edit-add-new-item"
@@ -81,11 +81,13 @@ export * from "./return/update-receive-item-return-request"
export * from "./return/update-request-item-return"
export * from "./return/update-return"
export * from "./return/update-return-shipping-method"
+export * from "./on-carry-promotions-flag-set"
export * from "./transfer/accept-order-transfer"
export * from "./transfer/cancel-order-transfer"
export * from "./transfer/decline-order-transfer"
export * from "./transfer/request-order-transfer"
export * from "./update-order"
+export * from "./update-order-change"
export * from "./update-order-change-actions"
export * from "./update-order-changes"
export * from "./update-tax-lines"
diff --git a/packages/core/core-flows/src/order/workflows/on-carry-promotions-flag-set.ts b/packages/core/core-flows/src/order/workflows/on-carry-promotions-flag-set.ts
new file mode 100644
index 0000000000..ccac690607
--- /dev/null
+++ b/packages/core/core-flows/src/order/workflows/on-carry-promotions-flag-set.ts
@@ -0,0 +1,201 @@
+import {
+ OrderChangeDTO,
+ OrderDTO,
+ PromotionDTO,
+} from "@medusajs/framework/types"
+import {
+ ApplicationMethodAllocation,
+ MedusaError,
+} from "@medusajs/framework/utils"
+import {
+ WorkflowData,
+ WorkflowResponse,
+ createStep,
+ createWorkflow,
+ transform,
+} from "@medusajs/framework/workflows-sdk"
+import { useRemoteQueryStep } from "../../common"
+import { throwIfOrderChangeIsNotActive } from "../utils/order-validation"
+
+import { computeAdjustmentsForPreviewWorkflow } from "./compute-adjustments-for-preview"
+
+/**
+ * The data to set the carry over promotions flag for an order change.
+ */
+export type OnCarryPromotionsFlagSetWorkflowInput = {
+ /**
+ * The order change's ID.
+ */
+ order_change_id: string
+ /**
+ * Whether to carry over promotions to outbound exchange items.
+ */
+ carry_over_promotions: boolean
+}
+
+/**
+ * This step validates that the order change is an exchange and validates promotion allocation.
+ */
+export const validateCarryPromotionsFlagStep = createStep(
+ "validate-carry-promotions-flag",
+ async function ({
+ orderChange,
+ order,
+ input,
+ }: {
+ orderChange: OrderChangeDTO
+ order: OrderDTO & { promotions?: PromotionDTO[] }
+ input: OnCarryPromotionsFlagSetWorkflowInput
+ }) {
+ // Validate order change is active
+ throwIfOrderChangeIsNotActive({ orderChange })
+
+ // we don't need to validate promotion since we will be resetting the adjustments
+ if (!input.carry_over_promotions) {
+ return
+ }
+
+ // Validate promotion allocation if promotions exist
+ if (order.promotions && order.promotions.length > 0) {
+ const invalidPromotions: string[] = []
+
+ for (const promotion of order.promotions) {
+ const applicationMethod = (promotion as any).application_method
+
+ if (!applicationMethod) {
+ continue
+ }
+
+ const allocation = applicationMethod.allocation
+ const type = applicationMethod.type
+
+ if (
+ allocation !== ApplicationMethodAllocation.ACROSS &&
+ allocation !== ApplicationMethodAllocation.EACH
+ ) {
+ throw new MedusaError(
+ MedusaError.Types.INVALID_DATA,
+ `Promotion ${
+ promotion.code || promotion.id
+ } has invalid allocation. Only promotions with EACH or ACROSS allocation can be carried over to outbound exchange items.`
+ )
+ }
+
+ // For fixed promotions, allocation must be EACH
+ if (
+ type === "fixed" &&
+ allocation !== ApplicationMethodAllocation.EACH
+ ) {
+ invalidPromotions.push(promotion.code || promotion.id)
+ }
+
+ // For percentage promotions, allocation must be EACH or ACROSS
+ if (
+ type === "percentage" &&
+ allocation !== ApplicationMethodAllocation.EACH &&
+ allocation !== ApplicationMethodAllocation.ACROSS
+ ) {
+ invalidPromotions.push(promotion.code || promotion.id)
+ }
+ }
+
+ if (invalidPromotions.length > 0) {
+ throw new MedusaError(
+ MedusaError.Types.INVALID_DATA,
+ `Promotions with codes ${invalidPromotions.join(
+ ", "
+ )} have invalid allocation. Fixed promotions must have EACH allocation, and percentage promotions must have EACH or ACROSS allocation.`
+ )
+ }
+ }
+ }
+)
+
+export const onCarryPromotionsFlagSetId = "on-carry-promotions-flag-set"
+
+/**
+ * This workflow sets the carry over promotions flag for an order change.
+ * It validates that the order change is active and is an exchange, validates promotion allocation,
+ * and either applies or removes promotion adjustments based on the flag value.
+ *
+ * @example
+ * const { result } = await onCarryPromotionsFlagSet(container)
+ * .run({
+ * input: {
+ * order_change_id: "orch_123",
+ * carry_over_promotions: true,
+ * }
+ * })
+ *
+ * @summary
+ *
+ * Set the carry over promotions flag for an order change.
+ */
+export const onCarryPromotionsFlagSet = createWorkflow(
+ onCarryPromotionsFlagSetId,
+ function (
+ input: WorkflowData
+ ): WorkflowResponse {
+ const orderChange: OrderChangeDTO = useRemoteQueryStep({
+ entry_point: "order_change",
+ fields: [
+ "id",
+ "status",
+ "version",
+ "exchange_id",
+ "claim_id",
+ "return_id",
+ "order_id",
+ "canceled_at",
+ "confirmed_at",
+ "declined_at",
+ "carry_over_promotions",
+ ],
+ variables: {
+ filters: {
+ id: input.order_change_id,
+ },
+ },
+ list: false,
+ throw_if_key_not_found: true,
+ }).config({ name: "order-change-query" })
+
+ const order: OrderDTO & { promotions?: PromotionDTO[] } =
+ useRemoteQueryStep({
+ entry_point: "orders",
+ fields: [
+ "id",
+ "currency_code",
+ "promotions.*",
+ "promotions.application_method.*",
+ ],
+ variables: {
+ id: orderChange.order_id,
+ },
+ list: false,
+ throw_if_key_not_found: true,
+ }).config({ name: "order-query" })
+
+ validateCarryPromotionsFlagStep({
+ orderChange,
+ order,
+ input,
+ })
+
+ const orderWithPromotions = transform({ order }, ({ order }) => {
+ return {
+ ...order,
+ promotions: (order as any).promotions ?? [],
+ } as OrderDTO & { promotions: PromotionDTO[] }
+ })
+
+ computeAdjustmentsForPreviewWorkflow.runAsStep({
+ input: {
+ orderChange,
+ order: orderWithPromotions,
+ },
+ })
+
+ return new WorkflowResponse(void 0)
+ }
+)
diff --git a/packages/core/core-flows/src/order/workflows/order-edit/compute-adjustments-for-preview.ts b/packages/core/core-flows/src/order/workflows/order-edit/compute-adjustments-for-preview.ts
deleted file mode 100644
index 5ea22e39ba..0000000000
--- a/packages/core/core-flows/src/order/workflows/order-edit/compute-adjustments-for-preview.ts
+++ /dev/null
@@ -1,163 +0,0 @@
-import {
- ComputeActionContext,
- OrderChangeDTO,
- OrderDTO,
- PromotionDTO,
-} from "@medusajs/framework/types"
-import { ChangeActionType } from "@medusajs/framework/utils"
-import {
- createWorkflow,
- transform,
- when,
- WorkflowData,
-} from "@medusajs/framework/workflows-sdk"
-import {
- getActionsToComputeFromPromotionsStep,
- prepareAdjustmentsFromPromotionActionsStep,
-} from "../../../cart"
-import { previewOrderChangeStep } from "../../steps/preview-order-change"
-import { createOrderChangeActionsWorkflow } from "../create-order-change-actions"
-
-/**
- * The data to compute adjustments for an order edit, exchange, claim, or return.
- */
-export type ComputeAdjustmentsForPreviewWorkflowInput = {
- /**
- * The order's details.
- */
- order: OrderDTO & { promotions: PromotionDTO[] }
- /**
- * The order change's details.
- */
- orderChange: OrderChangeDTO
- /**
- * Optional exchange ID to include in the order change action.
- */
- exchange_id?: string
- /**
- * Optional claim ID to include in the order change action.
- */
- claim_id?: string
- /**
- * Optional return ID to include in the order change action.
- */
- return_id?: string
-}
-
-export const computeAdjustmentsForPreviewWorkflowId =
- "compute-adjustments-for-preview"
-/**
- * This workflow computes adjustments for an order edit, exchange, claim, or return.
- * It's used by the [Add Items to Order Edit Admin API Route](https://docs.medusajs.com/api/admin#order-edits_postordereditsiditems),
- * [Add Outbound Items Admin API Route](https://docs.medusajs.com/api/admin#exchanges_postexchangesidoutbounditems),
- * and [Add Inbound Items Admin API Route](https://docs.medusajs.com/api/admin#exchanges_postexchangesidinbounditems).
- *
- * You can use this workflow within your customizations or your own custom workflows, allowing you to compute adjustments
- * in your custom flows.
- *
- * @example
- * const { result } = await computeAdjustmentsForPreviewWorkflow(container)
- * .run({
- * input: {
- * order: {
- * id: "order_123",
- * // other order details...
- * },
- * orderChange: {
- * id: "orch_123",
- * // other order change details...
- * },
- * exchange_id: "exchange_123", // optional, for exchanges
- * }
- * })
- *
- * @summary
- *
- * Compute adjustments for an order edit, exchange, claim, or return.
- */
-export const computeAdjustmentsForPreviewWorkflow = createWorkflow(
- computeAdjustmentsForPreviewWorkflowId,
- function (input: WorkflowData) {
- const previewedOrder = previewOrderChangeStep(input.order.id)
-
- when({ order: input.order }, ({ order }) => !!order.promotions.length).then(
- () => {
- const actionsToComputeItemsInput = transform(
- { previewedOrder, order: input.order },
- ({ previewedOrder, order }) => {
- return {
- currency_code: order.currency_code,
- items: previewedOrder.items.map((item) => ({
- ...item,
- // Buy-Get promotions rely on the product ID, so we need to manually set it before refreshing adjustments
- product: { id: item.product_id },
- })),
- } as ComputeActionContext
- }
- )
-
- const orderPromotions = transform(
- { order: input.order },
- ({ order }) => {
- return order.promotions
- .map((p) => p.code)
- .filter((p) => p !== undefined)
- }
- )
-
- const actions = getActionsToComputeFromPromotionsStep({
- computeActionContext: actionsToComputeItemsInput,
- promotionCodesToApply: orderPromotions,
- })
-
- const { lineItemAdjustmentsToCreate } =
- prepareAdjustmentsFromPromotionActionsStep({ actions })
-
- const orderChangeActionAdjustmentsInput = transform(
- {
- order: input.order,
- previewedOrder,
- orderChange: input.orderChange,
- lineItemAdjustmentsToCreate,
- exchangeId: input.exchange_id,
- claimId: input.claim_id,
- returnId: input.return_id,
- },
- ({
- order,
- previewedOrder,
- orderChange,
- lineItemAdjustmentsToCreate,
- exchangeId,
- claimId,
- returnId,
- }) => {
- return previewedOrder.items.map((item) => {
- const itemAdjustments = lineItemAdjustmentsToCreate.filter(
- (adjustment) => adjustment.item_id === item.id
- )
-
- return {
- order_change_id: orderChange.id,
- order_id: order.id,
- ...(exchangeId && { exchange_id: exchangeId }),
- ...(claimId && { claim_id: claimId }),
- ...(returnId && { return_id: returnId }),
- version: orderChange.version,
- action: ChangeActionType.ITEM_ADJUSTMENTS_REPLACE,
- details: {
- reference_id: item.id,
- adjustments: itemAdjustments,
- },
- }
- })
- }
- )
-
- createOrderChangeActionsWorkflow
- .runAsStep({ input: orderChangeActionAdjustmentsInput })
- .config({ name: "order-change-action-adjustments-input" })
- }
- )
- }
-)
diff --git a/packages/core/core-flows/src/order/workflows/order-edit/order-edit-add-new-item.ts b/packages/core/core-flows/src/order/workflows/order-edit/order-edit-add-new-item.ts
index 50171a09d9..525123318f 100644
--- a/packages/core/core-flows/src/order/workflows/order-edit/order-edit-add-new-item.ts
+++ b/packages/core/core-flows/src/order/workflows/order-edit/order-edit-add-new-item.ts
@@ -22,7 +22,7 @@ import { addOrderLineItemsWorkflow } from "../add-line-items"
import { createOrderChangeActionsWorkflow } from "../create-order-change-actions"
import { updateOrderTaxLinesWorkflow } from "../update-tax-lines"
import { fieldsToRefreshOrderEdit } from "./utils/fields"
-import { computeAdjustmentsForPreviewWorkflow } from "./compute-adjustments-for-preview"
+import { computeAdjustmentsForPreviewWorkflow } from "../compute-adjustments-for-preview"
/**
* The data to validate that new items can be added to an order edit.
@@ -118,7 +118,7 @@ export const orderEditAddNewItemWorkflow = createWorkflow(
const orderChangeResult = useQueryGraphStep({
entity: "order_change",
- fields: ["id", "status", "version", "actions.*"],
+ fields: ["id", "status", "version", "actions.*", "carry_over_promotions"],
filters: {
order_id: input.order_id,
status: [OrderChangeStatus.PENDING, OrderChangeStatus.REQUESTED],
diff --git a/packages/core/core-flows/src/order/workflows/order-edit/order-edit-update-item-quantity.ts b/packages/core/core-flows/src/order/workflows/order-edit/order-edit-update-item-quantity.ts
index 57e8a14d9f..594034e85c 100644
--- a/packages/core/core-flows/src/order/workflows/order-edit/order-edit-update-item-quantity.ts
+++ b/packages/core/core-flows/src/order/workflows/order-edit/order-edit-update-item-quantity.ts
@@ -24,7 +24,7 @@ import {
throwIfOrderChangeIsNotActive,
} from "../../utils/order-validation"
import { createOrderChangeActionsWorkflow } from "../create-order-change-actions"
-import { computeAdjustmentsForPreviewWorkflow } from "./compute-adjustments-for-preview"
+import { computeAdjustmentsForPreviewWorkflow } from "../compute-adjustments-for-preview"
import { fieldsToRefreshOrderEdit } from "./utils/fields"
/**
@@ -128,7 +128,7 @@ export const orderEditUpdateItemQuantityWorkflow = createWorkflow(
const orderChangeResult = useQueryGraphStep({
entity: "order_change",
- fields: ["id", "status", "version", "actions.*"],
+ fields: ["id", "status", "version", "actions.*", "carry_over_promotions"],
filters: {
order_id: input.order_id,
status: [OrderChangeStatus.PENDING, OrderChangeStatus.REQUESTED],
diff --git a/packages/core/core-flows/src/order/workflows/order-edit/remove-order-edit-item-action.ts b/packages/core/core-flows/src/order/workflows/order-edit/remove-order-edit-item-action.ts
index f44f03970f..53805d4b7d 100644
--- a/packages/core/core-flows/src/order/workflows/order-edit/remove-order-edit-item-action.ts
+++ b/packages/core/core-flows/src/order/workflows/order-edit/remove-order-edit-item-action.ts
@@ -22,7 +22,7 @@ import {
throwIfIsCancelled,
throwIfOrderChangeIsNotActive,
} from "../../utils/order-validation"
-import { computeAdjustmentsForPreviewWorkflow } from "./compute-adjustments-for-preview"
+import { computeAdjustmentsForPreviewWorkflow } from "../compute-adjustments-for-preview"
import { fieldsToRefreshOrderEdit } from "./utils/fields"
/**
@@ -143,7 +143,7 @@ export const removeItemOrderEditActionWorkflow = createWorkflow(
const orderChangeResult = useQueryGraphStep({
entity: "order_change",
- fields: ["id", "status", "version", "actions.*"],
+ fields: ["id", "status", "version", "actions.*", "carry_over_promotions"],
filters: {
order_id: input.order_id,
status: [OrderChangeStatus.PENDING, OrderChangeStatus.REQUESTED],
diff --git a/packages/core/core-flows/src/order/workflows/order-edit/update-order-edit-add-item.ts b/packages/core/core-flows/src/order/workflows/order-edit/update-order-edit-add-item.ts
index 272ba12b32..7213f15c78 100644
--- a/packages/core/core-flows/src/order/workflows/order-edit/update-order-edit-add-item.ts
+++ b/packages/core/core-flows/src/order/workflows/order-edit/update-order-edit-add-item.ts
@@ -22,7 +22,7 @@ import {
throwIfIsCancelled,
throwIfOrderChangeIsNotActive,
} from "../../utils/order-validation"
-import { computeAdjustmentsForPreviewWorkflow } from "./compute-adjustments-for-preview"
+import { computeAdjustmentsForPreviewWorkflow } from "../compute-adjustments-for-preview"
import { fieldsToRefreshOrderEdit } from "./utils/fields"
/**
@@ -142,7 +142,7 @@ export const updateOrderEditAddItemWorkflow = createWorkflow(
const orderChangeResult = useQueryGraphStep({
entity: "order_change",
- fields: ["id", "status", "version", "actions.*"],
+ fields: ["id", "status", "version", "actions.*", "carry_over_promotions"],
filters: {
order_id: input.order_id,
status: [OrderChangeStatus.PENDING, OrderChangeStatus.REQUESTED],
diff --git a/packages/core/core-flows/src/order/workflows/order-edit/update-order-edit-item-quantity.ts b/packages/core/core-flows/src/order/workflows/order-edit/update-order-edit-item-quantity.ts
index 6ee6d094e8..09a758eb09 100644
--- a/packages/core/core-flows/src/order/workflows/order-edit/update-order-edit-item-quantity.ts
+++ b/packages/core/core-flows/src/order/workflows/order-edit/update-order-edit-item-quantity.ts
@@ -22,7 +22,7 @@ import {
throwIfIsCancelled,
throwIfOrderChangeIsNotActive,
} from "../../utils/order-validation"
-import { computeAdjustmentsForPreviewWorkflow } from "./compute-adjustments-for-preview"
+import { computeAdjustmentsForPreviewWorkflow } from "../compute-adjustments-for-preview"
import { fieldsToRefreshOrderEdit } from "./utils/fields"
/**
diff --git a/packages/core/core-flows/src/order/workflows/update-order-change.ts b/packages/core/core-flows/src/order/workflows/update-order-change.ts
new file mode 100644
index 0000000000..99b779533d
--- /dev/null
+++ b/packages/core/core-flows/src/order/workflows/update-order-change.ts
@@ -0,0 +1,59 @@
+import { OrderChangeDTO, UpdateOrderChangeDTO } from "@medusajs/framework/types"
+import {
+ WorkflowData,
+ WorkflowResponse,
+ createWorkflow,
+ transform,
+ when,
+} from "@medusajs/framework/workflows-sdk"
+import { updateOrderChangesStep } from "../steps/update-order-changes"
+import { onCarryPromotionsFlagSet } from "./on-carry-promotions-flag-set"
+
+export const updateOrderChangeWorkflowId = "update-order-change-workflow"
+
+/**
+ * This workflow updates an order change.
+ * If the carry_over_promotions flag is provided, it calls onCarryPromotionsFlagSet
+ * to handle the promotion logic. Otherwise, it updates the order change directly.
+ *
+ * @example
+ * const { result } = await updateOrderChangeWorkflow(container)
+ * .run({
+ * input: {
+ * id: "orch_123",
+ * carry_over_promotions: true,
+ * }
+ * })
+ *
+ * @summary
+ *
+ * Update an order change, conditionally handling promotion carry-over if specified.
+ */
+export const updateOrderChangeWorkflow = createWorkflow(
+ updateOrderChangeWorkflowId,
+ function (
+ input: WorkflowData
+ ): WorkflowResponse {
+ const updatedOrderChange = updateOrderChangesStep([input])
+
+ when(
+ "should-call-carry-over-promotion-workflow",
+ input,
+ ({ carry_over_promotions }) => typeof carry_over_promotions === "boolean"
+ ).then(() => {
+ return onCarryPromotionsFlagSet.runAsStep({
+ input: {
+ order_change_id: input.id,
+ carry_over_promotions: input.carry_over_promotions!,
+ },
+ })
+ })
+
+ return new WorkflowResponse(
+ transform(
+ { updatedOrderChange },
+ ({ updatedOrderChange }) => updatedOrderChange?.[0]
+ )
+ )
+ }
+)
diff --git a/packages/core/js-sdk/src/admin/order.ts b/packages/core/js-sdk/src/admin/order.ts
index 122392e4de..63ae9ce5e2 100644
--- a/packages/core/js-sdk/src/admin/order.ts
+++ b/packages/core/js-sdk/src/admin/order.ts
@@ -633,4 +633,43 @@ export class Order {
}
)
}
+
+ /**
+ * This method updates an order change. It sends a request to the
+ * [Update Order Change](https://docs.medusajs.com/api/admin#order-changes_postorder-changesid)
+ * API route.
+ *
+ * @param id - The order change's ID.
+ * @param body - The update details.
+ * @param query - Configure the fields to retrieve in the order change.
+ * @param headers - Headers to pass in the request
+ * @returns The order change's details.
+ *
+ * @example
+ * sdk.admin.order.updateOrderChange(
+ * "ordch_123",
+ * {
+ * carry_over_promotions: true
+ * }
+ * )
+ * .then(({ order_change }) => {
+ * console.log(order_change)
+ * })
+ */
+ async updateOrderChange(
+ id: string,
+ body: { carry_over_promotions: boolean },
+ query?: SelectParams,
+ headers?: ClientHeaders
+ ) {
+ return await this.client.fetch(
+ `/admin/order-changes/${id}`,
+ {
+ method: "POST",
+ headers,
+ body,
+ query,
+ }
+ )
+ }
}
diff --git a/packages/core/types/src/http/order/admin/responses.ts b/packages/core/types/src/http/order/admin/responses.ts
index 261f862fa4..548e2631a1 100644
--- a/packages/core/types/src/http/order/admin/responses.ts
+++ b/packages/core/types/src/http/order/admin/responses.ts
@@ -20,6 +20,13 @@ export interface AdminOrderChangesResponse {
order_changes: AdminOrderChange[]
}
+export interface AdminOrderChangeResponse {
+ /**
+ * The order change's details.
+ */
+ order_change: AdminOrderChange
+}
+
export type AdminOrderListResponse = PaginatedResponse<{
/**
* The list of orders.
diff --git a/packages/core/types/src/http/order/common.ts b/packages/core/types/src/http/order/common.ts
index f6b7be2510..586bd049ff 100644
--- a/packages/core/types/src/http/order/common.ts
+++ b/packages/core/types/src/http/order/common.ts
@@ -1012,6 +1012,11 @@ export interface BaseOrderChange {
*/
actions: BaseOrderChangeAction[]
+ /**
+ * Whether to carry over promotions (apply promotions to outbound exchange items).
+ */
+ carry_over_promotions?: boolean | null
+
/**
* The status of the order change
*/
diff --git a/packages/core/types/src/order/common.ts b/packages/core/types/src/order/common.ts
index 963efdf447..f6c9994f3d 100644
--- a/packages/core/types/src/order/common.ts
+++ b/packages/core/types/src/order/common.ts
@@ -2117,6 +2117,11 @@ export interface OrderChangeDTO {
*/
change_type?: "return" | "exchange" | "claim" | "edit" | "transfer"
+ /**
+ * Whether to carry over promotions (apply promotions to outbound exchange items).
+ */
+ carry_over_promotions?: boolean | null
+
/**
* The ID of the associated order
*/
diff --git a/packages/core/types/src/order/mutations.ts b/packages/core/types/src/order/mutations.ts
index e2bfeabf2f..d9933064b1 100644
--- a/packages/core/types/src/order/mutations.ts
+++ b/packages/core/types/src/order/mutations.ts
@@ -908,6 +908,11 @@ export interface CreateOrderChangeDTO {
*/
internal_note?: string | null
+ /**
+ * Whether to carry over promotions (apply promotions to outbound exchange items).
+ */
+ carry_over_promotions?: boolean | null
+
/**
* The user or customer that requested the order change.
*/
@@ -1016,6 +1021,11 @@ export interface UpdateOrderChangeDTO {
* Holds custom data in key-value pairs.
*/
metadata?: Record | null
+
+ /**
+ * Whether to carry over promotions to outbound exchange items.
+ */
+ carry_over_promotions?: boolean | null
}
/**
diff --git a/packages/medusa/src/api/admin/order-changes/[id]/route.ts b/packages/medusa/src/api/admin/order-changes/[id]/route.ts
new file mode 100644
index 0000000000..5e9c571685
--- /dev/null
+++ b/packages/medusa/src/api/admin/order-changes/[id]/route.ts
@@ -0,0 +1,38 @@
+import { updateOrderChangeWorkflow } from "@medusajs/core-flows"
+import { HttpTypes, RemoteQueryFunction } from "@medusajs/framework/types"
+import {
+ AuthenticatedMedusaRequest,
+ MedusaResponse,
+} from "@medusajs/framework/http"
+import { AdminPostOrderChangesReqSchemaType } from "../validators"
+import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
+
+export const POST = async (
+ req: AuthenticatedMedusaRequest,
+ res: MedusaResponse
+) => {
+ const { id } = req.params
+ const { carry_over_promotions } = req.validatedBody
+ const query = req.scope.resolve(
+ ContainerRegistrationKeys.QUERY
+ )
+
+ const workflow = updateOrderChangeWorkflow(req.scope)
+ await workflow.run({
+ input: {
+ id,
+ carry_over_promotions,
+ },
+ })
+
+ const result = await query.graph({
+ entity: "order_change",
+ filters: {
+ ...req.filterableFields,
+ id,
+ },
+ fields: req.queryConfig.fields,
+ })
+
+ res.status(200).json({ order_change: result.data[0] })
+}
diff --git a/packages/medusa/src/api/admin/order-changes/middlewares.ts b/packages/medusa/src/api/admin/order-changes/middlewares.ts
new file mode 100644
index 0000000000..8a73263fa6
--- /dev/null
+++ b/packages/medusa/src/api/admin/order-changes/middlewares.ts
@@ -0,0 +1,24 @@
+import { MiddlewareRoute } from "@medusajs/framework/http"
+import {
+ validateAndTransformBody,
+ validateAndTransformQuery,
+} from "@medusajs/framework"
+import * as QueryConfig from "./query-config"
+import {
+ AdminPostOrderChangesReqSchema,
+ AdminOrderChangeParams,
+} from "./validators"
+
+export const adminOrderChangesRoutesMiddlewares: MiddlewareRoute[] = [
+ {
+ method: ["POST"],
+ matcher: "/admin/order-changes/:id",
+ middlewares: [
+ validateAndTransformBody(AdminPostOrderChangesReqSchema),
+ validateAndTransformQuery(
+ AdminOrderChangeParams,
+ QueryConfig.retrieveTransformQueryConfig
+ ),
+ ],
+ },
+]
diff --git a/packages/medusa/src/api/admin/order-changes/query-config.ts b/packages/medusa/src/api/admin/order-changes/query-config.ts
new file mode 100644
index 0000000000..994878695b
--- /dev/null
+++ b/packages/medusa/src/api/admin/order-changes/query-config.ts
@@ -0,0 +1,32 @@
+export const defaultAdminRetrieveOrderChangeFields = [
+ "id",
+ "order_id",
+ "return_id",
+ "claim_id",
+ "exchange_id",
+ "version",
+ "change_type",
+ "*actions",
+ "description",
+ "status",
+ "internal_note",
+ "created_by",
+ "requested_by",
+ "requested_at",
+ "confirmed_by",
+ "confirmed_at",
+ "declined_by",
+ "declined_reason",
+ "metadata",
+ "declined_at",
+ "canceled_by",
+ "canceled_at",
+ "created_at",
+ "updated_at",
+ "carry_over_promotions",
+]
+
+export const retrieveTransformQueryConfig = {
+ defaults: defaultAdminRetrieveOrderChangeFields,
+ isList: false,
+}
diff --git a/packages/medusa/src/api/admin/order-changes/validators.ts b/packages/medusa/src/api/admin/order-changes/validators.ts
new file mode 100644
index 0000000000..814ec78829
--- /dev/null
+++ b/packages/medusa/src/api/admin/order-changes/validators.ts
@@ -0,0 +1,22 @@
+import { z } from "zod"
+
+import { createOperatorMap, createSelectParams } from "../../utils/validators"
+
+export const AdminOrderChangeParams = createSelectParams().merge(
+ z.object({
+ id: z.union([z.string(), z.array(z.string())]).optional(),
+ status: z.union([z.string(), z.array(z.string())]).optional(),
+ change_type: z.union([z.string(), z.array(z.string())]).optional(),
+ created_at: createOperatorMap().optional(),
+ updated_at: createOperatorMap().optional(),
+ deleted_at: createOperatorMap().optional(),
+ })
+)
+
+export const AdminPostOrderChangesReqSchema = z.object({
+ carry_over_promotions: z.boolean(),
+})
+
+export type AdminPostOrderChangesReqSchemaType = z.infer<
+ typeof AdminPostOrderChangesReqSchema
+>
diff --git a/packages/medusa/src/api/admin/orders/query-config.ts b/packages/medusa/src/api/admin/orders/query-config.ts
index 6fb4a358bb..4d3bac7dd5 100644
--- a/packages/medusa/src/api/admin/orders/query-config.ts
+++ b/packages/medusa/src/api/admin/orders/query-config.ts
@@ -17,7 +17,6 @@ export const defaultAdminRetrieveOrderFields = [
"total",
"subtotal",
"tax_total",
- "order_change",
"discount_total",
"discount_tax_total",
"original_total",
@@ -81,6 +80,7 @@ export const defaultAdminRetrieveOrderChangesFields = [
"canceled_at",
"created_at",
"updated_at",
+ "carry_over_promotions",
]
export const defaultAdminOrderItemsFields = [
diff --git a/packages/medusa/src/api/middlewares.ts b/packages/medusa/src/api/middlewares.ts
index ec8f03f310..7f0e85422c 100644
--- a/packages/medusa/src/api/middlewares.ts
+++ b/packages/medusa/src/api/middlewares.ts
@@ -14,6 +14,7 @@ import { adminFulfillmentsRoutesMiddlewares } from "./admin/fulfillments/middlew
import { adminInventoryRoutesMiddlewares } from "./admin/inventory-items/middlewares"
import { adminInviteRoutesMiddlewares } from "./admin/invites/middlewares"
import { adminNotificationRoutesMiddlewares } from "./admin/notifications/middlewares"
+import { adminOrderChangesRoutesMiddlewares } from "./admin/order-changes/middlewares"
import { adminOrderEditRoutesMiddlewares } from "./admin/order-edits/middlewares"
import { adminOrderRoutesMiddlewares } from "./admin/orders/middlewares"
import { adminPaymentCollectionsMiddlewares } from "./admin/payment-collections/middlewares"
@@ -129,6 +130,7 @@ export default defineMiddlewares([
...adminExchangeRoutesMiddlewares,
...adminProductVariantRoutesMiddlewares,
...adminTaxProviderRoutesMiddlewares,
+ ...adminOrderChangesRoutesMiddlewares,
...adminOrderEditRoutesMiddlewares,
...adminPaymentCollectionsMiddlewares,
...viewConfigurationRoutesMiddlewares,
diff --git a/packages/modules/order/src/migrations/.snapshot-medusa-order.json b/packages/modules/order/src/migrations/.snapshot-medusa-order.json
index 91b2e0c5ae..c174cf53b5 100644
--- a/packages/modules/order/src/migrations/.snapshot-medusa-order.json
+++ b/packages/modules/order/src/migrations/.snapshot-medusa-order.json
@@ -721,6 +721,15 @@
"length": 6,
"mappedType": "datetime"
},
+ "carry_over_promotions": {
+ "name": "carry_over_promotions",
+ "type": "boolean",
+ "unsigned": false,
+ "autoincrement": false,
+ "primary": false,
+ "nullable": true,
+ "mappedType": "boolean"
+ },
"order_id": {
"name": "order_id",
"type": "text",
@@ -1978,15 +1987,6 @@
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_order_item_order_id_version\" ON \"order_item\" (\"order_id\", \"version\") WHERE deleted_at IS NULL"
},
- {
- "keyName": "IDX_order_item_version",
- "columnNames": [],
- "composite": false,
- "constraint": false,
- "primary": false,
- "unique": false,
- "expression": "CREATE INDEX IF NOT EXISTS \"IDX_order_item_version\" ON \"order_item\" (version) WHERE deleted_at IS NULL"
- },
{
"keyName": "IDX_order_item_item_id",
"columnNames": [],
diff --git a/packages/modules/order/src/migrations/Migration20251125164002.ts b/packages/modules/order/src/migrations/Migration20251125164002.ts
new file mode 100644
index 0000000000..39360ba666
--- /dev/null
+++ b/packages/modules/order/src/migrations/Migration20251125164002.ts
@@ -0,0 +1,15 @@
+import { Migration } from "@mikro-orm/migrations"
+
+export class Migration20251125164002 extends Migration {
+ override async up(): Promise {
+ this.addSql(
+ `alter table if exists "order_change" add column if not exists "carry_over_promotions" boolean null;`
+ )
+ }
+
+ override async down(): Promise {
+ this.addSql(
+ `alter table if exists "order_change" drop column if exists "carry_over_promotions";`
+ )
+ }
+}
diff --git a/packages/modules/order/src/models/order-change.ts b/packages/modules/order/src/models/order-change.ts
index e833036f30..fda45c211e 100644
--- a/packages/modules/order/src/models/order-change.ts
+++ b/packages/modules/order/src/models/order-change.ts
@@ -27,6 +27,7 @@ const _OrderChange = model
declined_at: model.dateTime().nullable(),
canceled_by: model.text().nullable(),
canceled_at: model.dateTime().nullable(),
+ carry_over_promotions: model.boolean().nullable(),
order: model.belongsTo<() => typeof Order>(() => Order, {
mappedBy: "changes",
}),
diff --git a/packages/modules/order/src/services/order-module-service.ts b/packages/modules/order/src/services/order-module-service.ts
index 788f602c23..8e5fd92849 100644
--- a/packages/modules/order/src/services/order-module-service.ts
+++ b/packages/modules/order/src/services/order-module-service.ts
@@ -3336,6 +3336,7 @@ export default class OrderModuleService
"status",
"description",
"internal_note",
+ "carry_over_promotions",
],
relations: [] as string[],
order: {},