feat: carry over promotions toggle on exchanges (#14128)
* feat: carry over promotions toggle on exchanges * fix: inital flag value, return the flag on preview * fix: validation of allocation type * fix: revert client changes * fix: invert condition * feat: recompute adjustments when outbound item is updated * fix: condition again * fix: display more accurate inbound/outbound totals for exchanges * fix: make exchanges specs green * feat: more testing cases * wip: pr feedback * fix: use plural for the flag on Admin * fix: schema test, route refactor * feat: tooltip * feat: refactor to use update workflow * feat: display applied promotion per item on order details, show copy sku on hover * feat: refactor edits and exchanges to have common flag toggle flow * fix: delete empty file * fix: exchange_id param query
This commit is contained in:
10
.changeset/public-brooms-give.md
Normal file
10
.changeset/public-brooms-give.md
Normal file
@@ -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
|
||||
@@ -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,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -8,7 +8,7 @@ export const sdk = new Medusa({
|
||||
baseUrl: backendUrl,
|
||||
auth: {
|
||||
type: authType,
|
||||
jwtTokenStorageKey
|
||||
jwtTokenStorageKey,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -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 = ({
|
||||
<span className="txt-small text-ui-fg-subtle">
|
||||
{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 = ({
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CARRY OVER PROMOTION */}
|
||||
{hasPromotions && (
|
||||
<div className="bg-ui-bg-field mt-4 rounded-lg border py-2 pl-2 pr-4">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="carry_over_promotions"
|
||||
render={({ field: { onChange, value, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<div className="flex items-center">
|
||||
<Form.Control className="mr-4 self-start">
|
||||
<Switch
|
||||
dir="ltr"
|
||||
className="mt-[2px] rtl:rotate-180"
|
||||
checked={!!value}
|
||||
onCheckedChange={async (checked) => {
|
||||
onChange(checked)
|
||||
if (preview?.order_change?.id) {
|
||||
await updateOrderChange({
|
||||
carry_over_promotions: checked,
|
||||
})
|
||||
}
|
||||
}}
|
||||
{...field}
|
||||
/>
|
||||
</Form.Control>
|
||||
<div className="block">
|
||||
<Form.Label className="flex items-center gap-x-2">
|
||||
{t("orders.exchanges.carryOverPromotion")}
|
||||
<Form.Hint>
|
||||
<Tooltip
|
||||
content={t(
|
||||
"orders.exchanges.carryOverPromotionTooltip"
|
||||
)}
|
||||
>
|
||||
<InformationCircleSolid />
|
||||
</Tooltip>
|
||||
</Form.Hint>
|
||||
</Form.Label>
|
||||
<Form.Hint className="!mt-1">
|
||||
{t("orders.exchanges.carryOverPromotionHint")}
|
||||
</Form.Hint>
|
||||
</div>
|
||||
</div>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* SEND NOTIFICATION*/}
|
||||
<div className="bg-ui-bg-field mt-8 rounded-lg border py-2 pl-2 pr-4">
|
||||
<Form.Field
|
||||
|
||||
@@ -19,6 +19,7 @@ export const ExchangeCreateSchema = z.object({
|
||||
inbound_option_id: z.string().nullish(),
|
||||
outbound_option_id: z.string().nullish(),
|
||||
send_notification: z.boolean().optional(),
|
||||
carry_over_promotions: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export type CreateExchangeSchemaType = z.infer<typeof ExchangeCreateSchema>
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<div
|
||||
key={item.id}
|
||||
className="text-ui-fg-subtle grid grid-cols-2 items-center gap-x-4 px-6 py-4"
|
||||
>
|
||||
<div className="flex items-start gap-x-4">
|
||||
<Thumbnail src={item.thumbnail} />
|
||||
<div>
|
||||
<Text size="small" leading="compact" className="text-ui-fg-base">
|
||||
{item.title}
|
||||
</Text>
|
||||
<div className=" flex justify-between gap-x-2 ">
|
||||
<div className=" group flex items-start gap-x-4">
|
||||
<Thumbnail src={item.thumbnail} />
|
||||
<div>
|
||||
<Text size="small" leading="compact" className="text-ui-fg-base">
|
||||
{item.title}
|
||||
</Text>
|
||||
|
||||
{item.variant_sku && (
|
||||
<div className="flex items-center gap-x-1">
|
||||
<Text size="small">{item.variant_sku}</Text>
|
||||
<Copy content={item.variant_sku} className="text-ui-fg-muted" />
|
||||
</div>
|
||||
)}
|
||||
<Text size="small">
|
||||
{item.variant?.options?.map((o) => o.value).join(" · ")}
|
||||
</Text>
|
||||
{item.variant_sku && (
|
||||
<div className="flex items-center gap-x-1">
|
||||
<Text size="small">{item.variant_sku}</Text>
|
||||
<Copy
|
||||
content={item.variant_sku}
|
||||
className="text-ui-fg-muted hidden group-hover:block"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Text size="small">
|
||||
{item.variant?.options?.map((o) => o.value).join(" · ")}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
{appliedPromoCodes.length > 0 && (
|
||||
<Tooltip
|
||||
content={
|
||||
<span className="text-pretty">
|
||||
{appliedPromoCodes.map((code) => (
|
||||
<div key={code}>{code}</div>
|
||||
))}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<ReceiptPercent className="text-ui-fg-subtle flex-shrink self-center " />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 items-center gap-x-4">
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<IOrderModuleService>(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))
|
||||
}
|
||||
)
|
||||
@@ -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<ComputeAdjustmentsForPreviewWorkflowInput>) {
|
||||
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 })
|
||||
})
|
||||
}
|
||||
)
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<OnCarryPromotionsFlagSetWorkflowInput>
|
||||
): WorkflowResponse<void> {
|
||||
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)
|
||||
}
|
||||
)
|
||||
@@ -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<ComputeAdjustmentsForPreviewWorkflowInput>) {
|
||||
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" })
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -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],
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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"
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<UpdateOrderChangeDTO>
|
||||
): WorkflowResponse<OrderChangeDTO> {
|
||||
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]
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -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<HttpTypes.AdminOrderChangeResponse>(
|
||||
`/admin/order-changes/${id}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers,
|
||||
body,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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<string, unknown> | null
|
||||
|
||||
/**
|
||||
* Whether to carry over promotions to outbound exchange items.
|
||||
*/
|
||||
carry_over_promotions?: boolean | null
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
38
packages/medusa/src/api/admin/order-changes/[id]/route.ts
Normal file
38
packages/medusa/src/api/admin/order-changes/[id]/route.ts
Normal file
@@ -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<AdminPostOrderChangesReqSchemaType>,
|
||||
res: MedusaResponse<HttpTypes.AdminOrderChangeResponse>
|
||||
) => {
|
||||
const { id } = req.params
|
||||
const { carry_over_promotions } = req.validatedBody
|
||||
const query = req.scope.resolve<RemoteQueryFunction>(
|
||||
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] })
|
||||
}
|
||||
24
packages/medusa/src/api/admin/order-changes/middlewares.ts
Normal file
24
packages/medusa/src/api/admin/order-changes/middlewares.ts
Normal file
@@ -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
|
||||
),
|
||||
],
|
||||
},
|
||||
]
|
||||
32
packages/medusa/src/api/admin/order-changes/query-config.ts
Normal file
32
packages/medusa/src/api/admin/order-changes/query-config.ts
Normal file
@@ -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,
|
||||
}
|
||||
22
packages/medusa/src/api/admin/order-changes/validators.ts
Normal file
22
packages/medusa/src/api/admin/order-changes/validators.ts
Normal file
@@ -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
|
||||
>
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": [],
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Migration } from "@mikro-orm/migrations"
|
||||
|
||||
export class Migration20251125164002 extends Migration {
|
||||
override async up(): Promise<void> {
|
||||
this.addSql(
|
||||
`alter table if exists "order_change" add column if not exists "carry_over_promotions" boolean null;`
|
||||
)
|
||||
}
|
||||
|
||||
override async down(): Promise<void> {
|
||||
this.addSql(
|
||||
`alter table if exists "order_change" drop column if exists "carry_over_promotions";`
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
}),
|
||||
|
||||
@@ -3336,6 +3336,7 @@ export default class OrderModuleService
|
||||
"status",
|
||||
"description",
|
||||
"internal_note",
|
||||
"carry_over_promotions",
|
||||
],
|
||||
relations: [] as string[],
|
||||
order: {},
|
||||
|
||||
Reference in New Issue
Block a user