feat(core-flows,medusa,utils,types): adds delivered_quantity to order (#9130)
what: - adds delivered_quantity to order https://github.com/user-attachments/assets/709b1727-08ed-4a88-ae29-38f13540e301
This commit is contained in:
@@ -195,5 +195,18 @@ export async function createOrderSeeder({ api, container }) {
|
|||||||
|
|
||||||
order = (await api.get(`/admin/orders/${order.id}`, adminHeaders)).data.order
|
order = (await api.get(`/admin/orders/${order.id}`, adminHeaders)).data.order
|
||||||
|
|
||||||
return order
|
return {
|
||||||
|
order,
|
||||||
|
region,
|
||||||
|
salesChannel,
|
||||||
|
stockLocation,
|
||||||
|
inventoryItem,
|
||||||
|
shippingProfile,
|
||||||
|
product,
|
||||||
|
fulfillmentSets,
|
||||||
|
fulfillmentSet,
|
||||||
|
shippingOption,
|
||||||
|
cart,
|
||||||
|
paymentCollection,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { ModuleRegistrationName } from "@medusajs/utils"
|
||||||
|
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||||
|
import {
|
||||||
|
adminHeaders,
|
||||||
|
createAdminUser,
|
||||||
|
} from "../../../../helpers/create-admin-user"
|
||||||
|
import { setupTaxStructure } from "../../../../modules/__tests__/fixtures"
|
||||||
|
import { createOrderSeeder } from "../../fixtures/order"
|
||||||
|
|
||||||
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
|
medusaIntegrationTestRunner({
|
||||||
|
testSuite: ({ dbConnection, getContainer, api }) => {
|
||||||
|
let order, seeder
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const container = getContainer()
|
||||||
|
|
||||||
|
await setupTaxStructure(container.resolve(ModuleRegistrationName.TAX))
|
||||||
|
await createAdminUser(dbConnection, adminHeaders, container)
|
||||||
|
seeder = await createOrderSeeder({ api, container })
|
||||||
|
order = seeder.order
|
||||||
|
order = (await api.get(`/admin/orders/${order.id}`, adminHeaders)).data
|
||||||
|
.order
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("POST /orders/:id/fulfillments/:id/mark-as-delivered", () => {
|
||||||
|
it("should mark fulfillable item as delivered", async () => {
|
||||||
|
let fulfillableItem = order.items.find(
|
||||||
|
(item) => item.detail.fulfilled_quantity < item.detail.quantity
|
||||||
|
)
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/orders/${order.id}/fulfillments`,
|
||||||
|
{
|
||||||
|
location_id: seeder.stockLocation.id,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: fulfillableItem.id,
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
order = (await api.get(`/admin/orders/${order.id}`, adminHeaders)).data
|
||||||
|
.order
|
||||||
|
|
||||||
|
expect(order.items[0].detail).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
fulfilled_quantity: 1,
|
||||||
|
delivered_quantity: 0,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/orders/${order.id}/fulfillments/${order.fulfillments[0].id}/mark-as-delivered`,
|
||||||
|
{},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
order = (await api.get(`/admin/orders/${order.id}`, adminHeaders)).data
|
||||||
|
.order
|
||||||
|
|
||||||
|
expect(order.items[0].detail).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
fulfilled_quantity: 1,
|
||||||
|
delivered_quantity: 1,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const { response } = await api
|
||||||
|
.post(
|
||||||
|
`/admin/orders/${order.id}/fulfillments/${order.fulfillments[0].id}/mark-as-delivered`,
|
||||||
|
{},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
|
expect(response.data).toEqual({
|
||||||
|
type: "not_allowed",
|
||||||
|
message: "Fulfillment has already been marked delivered",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -24,7 +24,8 @@ medusaIntegrationTestRunner({
|
|||||||
|
|
||||||
await setupTaxStructure(container.resolve(Modules.TAX))
|
await setupTaxStructure(container.resolve(Modules.TAX))
|
||||||
await createAdminUser(dbConnection, adminHeaders, container)
|
await createAdminUser(dbConnection, adminHeaders, container)
|
||||||
order = await createOrderSeeder({ api, container })
|
const seeders = await createOrderSeeder({ api, container })
|
||||||
|
order = seeders.order
|
||||||
|
|
||||||
shippingProfile = (
|
shippingProfile = (
|
||||||
await api.post(
|
await api.post(
|
||||||
@@ -157,7 +158,7 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("RMA Flows", () => {
|
describe("RMA Flows", () => {
|
||||||
it.only("should verify order summary at each level", async () => {
|
it("should verify order summary at each level", async () => {
|
||||||
/* Case:
|
/* Case:
|
||||||
Purchased:
|
Purchased:
|
||||||
items: {
|
items: {
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ medusaIntegrationTestRunner({
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
container = getContainer()
|
container = getContainer()
|
||||||
await createAdminUser(dbConnection, adminHeaders, container)
|
await createAdminUser(dbConnection, adminHeaders, container)
|
||||||
order = await createOrderSeeder({ api, container })
|
const seeders = await createOrderSeeder({ api, container })
|
||||||
|
order = seeders.order
|
||||||
|
|
||||||
await api.post(
|
await api.post(
|
||||||
`/admin/orders/${order.id}/fulfillments`,
|
`/admin/orders/${order.id}/fulfillments`,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { FetchError } from "@medusajs/js-sdk"
|
||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
QueryKey,
|
QueryKey,
|
||||||
useMutation,
|
useMutation,
|
||||||
@@ -5,8 +7,6 @@ import {
|
|||||||
useQuery,
|
useQuery,
|
||||||
UseQueryOptions,
|
UseQueryOptions,
|
||||||
} from "@tanstack/react-query"
|
} from "@tanstack/react-query"
|
||||||
import { FetchError } from "@medusajs/js-sdk"
|
|
||||||
import { HttpTypes } from "@medusajs/types"
|
|
||||||
import { sdk } from "../../lib/client"
|
import { sdk } from "../../lib/client"
|
||||||
import { queryClient } from "../../lib/query-client"
|
import { queryClient } from "../../lib/query-client"
|
||||||
import { queryKeysFactory, TQueryKey } from "../../lib/query-key-factory"
|
import { queryKeysFactory, TQueryKey } from "../../lib/query-key-factory"
|
||||||
@@ -184,6 +184,33 @@ export const useCreateOrderShipment = (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useMarkOrderFulfillmentAsDelivered = (
|
||||||
|
orderId: string,
|
||||||
|
fulfillmentId: string,
|
||||||
|
options?: UseMutationOptions<
|
||||||
|
{ order: HttpTypes.AdminOrder },
|
||||||
|
FetchError,
|
||||||
|
HttpTypes.AdminMarkOrderFulfillmentAsDelivered
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (payload: HttpTypes.AdminMarkOrderFulfillmentAsDelivered) =>
|
||||||
|
sdk.admin.order.markAsDelivered(orderId, fulfillmentId, payload),
|
||||||
|
onSuccess: (data: any, variables: any, context: any) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ordersQueryKeys.all,
|
||||||
|
})
|
||||||
|
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ordersQueryKeys.preview(orderId),
|
||||||
|
})
|
||||||
|
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const useCancelOrder = (
|
export const useCancelOrder = (
|
||||||
orderId: string,
|
orderId: string,
|
||||||
options?: UseMutationOptions<any, FetchError, any>
|
options?: UseMutationOptions<any, FetchError, any>
|
||||||
|
|||||||
@@ -1065,6 +1065,7 @@
|
|||||||
},
|
},
|
||||||
"fulfillment": {
|
"fulfillment": {
|
||||||
"cancelWarning": "You are about to cancel a fulfillment. This action cannot be undone.",
|
"cancelWarning": "You are about to cancel a fulfillment. This action cannot be undone.",
|
||||||
|
"markAsDeliveredWarning": "You are about to mark fulfillment as delivered. This action cannot be undone.",
|
||||||
"unfulfilledItems": "Unfulfilled Items",
|
"unfulfilledItems": "Unfulfilled Items",
|
||||||
"statusLabel": "Fulfillment status",
|
"statusLabel": "Fulfillment status",
|
||||||
"statusTitle": "Fulfillment Status",
|
"statusTitle": "Fulfillment Status",
|
||||||
@@ -1076,6 +1077,7 @@
|
|||||||
"available": "Available",
|
"available": "Available",
|
||||||
"inStock": "In stock",
|
"inStock": "In stock",
|
||||||
"markAsShipped": "Mark as shipped",
|
"markAsShipped": "Mark as shipped",
|
||||||
|
"markAsDelivered": "Mark as delivered",
|
||||||
"itemsToFulfillDesc": "Choose items and quantities to fulfill",
|
"itemsToFulfillDesc": "Choose items and quantities to fulfill",
|
||||||
"locationDescription": "Choose which location you want to fulfill items from.",
|
"locationDescription": "Choose which location you want to fulfill items from.",
|
||||||
"sendNotificationHint": "Notify customers about the created fulfillment.",
|
"sendNotificationHint": "Notify customers about the created fulfillment.",
|
||||||
@@ -1091,6 +1093,8 @@
|
|||||||
"fulfilled": "Fulfilled",
|
"fulfilled": "Fulfilled",
|
||||||
"partiallyShipped": "Partially shipped",
|
"partiallyShipped": "Partially shipped",
|
||||||
"shipped": "Shipped",
|
"shipped": "Shipped",
|
||||||
|
"delivered": "Delivered",
|
||||||
|
"partiallyDelivered": "Partially delivered",
|
||||||
"partiallyReturned": "Partially returned",
|
"partiallyReturned": "Partially returned",
|
||||||
"returned": "Returned",
|
"returned": "Returned",
|
||||||
"canceled": "Canceled",
|
"canceled": "Canceled",
|
||||||
@@ -1099,7 +1103,8 @@
|
|||||||
"toast": {
|
"toast": {
|
||||||
"created": "Fulfillment created successfully",
|
"created": "Fulfillment created successfully",
|
||||||
"canceled": "Fulfillment successfully canceled",
|
"canceled": "Fulfillment successfully canceled",
|
||||||
"fulfillmentShipped": "Cannot cancel an already shipped fulfillment"
|
"fulfillmentShipped": "Cannot cancel an already shipped fulfillment",
|
||||||
|
"fulfillmentDelivered": "Fulfillment marked as delivered successfully"
|
||||||
},
|
},
|
||||||
"trackingLabel": "Tracking",
|
"trackingLabel": "Tracking",
|
||||||
"shippingFromLabel": "Shipping from",
|
"shippingFromLabel": "Shipping from",
|
||||||
@@ -1155,6 +1160,7 @@
|
|||||||
"created": "Items fulfilled",
|
"created": "Items fulfilled",
|
||||||
"canceled": "Fulfillment canceled",
|
"canceled": "Fulfillment canceled",
|
||||||
"shipped": "Items shipped",
|
"shipped": "Items shipped",
|
||||||
|
"delivered": "Items delivered",
|
||||||
"items_one": "{{count}} item",
|
"items_one": "{{count}} item",
|
||||||
"items_other": "{{count}} items"
|
"items_other": "{{count}} items"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -45,6 +45,11 @@ export const getOrderFulfillmentStatus = (
|
|||||||
"orange",
|
"orange",
|
||||||
],
|
],
|
||||||
shipped: [t("orders.fulfillment.status.shipped"), "green"],
|
shipped: [t("orders.fulfillment.status.shipped"), "green"],
|
||||||
|
delivered: [t("orders.fulfillment.status.delivered"), "green"],
|
||||||
|
partially_delivered: [
|
||||||
|
t("orders.fulfillment.status.partiallyDelivered"),
|
||||||
|
"orange",
|
||||||
|
],
|
||||||
partially_returned: [
|
partially_returned: [
|
||||||
t("orders.fulfillment.status.partiallyReturned"),
|
t("orders.fulfillment.status.partiallyReturned"),
|
||||||
"orange",
|
"orange",
|
||||||
|
|||||||
+15
-5
@@ -16,6 +16,7 @@ import {
|
|||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
import { AdminOrderLineItem } from "@medusajs/types"
|
import { AdminOrderLineItem } from "@medusajs/types"
|
||||||
|
import { useOrderChanges } from "../../../../../hooks/api"
|
||||||
import { useCancelClaim, useClaims } from "../../../../../hooks/api/claims"
|
import { useCancelClaim, useClaims } from "../../../../../hooks/api/claims"
|
||||||
import {
|
import {
|
||||||
useCancelExchange,
|
useCancelExchange,
|
||||||
@@ -25,7 +26,6 @@ import { useCancelReturn, useReturns } from "../../../../../hooks/api/returns"
|
|||||||
import { useDate } from "../../../../../hooks/use-date"
|
import { useDate } from "../../../../../hooks/use-date"
|
||||||
import { getStylizedAmount } from "../../../../../lib/money-amount-helpers"
|
import { getStylizedAmount } from "../../../../../lib/money-amount-helpers"
|
||||||
import { getPaymentsFromOrder } from "../order-payment-section"
|
import { getPaymentsFromOrder } from "../order-payment-section"
|
||||||
import { useOrderChanges } from "../../../../../hooks/api"
|
|
||||||
import ActivityItems from "./activity-items"
|
import ActivityItems from "./activity-items"
|
||||||
|
|
||||||
type OrderTimelineProps = {
|
type OrderTimelineProps = {
|
||||||
@@ -222,6 +222,16 @@ const useActivityItems = (order: AdminOrder): Activity[] => {
|
|||||||
children: <FulfillmentCreatedBody fulfillment={fulfillment} />,
|
children: <FulfillmentCreatedBody fulfillment={fulfillment} />,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (fulfillment.delivered_at) {
|
||||||
|
items.push({
|
||||||
|
title: t("orders.activity.events.fulfillment.delivered"),
|
||||||
|
timestamp: fulfillment.delivered_at,
|
||||||
|
children: (
|
||||||
|
<FulfillmentCreatedBody fulfillment={fulfillment} />
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (fulfillment.shipped_at) {
|
if (fulfillment.shipped_at) {
|
||||||
items.push({
|
items.push({
|
||||||
title: t("orders.activity.events.fulfillment.shipped"),
|
title: t("orders.activity.events.fulfillment.shipped"),
|
||||||
@@ -340,10 +350,10 @@ const useActivityItems = (order: AdminOrder): Activity[] => {
|
|||||||
edit.status === "requested"
|
edit.status === "requested"
|
||||||
? edit.requested_at
|
? edit.requested_at
|
||||||
: edit.status === "declined"
|
: edit.status === "declined"
|
||||||
? edit.declined_at
|
? edit.declined_at
|
||||||
: edit.status === "canceled"
|
: edit.status === "canceled"
|
||||||
? edit.canceled_at
|
? edit.canceled_at
|
||||||
: edit.created_at,
|
: edit.created_at,
|
||||||
children: isConfirmed ? (
|
children: isConfirmed ? (
|
||||||
<OrderEditBody edit={edit} itemsMap={itemsMap} />
|
<OrderEditBody edit={edit} itemsMap={itemsMap} />
|
||||||
) : null,
|
) : null,
|
||||||
|
|||||||
+59
-10
@@ -22,7 +22,10 @@ import { Link, useNavigate } from "react-router-dom"
|
|||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
import { Skeleton } from "../../../../../components/common/skeleton"
|
import { Skeleton } from "../../../../../components/common/skeleton"
|
||||||
import { Thumbnail } from "../../../../../components/common/thumbnail"
|
import { Thumbnail } from "../../../../../components/common/thumbnail"
|
||||||
import { useCancelOrderFulfillment } from "../../../../../hooks/api/orders"
|
import {
|
||||||
|
useCancelOrderFulfillment,
|
||||||
|
useMarkOrderFulfillmentAsDelivered,
|
||||||
|
} from "../../../../../hooks/api/orders"
|
||||||
import { useStockLocation } from "../../../../../hooks/api/stock-locations"
|
import { useStockLocation } from "../../../../../hooks/api/stock-locations"
|
||||||
import { formatProvider } from "../../../../../lib/format-provider"
|
import { formatProvider } from "../../../../../lib/format-provider"
|
||||||
import { getLocaleAmount } from "../../../../../lib/money-amount-helpers"
|
import { getLocaleAmount } from "../../../../../lib/money-amount-helpers"
|
||||||
@@ -183,6 +186,10 @@ const Fulfillment = ({
|
|||||||
statusText = "Canceled"
|
statusText = "Canceled"
|
||||||
statusColor = "red"
|
statusColor = "red"
|
||||||
statusTimestamp = fulfillment.canceled_at
|
statusTimestamp = fulfillment.canceled_at
|
||||||
|
} else if (fulfillment.delivered_at) {
|
||||||
|
statusText = "Delivered"
|
||||||
|
statusColor = "green"
|
||||||
|
statusTimestamp = fulfillment.delivered_at
|
||||||
} else if (fulfillment.shipped_at) {
|
} else if (fulfillment.shipped_at) {
|
||||||
statusText = "Shipped"
|
statusText = "Shipped"
|
||||||
statusColor = "green"
|
statusColor = "green"
|
||||||
@@ -190,8 +197,41 @@ const Fulfillment = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { mutateAsync } = useCancelOrderFulfillment(order.id, fulfillment.id)
|
const { mutateAsync } = useCancelOrderFulfillment(order.id, fulfillment.id)
|
||||||
|
const { mutateAsync: markAsDelivered } = useMarkOrderFulfillmentAsDelivered(
|
||||||
|
order.id,
|
||||||
|
fulfillment.id
|
||||||
|
)
|
||||||
|
|
||||||
const showShippingButton = !fulfillment.canceled_at && !fulfillment.shipped_at
|
const showShippingButton =
|
||||||
|
!fulfillment.canceled_at &&
|
||||||
|
!fulfillment.shipped_at &&
|
||||||
|
!fulfillment.delivered_at
|
||||||
|
const showDeliveryButton =
|
||||||
|
!fulfillment.canceled_at && !fulfillment.delivered_at
|
||||||
|
|
||||||
|
const handleMarkAsDelivered = async () => {
|
||||||
|
const res = await prompt({
|
||||||
|
title: t("general.areYouSure"),
|
||||||
|
description: t("orders.fulfillment.markAsDeliveredWarning"),
|
||||||
|
confirmText: t("actions.continue"),
|
||||||
|
cancelText: t("actions.cancel"),
|
||||||
|
variant: "confirmation",
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
await markAsDelivered(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(t("orders.fulfillment.toast.fulfillmentDelivered"))
|
||||||
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
toast.error(e.message)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleCancel = async () => {
|
const handleCancel = async () => {
|
||||||
if (fulfillment.shipped_at) {
|
if (fulfillment.shipped_at) {
|
||||||
@@ -343,14 +383,23 @@ const Fulfillment = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{showShippingButton && (
|
|
||||||
<div className="bg-ui-bg-subtle flex items-center justify-end rounded-b-xl px-4 py-4">
|
{(showShippingButton || showDeliveryButton) && (
|
||||||
<Button
|
<div className="bg-ui-bg-subtle flex items-center justify-end rounded-b-xl px-4 py-4 gap-x-2">
|
||||||
onClick={() => navigate(`./${fulfillment.id}/create-shipment`)}
|
{showDeliveryButton && (
|
||||||
variant="secondary"
|
<Button onClick={handleMarkAsDelivered} variant="secondary">
|
||||||
>
|
{t("orders.fulfillment.markAsDelivered")}
|
||||||
{t("orders.fulfillment.markAsShipped")}
|
</Button>
|
||||||
</Button>
|
)}
|
||||||
|
|
||||||
|
{showShippingButton && (
|
||||||
|
<Button
|
||||||
|
onClick={() => navigate(`./${fulfillment.id}/create-shipment`)}
|
||||||
|
variant="secondary"
|
||||||
|
>
|
||||||
|
{t("orders.fulfillment.markAsShipped")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export const updateFulfillmentStep = createStep(
|
|||||||
{ container }
|
{ container }
|
||||||
) => {
|
) => {
|
||||||
const { id, ...data } = input
|
const { id, ...data } = input
|
||||||
|
|
||||||
const service = container.resolve<IFulfillmentModuleService>(
|
const service = container.resolve<IFulfillmentModuleService>(
|
||||||
Modules.FULFILLMENT
|
Modules.FULFILLMENT
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ export * from "./create-shipping-profiles"
|
|||||||
export * from "./delete-fulfillment-sets"
|
export * from "./delete-fulfillment-sets"
|
||||||
export * from "./delete-service-zones"
|
export * from "./delete-service-zones"
|
||||||
export * from "./delete-shipping-options"
|
export * from "./delete-shipping-options"
|
||||||
|
export * from "./mark-fulfillment-as-delivered"
|
||||||
export * from "./update-fulfillment"
|
export * from "./update-fulfillment"
|
||||||
export * from "./update-service-zones"
|
export * from "./update-service-zones"
|
||||||
export * from "./update-shipping-options"
|
export * from "./update-shipping-options"
|
||||||
export * from "./update-shipping-profiles"
|
export * from "./update-shipping-profiles"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { FulfillmentDTO } from "@medusajs/types"
|
||||||
|
import { MedusaError } from "@medusajs/utils"
|
||||||
|
import {
|
||||||
|
StepResponse,
|
||||||
|
WorkflowData,
|
||||||
|
WorkflowResponse,
|
||||||
|
createStep,
|
||||||
|
createWorkflow,
|
||||||
|
transform,
|
||||||
|
} from "@medusajs/workflows-sdk"
|
||||||
|
import { useRemoteQueryStep } from "../../common"
|
||||||
|
import { updateFulfillmentWorkflow } from "./update-fulfillment"
|
||||||
|
|
||||||
|
export const validateFulfillmentDeliverabilityStepId =
|
||||||
|
"validate-fulfillment-deliverability"
|
||||||
|
/**
|
||||||
|
* This step validates that if a fulfillment can be marked delivered
|
||||||
|
*/
|
||||||
|
export const validateFulfillmentDeliverabilityStep = createStep(
|
||||||
|
validateFulfillmentDeliverabilityStepId,
|
||||||
|
async (fulfillment: FulfillmentDTO) => {
|
||||||
|
if (fulfillment.canceled_at) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_ALLOWED,
|
||||||
|
"Cannot deliver an already canceled fulfillment"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fulfillment.delivered_at) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_ALLOWED,
|
||||||
|
"Fulfillment has already been marked delivered"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new StepResponse(void 0)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const markFulfillmentAsDeliveredWorkflowId =
|
||||||
|
"mark-fulfillment-as-delivered-workflow"
|
||||||
|
/**
|
||||||
|
* This workflow marks fulfillment as delivered.
|
||||||
|
*/
|
||||||
|
export const markFulfillmentAsDeliveredWorkflow = createWorkflow(
|
||||||
|
markFulfillmentAsDeliveredWorkflowId,
|
||||||
|
({ id }: WorkflowData<{ id: string }>) => {
|
||||||
|
const fulfillment = useRemoteQueryStep({
|
||||||
|
entry_point: "fulfillment",
|
||||||
|
fields: ["id", "delivered_at", "canceled_at"],
|
||||||
|
variables: { id },
|
||||||
|
throw_if_key_not_found: true,
|
||||||
|
list: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
validateFulfillmentDeliverabilityStep(fulfillment)
|
||||||
|
|
||||||
|
const updateInput = transform({ id }, ({ id }) => ({
|
||||||
|
id,
|
||||||
|
delivered_at: new Date(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
return new WorkflowResponse(
|
||||||
|
updateFulfillmentWorkflow.runAsStep({ input: updateInput })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { IOrderModuleService, RegisterOrderDeliveryDTO } from "@medusajs/types"
|
||||||
|
import { ModuleRegistrationName } from "@medusajs/utils"
|
||||||
|
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||||
|
|
||||||
|
export const registerOrderDeliveryStepId = "register-order-delivery"
|
||||||
|
/**
|
||||||
|
* This step registers a delivery for an order fulfillment.
|
||||||
|
*/
|
||||||
|
export const registerOrderDeliveryStep = createStep(
|
||||||
|
registerOrderDeliveryStepId,
|
||||||
|
async (data: RegisterOrderDeliveryDTO, { container }) => {
|
||||||
|
const service = container.resolve<IOrderModuleService>(
|
||||||
|
ModuleRegistrationName.ORDER
|
||||||
|
)
|
||||||
|
|
||||||
|
await service.registerDelivery(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)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -41,6 +41,7 @@ export * from "./exchange/update-exchange-add-item"
|
|||||||
export * from "./exchange/update-exchange-shipping-method"
|
export * from "./exchange/update-exchange-shipping-method"
|
||||||
export * from "./get-order-detail"
|
export * from "./get-order-detail"
|
||||||
export * from "./get-orders-list"
|
export * from "./get-orders-list"
|
||||||
|
export * from "./mark-order-fulfillment-as-delivered"
|
||||||
export * from "./mark-payment-collection-as-paid"
|
export * from "./mark-payment-collection-as-paid"
|
||||||
export * from "./order-edit/begin-order-edit"
|
export * from "./order-edit/begin-order-edit"
|
||||||
export * from "./order-edit/cancel-begin-order-edit"
|
export * from "./order-edit/cancel-begin-order-edit"
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import {
|
||||||
|
FulfillmentDTO,
|
||||||
|
OrderDTO,
|
||||||
|
RegisterOrderDeliveryDTO,
|
||||||
|
} from "@medusajs/types"
|
||||||
|
import { FulfillmentEvents, Modules } from "@medusajs/utils"
|
||||||
|
import {
|
||||||
|
WorkflowData,
|
||||||
|
WorkflowResponse,
|
||||||
|
createStep,
|
||||||
|
createWorkflow,
|
||||||
|
parallelize,
|
||||||
|
transform,
|
||||||
|
} from "@medusajs/workflows-sdk"
|
||||||
|
import { emitEventStep, useRemoteQueryStep } from "../../common"
|
||||||
|
import { markFulfillmentAsDeliveredWorkflow } from "../../fulfillment"
|
||||||
|
import { registerOrderDeliveryStep } from "../steps/register-delivery"
|
||||||
|
import {
|
||||||
|
throwIfItemsDoesNotExistsInOrder,
|
||||||
|
throwIfOrderIsCancelled,
|
||||||
|
} from "../utils/order-validation"
|
||||||
|
|
||||||
|
export const orderFulfillmentDeliverablilityValidationStepId =
|
||||||
|
"order-fulfillment-deliverability-validation"
|
||||||
|
/**
|
||||||
|
* This step validates that order & fulfillment are valid
|
||||||
|
*/
|
||||||
|
export const orderFulfillmentDeliverablilityValidationStep = createStep(
|
||||||
|
orderFulfillmentDeliverablilityValidationStepId,
|
||||||
|
async ({
|
||||||
|
fulfillment,
|
||||||
|
order,
|
||||||
|
}: {
|
||||||
|
order: OrderDTO & { fulfillments: FulfillmentDTO[] }
|
||||||
|
fulfillment: FulfillmentDTO
|
||||||
|
}) => {
|
||||||
|
throwIfOrderIsCancelled({ order })
|
||||||
|
|
||||||
|
const orderFulfillment = order.fulfillments?.find(
|
||||||
|
(f) => f.id === fulfillment.id
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!orderFulfillment) {
|
||||||
|
throw new Error(
|
||||||
|
`Fulfillment with id ${fulfillment.id} not found in the order`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
throwIfItemsDoesNotExistsInOrder({
|
||||||
|
order,
|
||||||
|
inputItems: order.items!.map((i) => ({
|
||||||
|
id: i.id,
|
||||||
|
quantity: i.quantity,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function prepareRegisterDeliveryData({
|
||||||
|
fulfillment,
|
||||||
|
order,
|
||||||
|
}: {
|
||||||
|
fulfillment: FulfillmentDTO
|
||||||
|
order: OrderDTO & { fulfillments: FulfillmentDTO[] }
|
||||||
|
}): RegisterOrderDeliveryDTO {
|
||||||
|
const orderFulfillment = order.fulfillments.find(
|
||||||
|
(f) => f.id === fulfillment.id
|
||||||
|
)!
|
||||||
|
|
||||||
|
return {
|
||||||
|
order_id: order.id,
|
||||||
|
reference: Modules.FULFILLMENT,
|
||||||
|
reference_id: orderFulfillment.id,
|
||||||
|
items: orderFulfillment.items!.map((i) => {
|
||||||
|
return {
|
||||||
|
id: i.line_item_id!,
|
||||||
|
quantity: i.quantity!,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const markOrderFulfillmentAsDeliveredWorkflowId =
|
||||||
|
"mark-order-fulfillment-as-delivered-workflow"
|
||||||
|
/**
|
||||||
|
* This workflow marks a fulfillment in an order as delivered.
|
||||||
|
*/
|
||||||
|
export const markOrderFulfillmentAsDeliveredWorkflow = createWorkflow(
|
||||||
|
markOrderFulfillmentAsDeliveredWorkflowId,
|
||||||
|
(input: WorkflowData<{ orderId: string; fulfillmentId: string }>) => {
|
||||||
|
const { fulfillmentId, orderId } = input
|
||||||
|
const fulfillment = useRemoteQueryStep({
|
||||||
|
entry_point: "fulfillment",
|
||||||
|
fields: ["id"],
|
||||||
|
variables: { id: fulfillmentId },
|
||||||
|
throw_if_key_not_found: true,
|
||||||
|
list: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const order = useRemoteQueryStep({
|
||||||
|
entry_point: "order",
|
||||||
|
fields: [
|
||||||
|
"id",
|
||||||
|
"summary",
|
||||||
|
"currency_code",
|
||||||
|
"region_id",
|
||||||
|
"fulfillments.id",
|
||||||
|
"fulfillments.items.id",
|
||||||
|
"fulfillments.items.quantity",
|
||||||
|
"fulfillments.items.line_item_id",
|
||||||
|
"items.id",
|
||||||
|
"items.quantity",
|
||||||
|
],
|
||||||
|
variables: { id: orderId },
|
||||||
|
throw_if_key_not_found: true,
|
||||||
|
list: false,
|
||||||
|
}).config({ name: "order-query" })
|
||||||
|
|
||||||
|
orderFulfillmentDeliverablilityValidationStep({ order, fulfillment })
|
||||||
|
|
||||||
|
const deliveryData = transform(
|
||||||
|
{ order, fulfillment },
|
||||||
|
prepareRegisterDeliveryData
|
||||||
|
)
|
||||||
|
|
||||||
|
const [deliveredFulfillment] = parallelize(
|
||||||
|
markFulfillmentAsDeliveredWorkflow.runAsStep({
|
||||||
|
input: { id: fulfillment.id },
|
||||||
|
}),
|
||||||
|
registerOrderDeliveryStep(deliveryData)
|
||||||
|
)
|
||||||
|
|
||||||
|
emitEventStep({
|
||||||
|
eventName: FulfillmentEvents.DELIVERY_CREATED,
|
||||||
|
data: { id: deliveredFulfillment.id },
|
||||||
|
})
|
||||||
|
|
||||||
|
return new WorkflowResponse(void 0)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -112,6 +112,24 @@ export class Order {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async markAsDelivered(
|
||||||
|
id: string,
|
||||||
|
fulfillmentId: string,
|
||||||
|
body: HttpTypes.AdminMarkOrderFulfillmentAsDelivered,
|
||||||
|
query?: SelectParams,
|
||||||
|
headers?: ClientHeaders
|
||||||
|
) {
|
||||||
|
return await this.client.fetch<{ order: HttpTypes.AdminOrder }>(
|
||||||
|
`/admin/orders/${id}/fulfillments/${fulfillmentId}/mark-as-delivered`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
query,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async listChanges(
|
async listChanges(
|
||||||
id: string,
|
id: string,
|
||||||
queryParams?: FindParams & HttpTypes.AdminOrderChangesFilters,
|
queryParams?: FindParams & HttpTypes.AdminOrderChangesFilters,
|
||||||
|
|||||||
@@ -19,3 +19,5 @@ export interface AdminCreateOrderShipment {
|
|||||||
export interface AdminCancelOrderFulfillment {
|
export interface AdminCancelOrderFulfillment {
|
||||||
no_notification?: boolean
|
no_notification?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdminMarkOrderFulfillmentAsDelivered {}
|
||||||
|
|||||||
@@ -165,6 +165,7 @@ export interface BaseOrderItemDetail {
|
|||||||
item: BaseOrderLineItem
|
item: BaseOrderLineItem
|
||||||
quantity: number
|
quantity: number
|
||||||
fulfilled_quantity: number
|
fulfilled_quantity: number
|
||||||
|
delivered_quantity: number
|
||||||
shipped_quantity: number
|
shipped_quantity: number
|
||||||
return_requested_quantity: number
|
return_requested_quantity: number
|
||||||
return_received_quantity: number
|
return_received_quantity: number
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { ClaimReason } from "./mutations"
|
|||||||
export type ChangeActionType =
|
export type ChangeActionType =
|
||||||
| "CANCEL_RETURN_ITEM"
|
| "CANCEL_RETURN_ITEM"
|
||||||
| "FULFILL_ITEM"
|
| "FULFILL_ITEM"
|
||||||
|
| "DELIVER_ITEM"
|
||||||
| "CANCEL_ITEM_FULFILLMENT"
|
| "CANCEL_ITEM_FULFILLMENT"
|
||||||
| "ITEM_ADD"
|
| "ITEM_ADD"
|
||||||
| "ITEM_REMOVE"
|
| "ITEM_REMOVE"
|
||||||
@@ -899,6 +900,16 @@ export interface OrderItemDTO {
|
|||||||
*/
|
*/
|
||||||
raw_fulfilled_quantity: BigNumberRawValue
|
raw_fulfilled_quantity: BigNumberRawValue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The delivered quantity of the order line item.
|
||||||
|
*/
|
||||||
|
delivered_quantity: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The raw delivered quantity of the order line item.
|
||||||
|
*/
|
||||||
|
raw_delivered_quantity: BigNumberRawValue
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The shipped quantity of the order line item.
|
* The shipped quantity of the order line item.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1468,6 +1468,22 @@ export interface RegisterOrderShipmentDTO extends BaseOrderBundledActionsDTO {
|
|||||||
no_notification?: boolean
|
no_notification?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The details to register a delivery of an order, return, exchange,
|
||||||
|
* or claim.
|
||||||
|
*/
|
||||||
|
export interface RegisterOrderDeliveryDTO extends BaseOrderBundledActionsDTO {
|
||||||
|
/**
|
||||||
|
* The items of the delivery.
|
||||||
|
*/
|
||||||
|
items?: BaseOrderBundledItemActionsDTO[]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the customer should receive notifications about the delivery.
|
||||||
|
*/
|
||||||
|
no_notification?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The return to be created.
|
* The return to be created.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ import {
|
|||||||
CreateOrderTransactionDTO,
|
CreateOrderTransactionDTO,
|
||||||
DeclineOrderChangeDTO,
|
DeclineOrderChangeDTO,
|
||||||
ReceiveOrderReturnDTO,
|
ReceiveOrderReturnDTO,
|
||||||
|
RegisterOrderDeliveryDTO,
|
||||||
RegisterOrderFulfillmentDTO,
|
RegisterOrderFulfillmentDTO,
|
||||||
RegisterOrderShipmentDTO,
|
RegisterOrderShipmentDTO,
|
||||||
UpdateOrderAddressDTO,
|
UpdateOrderAddressDTO,
|
||||||
@@ -4591,6 +4592,29 @@ export interface IOrderModuleService extends IModuleService {
|
|||||||
sharedContext?: Context
|
sharedContext?: Context
|
||||||
): Promise<void>
|
): Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method registers a delivery for an order's fulfillment
|
||||||
|
*
|
||||||
|
* @param {RegisterOrderDeliveryDTO} data - The ordes's delivery data.
|
||||||
|
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
|
||||||
|
* @returns {Promise<void>} Resolves when the delivery is registered successfully.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* await orderModuleService.registerDelivery({
|
||||||
|
* order_id: "123",
|
||||||
|
* items: [
|
||||||
|
* {
|
||||||
|
* id: "321",
|
||||||
|
* quantity: 1
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
registerDelivery(
|
||||||
|
data: RegisterOrderDeliveryDTO,
|
||||||
|
sharedContext?: Context
|
||||||
|
): Promise<void>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method creates a return.
|
* This method creates a return.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -16,3 +16,13 @@ export interface CreateOrderShipmentWorkflowInput {
|
|||||||
no_notification?: boolean
|
no_notification?: boolean
|
||||||
metadata?: MetadataType
|
metadata?: MetadataType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CreateOrderDeliveryItem {
|
||||||
|
id: string
|
||||||
|
quantity: BigNumberInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateOrderDeliveryWorkflowInput {
|
||||||
|
order_id: string
|
||||||
|
fulfillment_id: string
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,4 +30,5 @@ const eventBaseNames: [
|
|||||||
export const FulfillmentEvents = {
|
export const FulfillmentEvents = {
|
||||||
...buildEventNamesFromEntityName(eventBaseNames, Modules.FULFILLMENT),
|
...buildEventNamesFromEntityName(eventBaseNames, Modules.FULFILLMENT),
|
||||||
SHIPMENT_CREATED: "shipment.created",
|
SHIPMENT_CREATED: "shipment.created",
|
||||||
|
DELIVERY_CREATED: "delivery.created",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export enum ChangeActionType {
|
export enum ChangeActionType {
|
||||||
FULFILL_ITEM = "FULFILL_ITEM",
|
FULFILL_ITEM = "FULFILL_ITEM",
|
||||||
|
DELIVER_ITEM = "DELIVER_ITEM",
|
||||||
CANCEL_ITEM_FULFILLMENT = "CANCEL_ITEM_FULFILLMENT",
|
CANCEL_ITEM_FULFILLMENT = "CANCEL_ITEM_FULFILLMENT",
|
||||||
ITEM_ADD = "ITEM_ADD",
|
ITEM_ADD = "ITEM_ADD",
|
||||||
ITEM_REMOVE = "ITEM_REMOVE",
|
ITEM_REMOVE = "ITEM_REMOVE",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export interface GetItemTotalInput {
|
|||||||
adjustments?: Pick<AdjustmentLineDTO, "amount">[]
|
adjustments?: Pick<AdjustmentLineDTO, "amount">[]
|
||||||
detail?: {
|
detail?: {
|
||||||
fulfilled_quantity: BigNumber
|
fulfilled_quantity: BigNumber
|
||||||
|
delivered_quantity: BigNumber
|
||||||
shipped_quantity: BigNumber
|
shipped_quantity: BigNumber
|
||||||
return_requested_quantity: BigNumber
|
return_requested_quantity: BigNumber
|
||||||
return_received_quantity: BigNumber
|
return_received_quantity: BigNumber
|
||||||
|
|||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
import { markOrderFulfillmentAsDeliveredWorkflow } from "@medusajs/core-flows"
|
||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
import {
|
||||||
|
AuthenticatedMedusaRequest,
|
||||||
|
MedusaResponse,
|
||||||
|
} from "../../../../../../../types/routing"
|
||||||
|
import { refetchEntity } from "../../../../../../utils/refetch-entity"
|
||||||
|
import { AdminMarkOrderFulfillmentDeliveredType } from "../../../../validators"
|
||||||
|
|
||||||
|
export const POST = async (
|
||||||
|
req: AuthenticatedMedusaRequest<AdminMarkOrderFulfillmentDeliveredType>,
|
||||||
|
res: MedusaResponse<HttpTypes.AdminOrderResponse>
|
||||||
|
) => {
|
||||||
|
const { id: orderId, fulfillment_id: fulfillmentId } = req.params
|
||||||
|
|
||||||
|
await markOrderFulfillmentAsDeliveredWorkflow(req.scope).run({
|
||||||
|
input: { orderId, fulfillmentId },
|
||||||
|
})
|
||||||
|
|
||||||
|
const order = await refetchEntity(
|
||||||
|
"order",
|
||||||
|
orderId,
|
||||||
|
req.scope,
|
||||||
|
req.remoteQueryConfig.fields
|
||||||
|
)
|
||||||
|
|
||||||
|
res.status(200).json({ order })
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
AdminCompleteOrder,
|
AdminCompleteOrder,
|
||||||
AdminGetOrdersOrderParams,
|
AdminGetOrdersOrderParams,
|
||||||
AdminGetOrdersParams,
|
AdminGetOrdersParams,
|
||||||
|
AdminMarkOrderFulfillmentDelivered,
|
||||||
AdminOrderCancelFulfillment,
|
AdminOrderCancelFulfillment,
|
||||||
AdminOrderChanges,
|
AdminOrderChanges,
|
||||||
AdminOrderCreateFulfillment,
|
AdminOrderCreateFulfillment,
|
||||||
@@ -119,4 +120,15 @@ export const adminOrderRoutesMiddlewares: MiddlewareRoute[] = [
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
method: ["POST"],
|
||||||
|
matcher: "/admin/orders/:id/fulfillments/:fulfillment_id/mark-as-delivered",
|
||||||
|
middlewares: [
|
||||||
|
validateAndTransformBody(AdminMarkOrderFulfillmentDelivered),
|
||||||
|
validateAndTransformQuery(
|
||||||
|
AdminGetOrdersOrderParams,
|
||||||
|
QueryConfig.retrieveTransformQueryConfig
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -101,3 +101,8 @@ export const AdminOrderChanges = z.object({
|
|||||||
deleted_at: createOperatorMap().optional(),
|
deleted_at: createOperatorMap().optional(),
|
||||||
})
|
})
|
||||||
export type AdminOrderChangesType = z.infer<typeof AdminOrderChanges>
|
export type AdminOrderChangesType = z.infer<typeof AdminOrderChanges>
|
||||||
|
|
||||||
|
export type AdminMarkOrderFulfillmentDeliveredType = z.infer<
|
||||||
|
typeof AdminMarkOrderFulfillmentDelivered
|
||||||
|
>
|
||||||
|
export const AdminMarkOrderFulfillmentDelivered = z.object({})
|
||||||
|
|||||||
@@ -1043,7 +1043,7 @@ export default class InventoryModuleService
|
|||||||
locationId: string,
|
locationId: string,
|
||||||
@MedusaContext() context: Context = {}
|
@MedusaContext() context: Context = {}
|
||||||
): Promise<InventoryTypes.InventoryLevelDTO> {
|
): Promise<InventoryTypes.InventoryLevelDTO> {
|
||||||
const inventoryLevel = await this.listInventoryLevels(
|
const [inventoryLevel] = await this.listInventoryLevels(
|
||||||
{ inventory_item_id: inventoryItemId, location_id: locationId },
|
{ inventory_item_id: inventoryItemId, location_id: locationId },
|
||||||
{ take: null },
|
{ take: null },
|
||||||
context
|
context
|
||||||
@@ -1056,7 +1056,7 @@ export default class InventoryModuleService
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return inventoryLevel[0]
|
return inventoryLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -397,6 +397,13 @@ moduleIntegrationTestRunner<IOrderModuleService>({
|
|||||||
quantity: 4,
|
quantity: 4,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
action: ChangeActionType.DELIVER_ITEM,
|
||||||
|
details: {
|
||||||
|
reference_id: createdOrder.items![1].id,
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -421,6 +428,7 @@ moduleIntegrationTestRunner<IOrderModuleService>({
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
quantity: 4,
|
quantity: 4,
|
||||||
fulfilled_quantity: 1,
|
fulfilled_quantity: 1,
|
||||||
|
delivered_quantity: 1,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -499,6 +507,30 @@ moduleIntegrationTestRunner<IOrderModuleService>({
|
|||||||
fulfilled_quantity: 2,
|
fulfilled_quantity: 2,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const orderChange5 = await service.createOrderChange({
|
||||||
|
order_id: createdOrder.id,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
action: ChangeActionType.DELIVER_ITEM,
|
||||||
|
details: {
|
||||||
|
reference_id: createdOrder.items![1].id,
|
||||||
|
quantity: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.confirmOrderChange({
|
||||||
|
id: orderChange5.id,
|
||||||
|
})
|
||||||
|
).rejects.toThrow(
|
||||||
|
`Cannot deliver more items than what was fulfilled for item ${
|
||||||
|
createdOrder.items![1].id
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
await service.deleteOrderChanges([orderChange5.id])
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should create an order change, add actions to it, confirm the changes, revert all the changes and restore the changes again.", async function () {
|
it("should create an order change, add actions to it, confirm the changes, revert all the changes and restore the changes again.", async function () {
|
||||||
|
|||||||
@@ -869,6 +869,25 @@
|
|||||||
"nullable": false,
|
"nullable": false,
|
||||||
"mappedType": "json"
|
"mappedType": "json"
|
||||||
},
|
},
|
||||||
|
"delivered_quantity": {
|
||||||
|
"name": "delivered_quantity",
|
||||||
|
"type": "numeric",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": false,
|
||||||
|
"default": "0",
|
||||||
|
"mappedType": "decimal"
|
||||||
|
},
|
||||||
|
"raw_delivered_quantity": {
|
||||||
|
"name": "raw_delivered_quantity",
|
||||||
|
"type": "jsonb",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": false,
|
||||||
|
"mappedType": "json"
|
||||||
|
},
|
||||||
"shipped_quantity": {
|
"shipped_quantity": {
|
||||||
"name": "shipped_quantity",
|
"name": "shipped_quantity",
|
||||||
"type": "numeric",
|
"type": "numeric",
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Migration } from "@mikro-orm/migrations"
|
||||||
|
|
||||||
|
export class Migration20240913092514 extends Migration {
|
||||||
|
async up(): Promise<void> {
|
||||||
|
this.addSql(
|
||||||
|
'alter table if exists "order_item" add column if not exists "delivered_quantity" numeric not null default 0, add column if not exists "raw_delivered_quantity" jsonb;'
|
||||||
|
)
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`UPDATE "order_item" SET raw_delivered_quantity = '{"value": "0", "precision": 20}'::jsonb;`
|
||||||
|
)
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
'ALTER TABLE IF EXISTS "order_item" ALTER COLUMN "raw_delivered_quantity" SET NOT NULL;'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(): Promise<void> {
|
||||||
|
this.addSql(
|
||||||
|
'alter table if exists "order_item" drop column if exists "delivered_quantity";'
|
||||||
|
)
|
||||||
|
this.addSql(
|
||||||
|
'alter table if exists "order_item" drop column if exists "raw_delivered_quantity";'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -96,6 +96,12 @@ export default class OrderItem {
|
|||||||
@Property({ columnType: "jsonb" })
|
@Property({ columnType: "jsonb" })
|
||||||
raw_fulfilled_quantity: BigNumberRawValue
|
raw_fulfilled_quantity: BigNumberRawValue
|
||||||
|
|
||||||
|
@MikroOrmBigNumberProperty()
|
||||||
|
delivered_quantity: BigNumber | number = 0
|
||||||
|
|
||||||
|
@Property({ columnType: "jsonb" })
|
||||||
|
raw_delivered_quantity: BigNumberRawValue
|
||||||
|
|
||||||
@MikroOrmBigNumberProperty()
|
@MikroOrmBigNumberProperty()
|
||||||
shipped_quantity: BigNumber | number = 0
|
shipped_quantity: BigNumber | number = 0
|
||||||
|
|
||||||
|
|||||||
@@ -6,5 +6,6 @@ export * from "./create-claim"
|
|||||||
export * from "./create-exchange"
|
export * from "./create-exchange"
|
||||||
export * from "./create-return"
|
export * from "./create-return"
|
||||||
export * from "./receive-return"
|
export * from "./receive-return"
|
||||||
|
export * from "./register-delivery"
|
||||||
export * from "./register-fulfillment"
|
export * from "./register-fulfillment"
|
||||||
export * from "./register-shipment"
|
export * from "./register-shipment"
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Context, OrderTypes } from "@medusajs/types"
|
||||||
|
import { ChangeActionType } from "@medusajs/utils"
|
||||||
|
|
||||||
|
export async function registerDelivery(
|
||||||
|
this: any,
|
||||||
|
data: OrderTypes.RegisterOrderDeliveryDTO,
|
||||||
|
sharedContext?: Context
|
||||||
|
): Promise<void> {
|
||||||
|
const items = data.items?.map((item) => {
|
||||||
|
return {
|
||||||
|
action: ChangeActionType.DELIVER_ITEM,
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -3295,4 +3295,12 @@ export default class OrderModuleService<
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return await BundledActions.registerShipment.bind(this)(data, sharedContext)
|
return await BundledActions.registerShipment.bind(this)(data, sharedContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@InjectTransactionManager("baseRepository_")
|
||||||
|
async registerDelivery(
|
||||||
|
data: OrderTypes.RegisterOrderDeliveryDTO,
|
||||||
|
@MedusaContext() sharedContext?: Context
|
||||||
|
): Promise<void> {
|
||||||
|
return await BundledActions.registerDelivery.bind(this)(data, sharedContext)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export type VirtualOrder = {
|
|||||||
quantity: BigNumberInput
|
quantity: BigNumberInput
|
||||||
shipped_quantity: BigNumberInput
|
shipped_quantity: BigNumberInput
|
||||||
fulfilled_quantity: BigNumberInput
|
fulfilled_quantity: BigNumberInput
|
||||||
|
delivered_quantity: BigNumberInput
|
||||||
return_requested_quantity: BigNumberInput
|
return_requested_quantity: BigNumberInput
|
||||||
return_received_quantity: BigNumberInput
|
return_received_quantity: BigNumberInput
|
||||||
return_dismissed_quantity: BigNumberInput
|
return_dismissed_quantity: BigNumberInput
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { ChangeActionType, MathBN, MedusaError } from "@medusajs/utils"
|
||||||
|
import { OrderChangeProcessing } from "../calculate-order-change"
|
||||||
|
import { setActionReference } from "../set-action-reference"
|
||||||
|
|
||||||
|
OrderChangeProcessing.registerActionType(ChangeActionType.DELIVER_ITEM, {
|
||||||
|
operation({ action, currentOrder, options }) {
|
||||||
|
const item = currentOrder.items.find(
|
||||||
|
(item) => item.id === action.details.reference_id
|
||||||
|
)!
|
||||||
|
|
||||||
|
item.detail.delivered_quantity ??= 0
|
||||||
|
|
||||||
|
item.detail.delivered_quantity = MathBN.add(
|
||||||
|
item.detail.delivered_quantity,
|
||||||
|
action.details.quantity
|
||||||
|
)
|
||||||
|
|
||||||
|
setActionReference(item, action, options)
|
||||||
|
},
|
||||||
|
validate({ action, currentOrder }) {
|
||||||
|
const refId = action.details?.reference_id
|
||||||
|
|
||||||
|
if (refId == null) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
"Reference ID is required."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = currentOrder.items.find((item) => item.id === refId)
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`Item ID "${refId}" not found.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalDeliverable = MathBN.convert(item.quantity)
|
||||||
|
const totalDelivered = MathBN.convert(item.detail?.delivered_quantity)
|
||||||
|
const newDelivered = MathBN.convert(action.details?.quantity ?? 0)
|
||||||
|
const newTotalDelivered = MathBN.sum(totalDelivered, newDelivered)
|
||||||
|
|
||||||
|
const totalFulfilled = MathBN.convert(item.detail?.fulfilled_quantity)
|
||||||
|
|
||||||
|
if (MathBN.lte(newDelivered, 0)) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`Quantity of item ${refId} must be greater than 0.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MathBN.gt(newTotalDelivered, totalFulfilled)) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`Cannot deliver more items than what was fulfilled for item ${refId}.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MathBN.gt(newTotalDelivered, totalDeliverable)) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`Cannot deliver more items than what was ordered for item ${refId}.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
export * from "./cancel-item-fulfillment"
|
export * from "./cancel-item-fulfillment"
|
||||||
export * from "./cancel-return"
|
export * from "./cancel-return"
|
||||||
|
export * from "./deliver-item"
|
||||||
export * from "./fulfill-item"
|
export * from "./fulfill-item"
|
||||||
export * from "./item-add"
|
export * from "./item-add"
|
||||||
export * from "./item-remove"
|
export * from "./item-remove"
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export function applyChangesToOrder(
|
|||||||
version,
|
version,
|
||||||
quantity: orderItem.quantity,
|
quantity: orderItem.quantity,
|
||||||
fulfilled_quantity: orderItem.fulfilled_quantity ?? 0,
|
fulfilled_quantity: orderItem.fulfilled_quantity ?? 0,
|
||||||
|
delivered_quantity: orderItem.delivered_quantity ?? 0,
|
||||||
shipped_quantity: orderItem.shipped_quantity ?? 0,
|
shipped_quantity: orderItem.shipped_quantity ?? 0,
|
||||||
return_requested_quantity: orderItem.return_requested_quantity ?? 0,
|
return_requested_quantity: orderItem.return_requested_quantity ?? 0,
|
||||||
return_received_quantity: orderItem.return_received_quantity ?? 0,
|
return_received_quantity: orderItem.return_received_quantity ?? 0,
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ export class OrderChangeProcessing {
|
|||||||
isReplay = false
|
isReplay = false
|
||||||
): BigNumberInput | void {
|
): BigNumberInput | void {
|
||||||
const definedType = OrderChangeProcessing.typeDefinition[action.action]
|
const definedType = OrderChangeProcessing.typeDefinition[action.action]
|
||||||
|
|
||||||
if (!isPresent(definedType)) {
|
if (!isPresent(definedType)) {
|
||||||
throw new Error(`Action type ${action.action} is not defined`)
|
throw new Error(`Action type ${action.action} is not defined`)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user