feat(order): cancel fulfillment (#7573)
This commit is contained in:
1 parent
4e04214612
commit
af0140d317
30 files changed
+745
-298
No files matched your search
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
cancelOrderFulfillmentWorkflow,
|
||||||
createOrderFulfillmentWorkflow,
|
createOrderFulfillmentWorkflow,
|
||||||
createShippingOptionsWorkflow,
|
createShippingOptionsWorkflow,
|
||||||
} from "@medusajs/core-flows"
|
} from "@medusajs/core-flows"
|
||||||
@@ -314,7 +315,7 @@ medusaIntegrationTestRunner({
|
|||||||
container = getContainer()
|
container = getContainer()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("Create order fulfillment workflow", () => {
|
describe("Order fulfillment workflow", () => {
|
||||||
let shippingOption: ShippingOptionDTO
|
let shippingOption: ShippingOptionDTO
|
||||||
let region: RegionDTO
|
let region: RegionDTO
|
||||||
let location: StockLocationDTO
|
let location: StockLocationDTO
|
||||||
@@ -335,9 +336,11 @@ medusaIntegrationTestRunner({
|
|||||||
orderService = container.resolve(ModuleRegistrationName.ORDER)
|
orderService = container.resolve(ModuleRegistrationName.ORDER)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should create a order fulfillment", async () => {
|
it("should create a order fulfillment and cancel it", async () => {
|
||||||
const order = await createOrderFixture({ container, product, location })
|
const order = await createOrderFixture({ container, product, location })
|
||||||
const createReturnOrderData: OrderWorkflow.CreateOrderFulfillmentWorkflowInput =
|
|
||||||
|
// Create a fulfillment
|
||||||
|
const createOrderFulfillmentData: OrderWorkflow.CreateOrderFulfillmentWorkflowInput =
|
||||||
{
|
{
|
||||||
order_id: order.id,
|
order_id: order.id,
|
||||||
created_by: "user_1",
|
created_by: "user_1",
|
||||||
@@ -352,7 +355,7 @@ medusaIntegrationTestRunner({
|
|||||||
}
|
}
|
||||||
|
|
||||||
await createOrderFulfillmentWorkflow(container).run({
|
await createOrderFulfillmentWorkflow(container).run({
|
||||||
input: createReturnOrderData,
|
input: createOrderFulfillmentData,
|
||||||
})
|
})
|
||||||
|
|
||||||
const remoteQuery = container.resolve(
|
const remoteQuery = container.resolve(
|
||||||
@@ -391,6 +394,48 @@ medusaIntegrationTestRunner({
|
|||||||
[location.id]
|
[location.id]
|
||||||
)
|
)
|
||||||
expect(stockAvailability).toEqual(1)
|
expect(stockAvailability).toEqual(1)
|
||||||
|
|
||||||
|
// Cancel the fulfillment
|
||||||
|
const cancelFulfillmentData: OrderWorkflow.CancelOrderFulfillmentWorkflowInput =
|
||||||
|
{
|
||||||
|
order_id: order.id,
|
||||||
|
fulfillment_id: orderFulfill.fulfillments[0].id,
|
||||||
|
no_notification: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
await cancelOrderFulfillmentWorkflow(container).run({
|
||||||
|
input: cancelFulfillmentData,
|
||||||
|
})
|
||||||
|
|
||||||
|
const remoteQueryObjectFulfill = remoteQueryObjectFromString({
|
||||||
|
entryPoint: "order",
|
||||||
|
variables: {
|
||||||
|
id: order.id,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
"*",
|
||||||
|
"items.*",
|
||||||
|
"shipping_methods.*",
|
||||||
|
"total",
|
||||||
|
"item_total",
|
||||||
|
"fulfillments.*",
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const [orderFulfillAfterCancelled] = await remoteQuery(
|
||||||
|
remoteQueryObjectFulfill
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(orderFulfillAfterCancelled.fulfillments).toHaveLength(1)
|
||||||
|
expect(
|
||||||
|
orderFulfillAfterCancelled.items[0].detail.fulfilled_quantity
|
||||||
|
).toEqual(0)
|
||||||
|
|
||||||
|
const stockAvailabilityAfterCancelled =
|
||||||
|
await inventoryModule.retrieveStockedQuantity(inventoryItem.id, [
|
||||||
|
location.id,
|
||||||
|
])
|
||||||
|
expect(stockAvailabilityAfterCancelled).toEqual(2)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { LinkDefinition, RemoteLink } from "@medusajs/modules-sdk"
|
import { LinkDefinition, RemoteLink } from "@medusajs/modules-sdk"
|
||||||
import { createStep, StepResponse } from "@medusajs/workflows-sdk"
|
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||||
|
|
||||||
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
||||||
|
|
||||||
type CreateRemoteLinksStepInput = LinkDefinition[]
|
type CreateRemoteLinksStepInput = LinkDefinition[]
|
||||||
|
|
||||||
export const createLinksStepId = "create-links"
|
export const createLinksStepId = "create-remote-links"
|
||||||
export const createLinkStep = createStep(
|
export const createRemoteLinkStep = createStep(
|
||||||
createLinksStepId,
|
createLinksStepId,
|
||||||
async (data: CreateRemoteLinksStepInput, { container }) => {
|
async (data: CreateRemoteLinksStepInput, { container }) => {
|
||||||
const link = container.resolve<RemoteLink>(
|
const link = container.resolve<RemoteLink>(
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||||
|
import { CancelOrderFulfillmentDTO, IOrderModuleService } from "@medusajs/types"
|
||||||
|
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||||
|
|
||||||
|
type CancelOrderFulfillmentStepInput = CancelOrderFulfillmentDTO
|
||||||
|
|
||||||
|
export const cancelOrderFulfillmentStepId = "cancel-order-fullfillment"
|
||||||
|
export const cancelOrderFulfillmentStep = createStep(
|
||||||
|
cancelOrderFulfillmentStepId,
|
||||||
|
async (data: CancelOrderFulfillmentStepInput, { container }) => {
|
||||||
|
const service = container.resolve<IOrderModuleService>(
|
||||||
|
ModuleRegistrationName.ORDER
|
||||||
|
)
|
||||||
|
|
||||||
|
await service.cancelFulfillment(data)
|
||||||
|
return new StepResponse(void 0, data.order_id)
|
||||||
|
},
|
||||||
|
async (orderId, { container }) => {
|
||||||
|
if (!orderId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const service = container.resolve<IOrderModuleService>(
|
||||||
|
ModuleRegistrationName.ORDER
|
||||||
|
)
|
||||||
|
|
||||||
|
await service.revertLastVersion(orderId)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import { Modules } from "@medusajs/modules-sdk"
|
||||||
|
import { FulfillmentDTO, OrderDTO, OrderWorkflow } from "@medusajs/types"
|
||||||
|
import { MedusaError } from "@medusajs/utils"
|
||||||
|
import {
|
||||||
|
WorkflowData,
|
||||||
|
createStep,
|
||||||
|
createWorkflow,
|
||||||
|
transform,
|
||||||
|
} from "@medusajs/workflows-sdk"
|
||||||
|
import { useRemoteQueryStep } from "../../common"
|
||||||
|
import { cancelFulfillmentWorkflow } from "../../fulfillment"
|
||||||
|
import { adjustInventoryLevelsStep } from "../../inventory"
|
||||||
|
import { cancelOrderFulfillmentStep } from "../steps/cancel-fulfillment"
|
||||||
|
import {
|
||||||
|
throwIfItemsDoesNotExistsInOrder,
|
||||||
|
throwIfOrderIsCancelled,
|
||||||
|
} from "../utils/order-validation"
|
||||||
|
|
||||||
|
const validateOrder = createStep(
|
||||||
|
"validate-order",
|
||||||
|
({
|
||||||
|
order,
|
||||||
|
input,
|
||||||
|
}: {
|
||||||
|
order: OrderDTO & { fulfillments: FulfillmentDTO[] }
|
||||||
|
input: OrderWorkflow.CancelOrderFulfillmentWorkflowInput
|
||||||
|
}) => {
|
||||||
|
throwIfOrderIsCancelled({ order })
|
||||||
|
|
||||||
|
const fulfillment = order.fulfillments.find(
|
||||||
|
(f) => f.id === input.fulfillment_id
|
||||||
|
)
|
||||||
|
if (!fulfillment) {
|
||||||
|
throw new Error(
|
||||||
|
`Fulfillment with id ${input.fulfillment_id} not found in the order`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fulfillment.shipped_at) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_ALLOWED,
|
||||||
|
`The fulfillment has already been shipped. Shipped fulfillments cannot be canceled`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
throwIfItemsDoesNotExistsInOrder({
|
||||||
|
order,
|
||||||
|
inputItems: fulfillment.items.map((i) => ({
|
||||||
|
id: i.line_item_id as string,
|
||||||
|
quantity: i.quantity,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function prepareCancelOrderFulfillmentData({
|
||||||
|
order,
|
||||||
|
fulfillment,
|
||||||
|
}: {
|
||||||
|
order: OrderDTO
|
||||||
|
fulfillment: FulfillmentDTO
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
order_id: order.id,
|
||||||
|
reference: Modules.FULFILLMENT,
|
||||||
|
reference_id: fulfillment.id,
|
||||||
|
items: fulfillment.items!.map((i) => {
|
||||||
|
return {
|
||||||
|
id: i.line_item_id as string,
|
||||||
|
quantity: i.quantity,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareInventoryUpdate({
|
||||||
|
fulfillment,
|
||||||
|
}: {
|
||||||
|
order: OrderDTO
|
||||||
|
fulfillment: FulfillmentDTO
|
||||||
|
}) {
|
||||||
|
const inventoryAdjustment: {
|
||||||
|
inventory_item_id: string
|
||||||
|
location_id: string
|
||||||
|
adjustment: number // TODO: BigNumberInput
|
||||||
|
}[] = []
|
||||||
|
|
||||||
|
for (const item of fulfillment.items) {
|
||||||
|
inventoryAdjustment.push({
|
||||||
|
inventory_item_id: item.inventory_item_id as string,
|
||||||
|
location_id: fulfillment.location_id,
|
||||||
|
adjustment: item.quantity,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
inventoryAdjustment,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cancelOrderFulfillmentWorkflowId = "cancel-order-fulfillment"
|
||||||
|
export const cancelOrderFulfillmentWorkflow = createWorkflow(
|
||||||
|
cancelOrderFulfillmentWorkflowId,
|
||||||
|
(
|
||||||
|
input: WorkflowData<OrderWorkflow.CancelOrderFulfillmentWorkflowInput>
|
||||||
|
): WorkflowData<void> => {
|
||||||
|
const order: OrderDTO & { fulfillments: FulfillmentDTO[] } =
|
||||||
|
useRemoteQueryStep({
|
||||||
|
entry_point: "orders",
|
||||||
|
fields: [
|
||||||
|
"id",
|
||||||
|
"status",
|
||||||
|
"items.*",
|
||||||
|
"fulfillments.*",
|
||||||
|
"fulfillments.items.*",
|
||||||
|
],
|
||||||
|
variables: { id: input.order_id },
|
||||||
|
list: false,
|
||||||
|
throw_if_key_not_found: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
validateOrder({ order, input })
|
||||||
|
|
||||||
|
const fulfillment = transform({ input, order }, ({ input, order }) => {
|
||||||
|
return order.fulfillments.find((f) => f.id === input.fulfillment_id)!
|
||||||
|
})
|
||||||
|
|
||||||
|
cancelFulfillmentWorkflow.runAsStep({
|
||||||
|
input: {
|
||||||
|
id: input.fulfillment_id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const cancelOrderFulfillmentData = transform(
|
||||||
|
{ order, fulfillment },
|
||||||
|
prepareCancelOrderFulfillmentData
|
||||||
|
)
|
||||||
|
|
||||||
|
cancelOrderFulfillmentStep(cancelOrderFulfillmentData)
|
||||||
|
|
||||||
|
const { inventoryAdjustment } = transform(
|
||||||
|
{ order, fulfillment },
|
||||||
|
prepareInventoryUpdate
|
||||||
|
)
|
||||||
|
|
||||||
|
adjustInventoryLevelsStep(inventoryAdjustment)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
FulfillmentWorkflow,
|
FulfillmentWorkflow,
|
||||||
OrderDTO,
|
OrderDTO,
|
||||||
OrderWorkflow,
|
OrderWorkflow,
|
||||||
|
ReservationItemDTO,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
import { MedusaError } from "@medusajs/utils"
|
import { MedusaError } from "@medusajs/utils"
|
||||||
import {
|
import {
|
||||||
@@ -13,7 +14,7 @@ import {
|
|||||||
parallelize,
|
parallelize,
|
||||||
transform,
|
transform,
|
||||||
} from "@medusajs/workflows-sdk"
|
} from "@medusajs/workflows-sdk"
|
||||||
import { createLinkStep, useRemoteQueryStep } from "../../common"
|
import { createRemoteLinkStep, useRemoteQueryStep } from "../../common"
|
||||||
import { createFulfillmentWorkflow } from "../../fulfillment"
|
import { createFulfillmentWorkflow } from "../../fulfillment"
|
||||||
import { adjustInventoryLevelsStep } from "../../inventory"
|
import { adjustInventoryLevelsStep } from "../../inventory"
|
||||||
import {
|
import {
|
||||||
@@ -70,6 +71,7 @@ function prepareFulfillmentData({
|
|||||||
order,
|
order,
|
||||||
input,
|
input,
|
||||||
shippingOption,
|
shippingOption,
|
||||||
|
reservations,
|
||||||
}: {
|
}: {
|
||||||
order: OrderDTO
|
order: OrderDTO
|
||||||
input: OrderWorkflow.CreateOrderFulfillmentWorkflowInput
|
input: OrderWorkflow.CreateOrderFulfillmentWorkflowInput
|
||||||
@@ -78,15 +80,21 @@ function prepareFulfillmentData({
|
|||||||
provider_id: string
|
provider_id: string
|
||||||
service_zone: { fulfillment_set: { location?: { id: string } } }
|
service_zone: { fulfillment_set: { location?: { id: string } } }
|
||||||
}
|
}
|
||||||
|
reservations: ReservationItemDTO[]
|
||||||
}) {
|
}) {
|
||||||
const inputItems = input.items
|
const inputItems = input.items
|
||||||
const orderItemsMap = new Map<string, Required<OrderDTO>["items"][0]>(
|
const orderItemsMap = new Map<string, Required<OrderDTO>["items"][0]>(
|
||||||
order.items!.map((i) => [i.id, i])
|
order.items!.map((i) => [i.id, i])
|
||||||
)
|
)
|
||||||
|
const reservationItemMap = new Map<string, ReservationItemDTO>(
|
||||||
|
reservations.map((r) => [r.line_item_id as string, r])
|
||||||
|
)
|
||||||
const fulfillmentItems = inputItems.map((i) => {
|
const fulfillmentItems = inputItems.map((i) => {
|
||||||
const orderItem = orderItemsMap.get(i.id)!
|
const orderItem = orderItemsMap.get(i.id)!
|
||||||
|
const reservation = reservationItemMap.get(i.id)!
|
||||||
return {
|
return {
|
||||||
line_item_id: i.id,
|
line_item_id: i.id,
|
||||||
|
inventory_item_id: reservation.inventory_item_id,
|
||||||
quantity: i.quantity,
|
quantity: i.quantity,
|
||||||
title: orderItem.variant_title ?? orderItem.title,
|
title: orderItem.variant_title ?? orderItem.title,
|
||||||
sku: orderItem.variant_sku || "",
|
sku: orderItem.variant_sku || "",
|
||||||
@@ -118,7 +126,7 @@ function prepareFulfillmentData({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepareInventoryReservations({ reservations, order, input }) {
|
function prepareInventoryUpdate({ reservations, order, input }) {
|
||||||
if (!reservations || !reservations.length) {
|
if (!reservations || !reservations.length) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`No stock reservation found for items ${input.items.map((i) => i.id)}`
|
`No stock reservation found for items ${input.items.map((i) => i.id)}`
|
||||||
@@ -215,8 +223,27 @@ export const createOrderFulfillmentWorkflow = createWorkflow(
|
|||||||
throw_if_key_not_found: true,
|
throw_if_key_not_found: true,
|
||||||
}).config({ name: "get-shipping-option" })
|
}).config({ name: "get-shipping-option" })
|
||||||
|
|
||||||
|
const lineItemIds = transform({ order }, ({ order }) => {
|
||||||
|
return order.items?.map((i) => i.id)
|
||||||
|
})
|
||||||
|
const reservations = useRemoteQueryStep({
|
||||||
|
entry_point: "reservations",
|
||||||
|
fields: [
|
||||||
|
"id",
|
||||||
|
"line_item_id",
|
||||||
|
"quantity",
|
||||||
|
"inventory_item_id",
|
||||||
|
"location_id",
|
||||||
|
],
|
||||||
|
variables: {
|
||||||
|
filter: {
|
||||||
|
line_item_id: lineItemIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).config({ name: "get-reservations" })
|
||||||
|
|
||||||
const fulfillmentData = transform(
|
const fulfillmentData = transform(
|
||||||
{ order, input, shippingOption },
|
{ order, input, shippingOption, reservations },
|
||||||
prepareFulfillmentData
|
prepareFulfillmentData
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -240,30 +267,11 @@ export const createOrderFulfillmentWorkflow = createWorkflow(
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
createLinkStep(link)
|
createRemoteLinkStep(link)
|
||||||
|
|
||||||
const lineItemIds = transform({ order }, ({ order }) => {
|
|
||||||
return order.items?.map((i) => i.id)
|
|
||||||
})
|
|
||||||
const reservations = useRemoteQueryStep({
|
|
||||||
entry_point: "reservations",
|
|
||||||
fields: [
|
|
||||||
"id",
|
|
||||||
"line_item_id",
|
|
||||||
"quantity",
|
|
||||||
"inventory_item_id",
|
|
||||||
"location_id",
|
|
||||||
],
|
|
||||||
variables: {
|
|
||||||
filter: {
|
|
||||||
line_item_id: lineItemIds,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}).config({ name: "get-reservations" })
|
|
||||||
|
|
||||||
const { toDelete, toUpdate, inventoryAdjustment } = transform(
|
const { toDelete, toUpdate, inventoryAdjustment } = transform(
|
||||||
{ order, reservations, input },
|
{ order, reservations, input },
|
||||||
prepareInventoryReservations
|
prepareInventoryUpdate
|
||||||
)
|
)
|
||||||
|
|
||||||
parallelize(
|
parallelize(
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
createWorkflow,
|
createWorkflow,
|
||||||
transform,
|
transform,
|
||||||
} from "@medusajs/workflows-sdk"
|
} from "@medusajs/workflows-sdk"
|
||||||
import { createLinkStep, useRemoteQueryStep } from "../../common"
|
import { createRemoteLinkStep, useRemoteQueryStep } from "../../common"
|
||||||
import { createReturnFulfillmentWorkflow } from "../../fulfillment"
|
import { createReturnFulfillmentWorkflow } from "../../fulfillment"
|
||||||
import { updateOrderTaxLinesStep } from "../steps"
|
import { updateOrderTaxLinesStep } from "../steps"
|
||||||
import { createReturnStep } from "../steps/create-return"
|
import { createReturnStep } from "../steps/create-return"
|
||||||
@@ -326,6 +326,6 @@ export const createReturnOrderWorkflow = createWorkflow(
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
createLinkStep(link)
|
createRemoteLinkStep(link)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from "./archive-orders"
|
export * from "./archive-orders"
|
||||||
|
export * from "./cancel-order-fulfillment"
|
||||||
export * from "./complete-orders"
|
export * from "./complete-orders"
|
||||||
export * from "./create-fulfillment"
|
export * from "./create-fulfillment"
|
||||||
export * from "./create-orders"
|
export * from "./create-orders"
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { updateProductsStep } from "../steps/update-products"
|
import { updateProductsStep } from "../steps/update-products"
|
||||||
|
|
||||||
import {
|
|
||||||
dismissRemoteLinkStep,
|
|
||||||
createLinkStep,
|
|
||||||
useRemoteQueryStep,
|
|
||||||
} from "../../common"
|
|
||||||
import { arrayDifference } from "@medusajs/utils"
|
|
||||||
import { Modules } from "@medusajs/modules-sdk"
|
import { Modules } from "@medusajs/modules-sdk"
|
||||||
import { ProductTypes } from "@medusajs/types"
|
import { ProductTypes } from "@medusajs/types"
|
||||||
|
import { arrayDifference } from "@medusajs/utils"
|
||||||
import {
|
import {
|
||||||
WorkflowData,
|
WorkflowData,
|
||||||
createWorkflow,
|
createWorkflow,
|
||||||
transform,
|
transform,
|
||||||
} from "@medusajs/workflows-sdk"
|
} from "@medusajs/workflows-sdk"
|
||||||
|
import {
|
||||||
|
createRemoteLinkStep,
|
||||||
|
dismissRemoteLinkStep,
|
||||||
|
useRemoteQueryStep,
|
||||||
|
} from "../../common"
|
||||||
|
|
||||||
type UpdateProductsStepInputSelector = {
|
type UpdateProductsStepInputSelector = {
|
||||||
selector: ProductTypes.FilterableProductProps
|
selector: ProductTypes.FilterableProductProps
|
||||||
@@ -161,7 +161,7 @@ export const updateProductsWorkflow = createWorkflow(
|
|||||||
prepareSalesChannelLinks
|
prepareSalesChannelLinks
|
||||||
)
|
)
|
||||||
|
|
||||||
createLinkStep(salesChannelLinks)
|
createRemoteLinkStep(salesChannelLinks)
|
||||||
|
|
||||||
return updatedProducts
|
return updatedProducts
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export type ChangeActionType =
|
|||||||
| "CANCEL"
|
| "CANCEL"
|
||||||
| "CANCEL_RETURN"
|
| "CANCEL_RETURN"
|
||||||
| "FULFILL_ITEM"
|
| "FULFILL_ITEM"
|
||||||
|
| "CANCEL_ITEM_FULFILLMENT"
|
||||||
| "ITEM_ADD"
|
| "ITEM_ADD"
|
||||||
| "ITEM_REMOVE"
|
| "ITEM_REMOVE"
|
||||||
| "RECEIVE_DAMAGED_RETURN_ITEM"
|
| "RECEIVE_DAMAGED_RETURN_ITEM"
|
||||||
|
|||||||
@@ -368,7 +368,7 @@ export interface UpdateOrderItemWithSelectorDTO {
|
|||||||
|
|
||||||
/** ORDER bundled action flows */
|
/** ORDER bundled action flows */
|
||||||
|
|
||||||
export interface RegisterOrderFulfillmentDTO {
|
interface BaseOrderBundledActionsDTO {
|
||||||
order_id: string
|
order_id: string
|
||||||
description?: string
|
description?: string
|
||||||
internal_note?: string
|
internal_note?: string
|
||||||
@@ -384,54 +384,18 @@ export interface RegisterOrderFulfillmentDTO {
|
|||||||
metadata?: Record<string, unknown> | null
|
metadata?: Record<string, unknown> | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RegisterOrderShipmentDTO {
|
export interface RegisterOrderFulfillmentDTO
|
||||||
order_id: string
|
extends BaseOrderBundledActionsDTO {}
|
||||||
description?: string
|
|
||||||
internal_note?: string
|
|
||||||
reference?: string
|
|
||||||
reference_id?: string
|
|
||||||
created_by?: string
|
|
||||||
items: {
|
|
||||||
id: string
|
|
||||||
quantity: BigNumberInput
|
|
||||||
internal_note?: string
|
|
||||||
metadata?: Record<string, unknown> | null
|
|
||||||
}[]
|
|
||||||
metadata?: Record<string, unknown> | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateOrderReturnDTO {
|
export interface CancelOrderFulfillmentDTO extends BaseOrderBundledActionsDTO {}
|
||||||
order_id: string
|
|
||||||
description?: string
|
export interface RegisterOrderShipmentDTO extends BaseOrderBundledActionsDTO {}
|
||||||
reference?: string
|
|
||||||
reference_id?: string
|
export interface CreateOrderReturnDTO extends BaseOrderBundledActionsDTO {
|
||||||
internal_note?: string
|
|
||||||
created_by?: string
|
|
||||||
shipping_method: Omit<CreateOrderShippingMethodDTO, "order_id"> | string
|
shipping_method: Omit<CreateOrderShippingMethodDTO, "order_id"> | string
|
||||||
items: {
|
|
||||||
id: string
|
|
||||||
quantity: BigNumberInput
|
|
||||||
internal_note?: string
|
|
||||||
metadata?: Record<string, unknown> | null
|
|
||||||
}[]
|
|
||||||
metadata?: Record<string, unknown> | null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReceiveOrderReturnDTO {
|
export interface ReceiveOrderReturnDTO extends BaseOrderBundledActionsDTO {}
|
||||||
order_id: string
|
|
||||||
description?: string
|
|
||||||
internal_note?: string
|
|
||||||
reference?: string
|
|
||||||
reference_id?: string
|
|
||||||
created_by?: string
|
|
||||||
items: {
|
|
||||||
id: string
|
|
||||||
quantity: BigNumberInput
|
|
||||||
internal_note?: string
|
|
||||||
metadata?: Record<string, unknown> | null
|
|
||||||
}[]
|
|
||||||
metadata?: Record<string, unknown> | null
|
|
||||||
}
|
|
||||||
|
|
||||||
/** ORDER bundled action flows */
|
/** ORDER bundled action flows */
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
} from "./common"
|
} from "./common"
|
||||||
import {
|
import {
|
||||||
CancelOrderChangeDTO,
|
CancelOrderChangeDTO,
|
||||||
|
CancelOrderFulfillmentDTO,
|
||||||
ConfirmOrderChangeDTO,
|
ConfirmOrderChangeDTO,
|
||||||
CreateOrderAddressDTO,
|
CreateOrderAddressDTO,
|
||||||
CreateOrderAdjustmentDTO,
|
CreateOrderAdjustmentDTO,
|
||||||
@@ -1496,6 +1497,11 @@ export interface IOrderModuleService extends IModuleService {
|
|||||||
sharedContext?: Context
|
sharedContext?: Context
|
||||||
): Promise<void>
|
): Promise<void>
|
||||||
|
|
||||||
|
cancelFulfillment(
|
||||||
|
data: CancelOrderFulfillmentDTO,
|
||||||
|
sharedContext?: Context
|
||||||
|
): Promise<void>
|
||||||
|
|
||||||
registerShipment(
|
registerShipment(
|
||||||
data: RegisterOrderShipmentDTO,
|
data: RegisterOrderShipmentDTO,
|
||||||
sharedContext?: Context
|
sharedContext?: Context
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export interface CancelOrderFulfillmentWorkflowInput {
|
||||||
|
order_id: string
|
||||||
|
fulfillment_id: string
|
||||||
|
no_notification?: boolean
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export * from "./cancel-fulfillment"
|
||||||
export * from "./create-fulfillment"
|
export * from "./create-fulfillment"
|
||||||
export * from "./create-return-order"
|
export * from "./create-return-order"
|
||||||
export * from "./create-shipment"
|
export * from "./create-shipment"
|
||||||
+16
-2
@@ -1,3 +1,4 @@
|
|||||||
|
import { cancelOrderFulfillmentWorkflow } from "@medusajs/core-flows"
|
||||||
import {
|
import {
|
||||||
ContainerRegistrationKeys,
|
ContainerRegistrationKeys,
|
||||||
remoteQueryObjectFromString,
|
remoteQueryObjectFromString,
|
||||||
@@ -6,16 +7,29 @@ import {
|
|||||||
AuthenticatedMedusaRequest,
|
AuthenticatedMedusaRequest,
|
||||||
MedusaResponse,
|
MedusaResponse,
|
||||||
} from "../../../../../../../types/routing"
|
} from "../../../../../../../types/routing"
|
||||||
|
import { AdminOrderCancelFulfillmentType } from "../../../../validators"
|
||||||
|
|
||||||
export const POST = async (
|
export const POST = async (
|
||||||
req: AuthenticatedMedusaRequest,
|
req: AuthenticatedMedusaRequest<AdminOrderCancelFulfillmentType>,
|
||||||
res: MedusaResponse
|
res: MedusaResponse
|
||||||
) => {
|
) => {
|
||||||
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
||||||
|
|
||||||
const variables = { id: req.params.id }
|
const variables = { id: req.params.id }
|
||||||
|
|
||||||
// TODO: Workflow to cancel fulfillment + adjust inventory
|
const input = {
|
||||||
|
...req.validatedBody,
|
||||||
|
order_id: req.params.id,
|
||||||
|
}
|
||||||
|
|
||||||
|
const { errors } = await cancelOrderFulfillmentWorkflow(req.scope).run({
|
||||||
|
input,
|
||||||
|
throwOnError: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (Array.isArray(errors) && errors[0]) {
|
||||||
|
throw errors[0].error
|
||||||
|
}
|
||||||
|
|
||||||
const queryObject = remoteQueryObjectFromString({
|
const queryObject = remoteQueryObjectFromString({
|
||||||
entryPoint: "order",
|
entryPoint: "order",
|
||||||
|
|||||||
@@ -79,3 +79,12 @@ export const AdminOrderCreateShipment = z.object({
|
|||||||
export type AdminOrderCreateShipmentType = z.infer<
|
export type AdminOrderCreateShipmentType = z.infer<
|
||||||
typeof AdminOrderCreateShipment
|
typeof AdminOrderCreateShipment
|
||||||
>
|
>
|
||||||
|
|
||||||
|
export const AdminOrderCancelFulfillment = z.object({
|
||||||
|
fulfillment_id: z.string(),
|
||||||
|
no_notification: z.boolean().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type AdminOrderCancelFulfillmentType = z.infer<
|
||||||
|
typeof AdminOrderCancelFulfillment
|
||||||
|
>
|
||||||
@@ -418,208 +418,6 @@ moduleIntegrationTestRunner({
|
|||||||
)
|
)
|
||||||
expect(orders4.length).toEqual(0)
|
expect(orders4.length).toEqual(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should create an order, fulfill, ship and return the items", async function () {
|
|
||||||
const createdOrder = await service.create(input)
|
|
||||||
|
|
||||||
// Fullfilment
|
|
||||||
await service.registerFulfillment({
|
|
||||||
order_id: createdOrder.id,
|
|
||||||
items: createdOrder.items!.map((item) => {
|
|
||||||
return {
|
|
||||||
id: item.id,
|
|
||||||
quantity: item.quantity,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
let getOrder = await service.retrieve(createdOrder.id, {
|
|
||||||
select: [
|
|
||||||
"id",
|
|
||||||
"version",
|
|
||||||
"items.id",
|
|
||||||
"items.quantity",
|
|
||||||
"items.detail.id",
|
|
||||||
"items.detail.version",
|
|
||||||
"items.detail.quantity",
|
|
||||||
"items.detail.shipped_quantity",
|
|
||||||
"items.detail.fulfilled_quantity",
|
|
||||||
],
|
|
||||||
relations: ["items", "items.detail"],
|
|
||||||
})
|
|
||||||
|
|
||||||
let serializedOrder = JSON.parse(JSON.stringify(getOrder))
|
|
||||||
|
|
||||||
expect(serializedOrder).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
version: 2,
|
|
||||||
items: [
|
|
||||||
expect.objectContaining({
|
|
||||||
quantity: 1,
|
|
||||||
detail: expect.objectContaining({
|
|
||||||
version: 2,
|
|
||||||
quantity: 1,
|
|
||||||
fulfilled_quantity: 1,
|
|
||||||
shipped_quantity: 0,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
quantity: 2,
|
|
||||||
detail: expect.objectContaining({
|
|
||||||
version: 2,
|
|
||||||
quantity: 2,
|
|
||||||
fulfilled_quantity: 2,
|
|
||||||
shipped_quantity: 0,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
quantity: 1,
|
|
||||||
detail: expect.objectContaining({
|
|
||||||
version: 2,
|
|
||||||
quantity: 1,
|
|
||||||
fulfilled_quantity: 1,
|
|
||||||
shipped_quantity: 0,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
// Shipment
|
|
||||||
await service.registerShipment({
|
|
||||||
order_id: createdOrder.id,
|
|
||||||
reference: Modules.FULFILLMENT,
|
|
||||||
items: createdOrder.items!.map((item) => {
|
|
||||||
return {
|
|
||||||
id: item.id,
|
|
||||||
quantity: item.quantity,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
getOrder = await service.retrieve(createdOrder.id, {
|
|
||||||
select: [
|
|
||||||
"id",
|
|
||||||
"version",
|
|
||||||
"items.id",
|
|
||||||
"items.quantity",
|
|
||||||
"items.detail.id",
|
|
||||||
"items.detail.version",
|
|
||||||
"items.detail.quantity",
|
|
||||||
"items.detail.shipped_quantity",
|
|
||||||
"items.detail.fulfilled_quantity",
|
|
||||||
],
|
|
||||||
relations: ["items", "items.detail"],
|
|
||||||
})
|
|
||||||
|
|
||||||
serializedOrder = JSON.parse(JSON.stringify(getOrder))
|
|
||||||
|
|
||||||
expect(serializedOrder).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
version: 3,
|
|
||||||
items: [
|
|
||||||
expect.objectContaining({
|
|
||||||
quantity: 1,
|
|
||||||
detail: expect.objectContaining({
|
|
||||||
version: 3,
|
|
||||||
quantity: 1,
|
|
||||||
fulfilled_quantity: 1,
|
|
||||||
shipped_quantity: 1,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
quantity: 2,
|
|
||||||
detail: expect.objectContaining({
|
|
||||||
version: 3,
|
|
||||||
quantity: 2,
|
|
||||||
fulfilled_quantity: 2,
|
|
||||||
shipped_quantity: 2,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
quantity: 1,
|
|
||||||
detail: expect.objectContaining({
|
|
||||||
version: 3,
|
|
||||||
quantity: 1,
|
|
||||||
fulfilled_quantity: 1,
|
|
||||||
shipped_quantity: 1,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
// Return
|
|
||||||
await service.createReturn({
|
|
||||||
order_id: createdOrder.id,
|
|
||||||
reference: Modules.FULFILLMENT,
|
|
||||||
description: "Return all the items",
|
|
||||||
internal_note: "user wants to return all items",
|
|
||||||
shipping_method: createdOrder.shipping_methods![0].id,
|
|
||||||
items: createdOrder.items!.map((item) => {
|
|
||||||
return {
|
|
||||||
id: item.id,
|
|
||||||
quantity: item.quantity,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
getOrder = await service.retrieve(createdOrder.id, {
|
|
||||||
select: [
|
|
||||||
"id",
|
|
||||||
"version",
|
|
||||||
"items.id",
|
|
||||||
"items.quantity",
|
|
||||||
"items.detail.id",
|
|
||||||
"items.detail.version",
|
|
||||||
"items.detail.quantity",
|
|
||||||
"items.detail.shipped_quantity",
|
|
||||||
"items.detail.fulfilled_quantity",
|
|
||||||
"items.detail.return_requested_quantity",
|
|
||||||
],
|
|
||||||
relations: ["items", "items.detail"],
|
|
||||||
})
|
|
||||||
|
|
||||||
serializedOrder = JSON.parse(JSON.stringify(getOrder))
|
|
||||||
|
|
||||||
expect(serializedOrder).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
version: 4,
|
|
||||||
items: [
|
|
||||||
expect.objectContaining({
|
|
||||||
quantity: 1,
|
|
||||||
detail: expect.objectContaining({
|
|
||||||
version: 4,
|
|
||||||
quantity: 1,
|
|
||||||
fulfilled_quantity: 1,
|
|
||||||
shipped_quantity: 1,
|
|
||||||
return_requested_quantity: 1,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
quantity: 2,
|
|
||||||
detail: expect.objectContaining({
|
|
||||||
version: 4,
|
|
||||||
quantity: 2,
|
|
||||||
fulfilled_quantity: 2,
|
|
||||||
shipped_quantity: 2,
|
|
||||||
return_requested_quantity: 2,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
quantity: 1,
|
|
||||||
detail: expect.objectContaining({
|
|
||||||
version: 4,
|
|
||||||
quantity: 1,
|
|
||||||
fulfilled_quantity: 1,
|
|
||||||
shipped_quantity: 1,
|
|
||||||
return_requested_quantity: 1,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -0,0 +1,308 @@
|
|||||||
|
import { Modules } from "@medusajs/modules-sdk"
|
||||||
|
import { CreateOrderDTO, IOrderModuleService } from "@medusajs/types"
|
||||||
|
import { SuiteOptions, moduleIntegrationTestRunner } from "medusa-test-utils"
|
||||||
|
|
||||||
|
jest.setTimeout(100000)
|
||||||
|
|
||||||
|
moduleIntegrationTestRunner({
|
||||||
|
moduleName: Modules.ORDER,
|
||||||
|
testSuite: ({ service }: SuiteOptions<IOrderModuleService>) => {
|
||||||
|
describe("Order Module Service - Return flows", () => {
|
||||||
|
const input = {
|
||||||
|
email: "foo@bar.com",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: "Item 1",
|
||||||
|
subtitle: "Subtitle 1",
|
||||||
|
thumbnail: "thumbnail1.jpg",
|
||||||
|
quantity: 1,
|
||||||
|
product_id: "product1",
|
||||||
|
product_title: "Product 1",
|
||||||
|
product_description: "Description 1",
|
||||||
|
product_subtitle: "Product Subtitle 1",
|
||||||
|
product_type: "Type 1",
|
||||||
|
product_collection: "Collection 1",
|
||||||
|
product_handle: "handle1",
|
||||||
|
variant_id: "variant1",
|
||||||
|
variant_sku: "SKU1",
|
||||||
|
variant_barcode: "Barcode1",
|
||||||
|
variant_title: "Variant 1",
|
||||||
|
variant_option_values: {
|
||||||
|
color: "Red",
|
||||||
|
size: "Large",
|
||||||
|
},
|
||||||
|
requires_shipping: true,
|
||||||
|
is_discountable: true,
|
||||||
|
is_tax_inclusive: true,
|
||||||
|
compare_at_unit_price: 10,
|
||||||
|
unit_price: 8,
|
||||||
|
tax_lines: [
|
||||||
|
{
|
||||||
|
description: "Tax 1",
|
||||||
|
tax_rate_id: "tax_usa",
|
||||||
|
code: "code",
|
||||||
|
rate: 0.1,
|
||||||
|
provider_id: "taxify_master",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
adjustments: [
|
||||||
|
{
|
||||||
|
code: "VIP_10",
|
||||||
|
amount: 10,
|
||||||
|
description: "VIP discount",
|
||||||
|
promotion_id: "prom_123",
|
||||||
|
provider_id: "coupon_kings",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Item 2",
|
||||||
|
quantity: 2,
|
||||||
|
unit_price: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Item 3",
|
||||||
|
quantity: 1,
|
||||||
|
unit_price: 30,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sales_channel_id: "test",
|
||||||
|
shipping_address: {
|
||||||
|
first_name: "Test",
|
||||||
|
last_name: "Test",
|
||||||
|
address_1: "Test",
|
||||||
|
city: "Test",
|
||||||
|
country_code: "US",
|
||||||
|
postal_code: "12345",
|
||||||
|
phone: "12345",
|
||||||
|
},
|
||||||
|
billing_address: {
|
||||||
|
first_name: "Test",
|
||||||
|
last_name: "Test",
|
||||||
|
address_1: "Test",
|
||||||
|
city: "Test",
|
||||||
|
country_code: "US",
|
||||||
|
postal_code: "12345",
|
||||||
|
},
|
||||||
|
shipping_methods: [
|
||||||
|
{
|
||||||
|
name: "Test shipping method",
|
||||||
|
amount: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
transactions: [
|
||||||
|
{
|
||||||
|
amount: 58,
|
||||||
|
currency_code: "USD",
|
||||||
|
reference: "payment",
|
||||||
|
reference_id: "pay_123",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
currency_code: "usd",
|
||||||
|
customer_id: "joe",
|
||||||
|
} as CreateOrderDTO
|
||||||
|
|
||||||
|
it("should create an order, fulfill, ship and return the items and cancel some item return", async function () {
|
||||||
|
const createdOrder = await service.create(input)
|
||||||
|
|
||||||
|
// Fullfilment
|
||||||
|
await service.registerFulfillment({
|
||||||
|
order_id: createdOrder.id,
|
||||||
|
items: createdOrder.items!.map((item) => {
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
quantity: item.quantity,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
let getOrder = await service.retrieve(createdOrder.id, {
|
||||||
|
select: [
|
||||||
|
"id",
|
||||||
|
"version",
|
||||||
|
"items.id",
|
||||||
|
"items.quantity",
|
||||||
|
"items.detail.id",
|
||||||
|
"items.detail.version",
|
||||||
|
"items.detail.quantity",
|
||||||
|
"items.detail.shipped_quantity",
|
||||||
|
"items.detail.fulfilled_quantity",
|
||||||
|
],
|
||||||
|
relations: ["items", "items.detail"],
|
||||||
|
})
|
||||||
|
|
||||||
|
let serializedOrder = JSON.parse(JSON.stringify(getOrder))
|
||||||
|
|
||||||
|
expect(serializedOrder).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
version: 2,
|
||||||
|
items: [
|
||||||
|
expect.objectContaining({
|
||||||
|
quantity: 1,
|
||||||
|
detail: expect.objectContaining({
|
||||||
|
version: 2,
|
||||||
|
quantity: 1,
|
||||||
|
fulfilled_quantity: 1,
|
||||||
|
shipped_quantity: 0,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
quantity: 2,
|
||||||
|
detail: expect.objectContaining({
|
||||||
|
version: 2,
|
||||||
|
quantity: 2,
|
||||||
|
fulfilled_quantity: 2,
|
||||||
|
shipped_quantity: 0,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
quantity: 1,
|
||||||
|
detail: expect.objectContaining({
|
||||||
|
version: 2,
|
||||||
|
quantity: 1,
|
||||||
|
fulfilled_quantity: 1,
|
||||||
|
shipped_quantity: 0,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// Shipment
|
||||||
|
await service.registerShipment({
|
||||||
|
order_id: createdOrder.id,
|
||||||
|
reference: Modules.FULFILLMENT,
|
||||||
|
items: createdOrder.items!.map((item) => {
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
quantity: item.quantity,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
getOrder = await service.retrieve(createdOrder.id, {
|
||||||
|
select: [
|
||||||
|
"id",
|
||||||
|
"version",
|
||||||
|
"items.id",
|
||||||
|
"items.quantity",
|
||||||
|
"items.detail.id",
|
||||||
|
"items.detail.version",
|
||||||
|
"items.detail.quantity",
|
||||||
|
"items.detail.shipped_quantity",
|
||||||
|
"items.detail.fulfilled_quantity",
|
||||||
|
],
|
||||||
|
relations: ["items", "items.detail"],
|
||||||
|
})
|
||||||
|
|
||||||
|
serializedOrder = JSON.parse(JSON.stringify(getOrder))
|
||||||
|
|
||||||
|
expect(serializedOrder).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
version: 3,
|
||||||
|
items: [
|
||||||
|
expect.objectContaining({
|
||||||
|
quantity: 1,
|
||||||
|
detail: expect.objectContaining({
|
||||||
|
version: 3,
|
||||||
|
quantity: 1,
|
||||||
|
fulfilled_quantity: 1,
|
||||||
|
shipped_quantity: 1,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
quantity: 2,
|
||||||
|
detail: expect.objectContaining({
|
||||||
|
version: 3,
|
||||||
|
quantity: 2,
|
||||||
|
fulfilled_quantity: 2,
|
||||||
|
shipped_quantity: 2,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
quantity: 1,
|
||||||
|
detail: expect.objectContaining({
|
||||||
|
version: 3,
|
||||||
|
quantity: 1,
|
||||||
|
fulfilled_quantity: 1,
|
||||||
|
shipped_quantity: 1,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// Return
|
||||||
|
await service.createReturn({
|
||||||
|
order_id: createdOrder.id,
|
||||||
|
reference: Modules.FULFILLMENT,
|
||||||
|
description: "Return all the items",
|
||||||
|
internal_note: "user wants to return all items",
|
||||||
|
shipping_method: createdOrder.shipping_methods![0].id,
|
||||||
|
items: createdOrder.items!.map((item) => {
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
quantity: item.quantity,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
getOrder = await service.retrieve(createdOrder.id, {
|
||||||
|
select: [
|
||||||
|
"id",
|
||||||
|
"version",
|
||||||
|
"items.id",
|
||||||
|
"items.quantity",
|
||||||
|
"items.detail.id",
|
||||||
|
"items.detail.version",
|
||||||
|
"items.detail.quantity",
|
||||||
|
"items.detail.shipped_quantity",
|
||||||
|
"items.detail.fulfilled_quantity",
|
||||||
|
"items.detail.return_requested_quantity",
|
||||||
|
],
|
||||||
|
relations: ["items", "items.detail"],
|
||||||
|
})
|
||||||
|
|
||||||
|
serializedOrder = JSON.parse(JSON.stringify(getOrder))
|
||||||
|
|
||||||
|
expect(serializedOrder).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
version: 4,
|
||||||
|
items: [
|
||||||
|
expect.objectContaining({
|
||||||
|
quantity: 1,
|
||||||
|
detail: expect.objectContaining({
|
||||||
|
version: 4,
|
||||||
|
quantity: 1,
|
||||||
|
fulfilled_quantity: 1,
|
||||||
|
shipped_quantity: 1,
|
||||||
|
return_requested_quantity: 1,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
quantity: 2,
|
||||||
|
detail: expect.objectContaining({
|
||||||
|
version: 4,
|
||||||
|
quantity: 2,
|
||||||
|
fulfilled_quantity: 2,
|
||||||
|
shipped_quantity: 2,
|
||||||
|
return_requested_quantity: 2,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
quantity: 1,
|
||||||
|
detail: expect.objectContaining({
|
||||||
|
version: 4,
|
||||||
|
quantity: 1,
|
||||||
|
fulfilled_quantity: 1,
|
||||||
|
shipped_quantity: 1,
|
||||||
|
return_requested_quantity: 1,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -95,7 +95,7 @@ describe("Order Return - Actions", function () {
|
|||||||
order: originalOrder,
|
order: originalOrder,
|
||||||
actions,
|
actions,
|
||||||
})
|
})
|
||||||
}).toThrow(`Reference ID "333" not found.`)
|
}).toThrow(`Item ID "333" not found.`)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should validate return received", function () {
|
it("should validate return received", function () {
|
||||||
|
|||||||
@@ -2142,6 +2142,40 @@ export default class OrderModuleService<
|
|||||||
await this.confirmOrderChange(change[0].id, sharedContext)
|
await this.confirmOrderChange(change[0].id, sharedContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@InjectTransactionManager("baseRepository_")
|
||||||
|
async cancelFulfillment(
|
||||||
|
data: OrderTypes.CancelOrderFulfillmentDTO,
|
||||||
|
sharedContext?: Context
|
||||||
|
): Promise<void> {
|
||||||
|
const items = data.items.map((item) => {
|
||||||
|
return {
|
||||||
|
action: ChangeActionType.CANCEL_ITEM_FULFILLMENT,
|
||||||
|
internal_note: item.internal_note,
|
||||||
|
reference: data.reference,
|
||||||
|
reference_id: data.reference_id,
|
||||||
|
details: {
|
||||||
|
reference_id: item.id,
|
||||||
|
quantity: item.quantity,
|
||||||
|
metadata: item.metadata,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const change = await this.createOrderChange_(
|
||||||
|
{
|
||||||
|
order_id: data.order_id,
|
||||||
|
description: data.description,
|
||||||
|
internal_note: data.internal_note,
|
||||||
|
created_by: data.created_by,
|
||||||
|
metadata: data.metadata,
|
||||||
|
actions: items,
|
||||||
|
},
|
||||||
|
sharedContext
|
||||||
|
)
|
||||||
|
|
||||||
|
await this.confirmOrderChange(change[0].id, sharedContext)
|
||||||
|
}
|
||||||
|
|
||||||
@InjectTransactionManager("baseRepository_")
|
@InjectTransactionManager("baseRepository_")
|
||||||
async registerShipment(
|
async registerShipment(
|
||||||
data: OrderTypes.RegisterOrderShipmentDTO,
|
data: OrderTypes.RegisterOrderShipmentDTO,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export enum ChangeActionType {
|
|||||||
CANCEL = "CANCEL",
|
CANCEL = "CANCEL",
|
||||||
CANCEL_RETURN = "CANCEL_RETURN",
|
CANCEL_RETURN = "CANCEL_RETURN",
|
||||||
FULFILL_ITEM = "FULFILL_ITEM",
|
FULFILL_ITEM = "FULFILL_ITEM",
|
||||||
|
CANCEL_ITEM_FULFILLMENT = "CANCEL_ITEM_FULFILLMENT",
|
||||||
ITEM_ADD = "ITEM_ADD",
|
ITEM_ADD = "ITEM_ADD",
|
||||||
ITEM_REMOVE = "ITEM_REMOVE",
|
ITEM_REMOVE = "ITEM_REMOVE",
|
||||||
RECEIVE_DAMAGED_RETURN_ITEM = "RECEIVE_DAMAGED_RETURN_ITEM",
|
RECEIVE_DAMAGED_RETURN_ITEM = "RECEIVE_DAMAGED_RETURN_ITEM",
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { MathBN, MedusaError, isDefined } from "@medusajs/utils"
|
||||||
|
import { ChangeActionType } from "../action-key"
|
||||||
|
import { OrderChangeProcessing } from "../calculate-order-change"
|
||||||
|
|
||||||
|
OrderChangeProcessing.registerActionType(
|
||||||
|
ChangeActionType.CANCEL_ITEM_FULFILLMENT,
|
||||||
|
{
|
||||||
|
operation({ action, currentOrder }) {
|
||||||
|
const existing = currentOrder.items.find(
|
||||||
|
(item) => item.id === action.details.reference_id
|
||||||
|
)!
|
||||||
|
|
||||||
|
existing.detail.fulfilled_quantity ??= 0
|
||||||
|
|
||||||
|
existing.detail.fulfilled_quantity = MathBN.sub(
|
||||||
|
existing.detail.fulfilled_quantity,
|
||||||
|
action.details.quantity
|
||||||
|
)
|
||||||
|
},
|
||||||
|
revert({ action, currentOrder }) {
|
||||||
|
const existing = currentOrder.items.find(
|
||||||
|
(item) => item.id === action.reference_id
|
||||||
|
)!
|
||||||
|
|
||||||
|
existing.detail.fulfilled_quantity = MathBN.add(
|
||||||
|
existing.detail.fulfilled_quantity,
|
||||||
|
action.details.quantity
|
||||||
|
)
|
||||||
|
},
|
||||||
|
validate({ action, currentOrder }) {
|
||||||
|
const refId = action.details?.reference_id
|
||||||
|
if (!isDefined(refId)) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
"Reference ID is required."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = currentOrder.items.find((item) => item.id === refId)
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`Item ID "${refId}" not found.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!action.details?.quantity) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`Quantity to cancel item fulfillment ${refId} is required.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.details?.quantity < 1) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`Quantity to cancel item ${refId} must be greater than 0.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const greater = MathBN.gt(
|
||||||
|
action.details?.quantity,
|
||||||
|
existing.detail?.fulfilled_quantity
|
||||||
|
)
|
||||||
|
if (greater) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`Cannot cancel more items than what was fulfilled for item ${refId}.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -48,7 +48,7 @@ OrderChangeProcessing.registerActionType(ChangeActionType.CANCEL_RETURN, {
|
|||||||
if (!existing) {
|
if (!existing) {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.INVALID_DATA,
|
MedusaError.Types.INVALID_DATA,
|
||||||
`Reference ID "${refId}" not found.`
|
`Item ID "${refId}" not found.`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ OrderChangeProcessing.registerActionType(ChangeActionType.FULFILL_ITEM, {
|
|||||||
if (!existing) {
|
if (!existing) {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.INVALID_DATA,
|
MedusaError.Types.INVALID_DATA,
|
||||||
`Reference ID "${refId}" not found.`
|
`Item ID "${refId}" not found.`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from "./cancel"
|
export * from "./cancel"
|
||||||
|
export * from "./cancel-item-fulfillment"
|
||||||
export * from "./cancel-return"
|
export * from "./cancel-return"
|
||||||
export * from "./fulfill-item"
|
export * from "./fulfill-item"
|
||||||
export * from "./item-add"
|
export * from "./item-add"
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ OrderChangeProcessing.registerActionType(ChangeActionType.ITEM_REMOVE, {
|
|||||||
if (!existing) {
|
if (!existing) {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.INVALID_DATA,
|
MedusaError.Types.INVALID_DATA,
|
||||||
`Reference ID "${refId}" not found.`
|
`Item ID "${refId}" not found.`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ OrderChangeProcessing.registerActionType(
|
|||||||
if (!existing) {
|
if (!existing) {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.INVALID_DATA,
|
MedusaError.Types.INVALID_DATA,
|
||||||
`Reference ID "${refId}" not found.`
|
`Item ID "${refId}" not found.`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ OrderChangeProcessing.registerActionType(ChangeActionType.RECEIVE_RETURN_ITEM, {
|
|||||||
if (!existing) {
|
if (!existing) {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.INVALID_DATA,
|
MedusaError.Types.INVALID_DATA,
|
||||||
`Reference ID "${refId}" not found.`
|
`Item ID "${refId}" not found.`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ OrderChangeProcessing.registerActionType(ChangeActionType.RETURN_ITEM, {
|
|||||||
if (!existing) {
|
if (!existing) {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.INVALID_DATA,
|
MedusaError.Types.INVALID_DATA,
|
||||||
`Reference ID "${refId}" not found.`
|
`Item ID "${refId}" not found.`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ OrderChangeProcessing.registerActionType(ChangeActionType.SHIP_ITEM, {
|
|||||||
if (!existing) {
|
if (!existing) {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.INVALID_DATA,
|
MedusaError.Types.INVALID_DATA,
|
||||||
`Reference ID "${refId}" not found.`
|
`Item ID "${refId}" not found.`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ OrderChangeProcessing.registerActionType(ChangeActionType.WRITE_OFF_ITEM, {
|
|||||||
if (!existing) {
|
if (!existing) {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.INVALID_DATA,
|
MedusaError.Types.INVALID_DATA,
|
||||||
`Reference ID "${refId}" not found.`
|
`Item ID "${refId}" not found.`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user