feat(core-flows,dashboard,types,fulfillment,medusa): uses requires shipping throughout lifecycle (#9170)
what: - uses requires shipping throughout lifecycle https://github.com/user-attachments/assets/d5ba89d3-5ea0-49c4-b2d5-490c4764933e
This commit is contained in:
1 parent
e54b339324
commit
69f6645716
22 files changed
+433
-51
No files matched your search
@@ -1,10 +1,30 @@
|
|||||||
|
import {
|
||||||
|
AdminInventoryItem,
|
||||||
|
AdminProduct,
|
||||||
|
AdminStockLocation,
|
||||||
|
MedusaContainer,
|
||||||
|
} from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
adminHeaders,
|
adminHeaders,
|
||||||
generatePublishableKey,
|
generatePublishableKey,
|
||||||
generateStoreHeaders,
|
generateStoreHeaders,
|
||||||
} from "../../../helpers/create-admin-user"
|
} from "../../../helpers/create-admin-user"
|
||||||
|
|
||||||
export async function createOrderSeeder({ api, container }) {
|
export async function createOrderSeeder({
|
||||||
|
api,
|
||||||
|
container,
|
||||||
|
productOverride,
|
||||||
|
additionalProducts,
|
||||||
|
stockChannelOverride,
|
||||||
|
inventoryItemOverride,
|
||||||
|
}: {
|
||||||
|
api: any
|
||||||
|
container: MedusaContainer
|
||||||
|
productOverride?: AdminProduct
|
||||||
|
stockChannelOverride?: AdminStockLocation
|
||||||
|
additionalProducts?: AdminProduct[]
|
||||||
|
inventoryItemOverride?: AdminInventoryItem
|
||||||
|
}) {
|
||||||
const publishableKey = await generatePublishableKey(container)
|
const publishableKey = await generatePublishableKey(container)
|
||||||
const storeHeaders = generateStoreHeaders({ publishableKey })
|
const storeHeaders = generateStoreHeaders({ publishableKey })
|
||||||
|
|
||||||
@@ -24,7 +44,9 @@ export async function createOrderSeeder({ api, container }) {
|
|||||||
)
|
)
|
||||||
).data.sales_channel
|
).data.sales_channel
|
||||||
|
|
||||||
const stockLocation = (
|
const stockLocation =
|
||||||
|
stockChannelOverride ??
|
||||||
|
(
|
||||||
await api.post(
|
await api.post(
|
||||||
`/admin/stock-locations`,
|
`/admin/stock-locations`,
|
||||||
{ name: "test location" },
|
{ name: "test location" },
|
||||||
@@ -32,7 +54,9 @@ export async function createOrderSeeder({ api, container }) {
|
|||||||
)
|
)
|
||||||
).data.stock_location
|
).data.stock_location
|
||||||
|
|
||||||
const inventoryItem = (
|
const inventoryItem =
|
||||||
|
inventoryItemOverride ??
|
||||||
|
(
|
||||||
await api.post(
|
await api.post(
|
||||||
`/admin/inventory-items`,
|
`/admin/inventory-items`,
|
||||||
{ sku: "test-variant" },
|
{ sku: "test-variant" },
|
||||||
@@ -63,7 +87,9 @@ export async function createOrderSeeder({ api, container }) {
|
|||||||
)
|
)
|
||||||
).data.shipping_profile
|
).data.shipping_profile
|
||||||
|
|
||||||
const product = (
|
const product =
|
||||||
|
productOverride ??
|
||||||
|
(
|
||||||
await api.post(
|
await api.post(
|
||||||
"/admin/products",
|
"/admin/products",
|
||||||
{
|
{
|
||||||
@@ -167,7 +193,13 @@ export async function createOrderSeeder({ api, container }) {
|
|||||||
postal_code: "94016",
|
postal_code: "94016",
|
||||||
},
|
},
|
||||||
sales_channel_id: salesChannel.id,
|
sales_channel_id: salesChannel.id,
|
||||||
items: [{ quantity: 1, variant_id: product.variants[0].id }],
|
items: [
|
||||||
|
{ quantity: 1, variant_id: product.variants[0].id },
|
||||||
|
...(additionalProducts || []).map((p) => ({
|
||||||
|
quantity: 1,
|
||||||
|
variant_id: p.variants?.[0]?.id,
|
||||||
|
})),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
storeHeaders
|
storeHeaders
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,13 +18,192 @@ medusaIntegrationTestRunner({
|
|||||||
|
|
||||||
await setupTaxStructure(container.resolve(ModuleRegistrationName.TAX))
|
await setupTaxStructure(container.resolve(ModuleRegistrationName.TAX))
|
||||||
await createAdminUser(dbConnection, adminHeaders, container)
|
await createAdminUser(dbConnection, adminHeaders, container)
|
||||||
seeder = await createOrderSeeder({ api, container })
|
})
|
||||||
|
|
||||||
|
describe("POST /orders/:id/fulfillments", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
const stockChannelOverride = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/stock-locations`,
|
||||||
|
{ name: "test location" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.stock_location
|
||||||
|
|
||||||
|
const inventoryItemOverride = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/inventory-items`,
|
||||||
|
{ sku: "test-variant", requires_shipping: true },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.inventory_item
|
||||||
|
|
||||||
|
const productOverride = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/products",
|
||||||
|
{
|
||||||
|
title: `Test fixture`,
|
||||||
|
options: [
|
||||||
|
{ title: "size", values: ["large", "small"] },
|
||||||
|
{ title: "color", values: ["green"] },
|
||||||
|
],
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "Test variant",
|
||||||
|
sku: "test-variant",
|
||||||
|
inventory_items: [
|
||||||
|
{
|
||||||
|
inventory_item_id: inventoryItemOverride.id,
|
||||||
|
required_quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
currency_code: "usd",
|
||||||
|
amount: 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
options: {
|
||||||
|
size: "large",
|
||||||
|
color: "green",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.product
|
||||||
|
|
||||||
|
const inventoryItemOverride2 = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/inventory-items`,
|
||||||
|
{ sku: "test-variant-2", requires_shipping: false },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.inventory_item
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/inventory-items/${inventoryItemOverride2.id}/location-levels`,
|
||||||
|
{
|
||||||
|
location_id: stockChannelOverride.id,
|
||||||
|
stocked_quantity: 10,
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const productOverride2 = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/products",
|
||||||
|
{
|
||||||
|
title: `Test fixture 2`,
|
||||||
|
options: [
|
||||||
|
{ title: "size", values: ["large", "small"] },
|
||||||
|
{ title: "color", values: ["green"] },
|
||||||
|
],
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "Test variant 2",
|
||||||
|
sku: "test-variant-2",
|
||||||
|
inventory_items: [
|
||||||
|
{
|
||||||
|
inventory_item_id: inventoryItemOverride2.id,
|
||||||
|
required_quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
currency_code: "usd",
|
||||||
|
amount: 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
options: {
|
||||||
|
size: "large",
|
||||||
|
color: "green",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.product
|
||||||
|
|
||||||
|
seeder = await createOrderSeeder({
|
||||||
|
api,
|
||||||
|
container: getContainer(),
|
||||||
|
productOverride,
|
||||||
|
additionalProducts: [productOverride2],
|
||||||
|
stockChannelOverride,
|
||||||
|
inventoryItemOverride,
|
||||||
|
})
|
||||||
order = seeder.order
|
order = seeder.order
|
||||||
order = (await api.get(`/admin/orders/${order.id}`, adminHeaders)).data
|
order = (await api.get(`/admin/orders/${order.id}`, adminHeaders)).data
|
||||||
.order
|
.order
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should only create fulfillments grouped by shipping requirement", async () => {
|
||||||
|
const {
|
||||||
|
response: { data },
|
||||||
|
} = await api
|
||||||
|
.post(
|
||||||
|
`/admin/orders/${order.id}/fulfillments`,
|
||||||
|
{
|
||||||
|
location_id: seeder.stockLocation.id,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: order.items[0].id,
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: order.items[1].id,
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
|
expect(data).toEqual({
|
||||||
|
type: "invalid_data",
|
||||||
|
message: `Fulfillment can only be created entirely with items with shipping or items without shipping. Split this request into 2 fulfillments.`,
|
||||||
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: { order: fulfillableOrder },
|
||||||
|
} = await api.post(
|
||||||
|
`/admin/orders/${order.id}/fulfillments?fields=+fulfillments.id,fulfillments.requires_shipping`,
|
||||||
|
{
|
||||||
|
location_id: seeder.stockLocation.id,
|
||||||
|
items: [{ id: order.items[0].id, quantity: 1 }],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(fulfillableOrder.fulfillments).toHaveLength(1)
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: { order: fulfillableOrder2 },
|
||||||
|
} = await api.post(
|
||||||
|
`/admin/orders/${order.id}/fulfillments?fields=+fulfillments.id,fulfillments.requires_shipping`,
|
||||||
|
{
|
||||||
|
location_id: seeder.stockLocation.id,
|
||||||
|
items: [{ id: order.items[1].id, quantity: 1 }],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(fulfillableOrder2.fulfillments).toHaveLength(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("POST /orders/:id/fulfillments/:id/mark-as-delivered", () => {
|
describe("POST /orders/:id/fulfillments/:id/mark-as-delivered", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
seeder = await createOrderSeeder({ api, container: getContainer() })
|
||||||
|
order = seeder.order
|
||||||
|
order = (await api.get(`/admin/orders/${order.id}`, adminHeaders)).data
|
||||||
|
.order
|
||||||
|
})
|
||||||
|
|
||||||
it("should mark fulfillable item as delivered", async () => {
|
it("should mark fulfillable item as delivered", async () => {
|
||||||
let fulfillableItem = order.items.find(
|
let fulfillableItem = order.items.find(
|
||||||
(item) => item.detail.fulfilled_quantity < item.detail.quantity
|
(item) => item.detail.fulfilled_quantity < item.detail.quantity
|
||||||
|
|||||||
@@ -1091,6 +1091,7 @@
|
|||||||
"statusTitle": "Fulfillment Status",
|
"statusTitle": "Fulfillment Status",
|
||||||
"fulfillItems": "Fulfill items",
|
"fulfillItems": "Fulfill items",
|
||||||
"awaitingFulfillmentBadge": "Awaiting fulfillment",
|
"awaitingFulfillmentBadge": "Awaiting fulfillment",
|
||||||
|
"requiresShipping": "Requires shipping",
|
||||||
"number": "Fulfillment #{{number}}",
|
"number": "Fulfillment #{{number}}",
|
||||||
"itemsToFulfill": "Items to fulfill",
|
"itemsToFulfill": "Items to fulfill",
|
||||||
"create": "Create Fulfillment",
|
"create": "Create Fulfillment",
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import { AdminOrderLineItem } from "@medusajs/types"
|
|||||||
|
|
||||||
export function getReturnableQuantity(item: AdminOrderLineItem): number {
|
export function getReturnableQuantity(item: AdminOrderLineItem): number {
|
||||||
const {
|
const {
|
||||||
shipped_quantity,
|
delivered_quantity,
|
||||||
return_received_quantity,
|
return_received_quantity,
|
||||||
return_dismissed_quantity,
|
return_dismissed_quantity,
|
||||||
return_requested_quantity,
|
return_requested_quantity,
|
||||||
} = item.detail
|
} = item.detail
|
||||||
|
|
||||||
return (
|
return (
|
||||||
shipped_quantity -
|
delivered_quantity -
|
||||||
(return_received_quantity +
|
(return_received_quantity +
|
||||||
return_requested_quantity +
|
return_requested_quantity +
|
||||||
return_dismissed_quantity)
|
return_dismissed_quantity)
|
||||||
|
|||||||
+29
-11
@@ -8,6 +8,7 @@ import { Alert, Button, Select, Switch, toast } from "@medusajs/ui"
|
|||||||
import { useForm, useWatch } from "react-hook-form"
|
import { useForm, useWatch } from "react-hook-form"
|
||||||
|
|
||||||
import { OrderLineItemDTO } from "@medusajs/types"
|
import { OrderLineItemDTO } from "@medusajs/types"
|
||||||
|
import { useSearchParams } from "react-router-dom"
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
import {
|
import {
|
||||||
RouteFocusModal,
|
RouteFocusModal,
|
||||||
@@ -16,40 +17,48 @@ import {
|
|||||||
import { useCreateOrderFulfillment } from "../../../../../hooks/api/orders"
|
import { useCreateOrderFulfillment } from "../../../../../hooks/api/orders"
|
||||||
import { useStockLocations } from "../../../../../hooks/api/stock-locations"
|
import { useStockLocations } from "../../../../../hooks/api/stock-locations"
|
||||||
import { getFulfillableQuantity } from "../../../../../lib/order-item"
|
import { getFulfillableQuantity } from "../../../../../lib/order-item"
|
||||||
import { OrderCreateFulfillmentItem } from "./order-create-fulfillment-item"
|
|
||||||
import { CreateFulfillmentSchema } from "./constants"
|
import { CreateFulfillmentSchema } from "./constants"
|
||||||
import { useShippingOptions } from "../../../../../hooks/api"
|
import { OrderCreateFulfillmentItem } from "./order-create-fulfillment-item"
|
||||||
|
|
||||||
type OrderCreateFulfillmentFormProps = {
|
type OrderCreateFulfillmentFormProps = {
|
||||||
order: AdminOrder
|
order: AdminOrder
|
||||||
|
requiresShipping: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OrderCreateFulfillmentForm({
|
export function OrderCreateFulfillmentForm({
|
||||||
order,
|
order,
|
||||||
|
requiresShipping,
|
||||||
}: OrderCreateFulfillmentFormProps) {
|
}: OrderCreateFulfillmentFormProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { handleSuccess } = useRouteModal()
|
const { handleSuccess } = useRouteModal()
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
|
|
||||||
const { mutateAsync: createOrderFulfillment, isPending: isMutating } =
|
const { mutateAsync: createOrderFulfillment, isPending: isMutating } =
|
||||||
useCreateOrderFulfillment(order.id)
|
useCreateOrderFulfillment(order.id)
|
||||||
|
|
||||||
const [fulfillableItems, setFulfillableItems] = useState(() =>
|
const [fulfillableItems, setFulfillableItems] = useState(() =>
|
||||||
(order.items || []).filter((item) => getFulfillableQuantity(item) > 0)
|
(order.items || []).filter(
|
||||||
|
(item) =>
|
||||||
|
item.requires_shipping === requiresShipping &&
|
||||||
|
getFulfillableQuantity(item) > 0
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
const form = useForm<zod.infer<typeof CreateFulfillmentSchema>>({
|
const form = useForm<zod.infer<typeof CreateFulfillmentSchema>>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
quantity: fulfillableItems.reduce((acc, item) => {
|
quantity: fulfillableItems.reduce(
|
||||||
|
(acc, item) => {
|
||||||
acc[item.id] = getFulfillableQuantity(item)
|
acc[item.id] = getFulfillableQuantity(item)
|
||||||
return acc
|
return acc
|
||||||
}, {} as Record<string, number>),
|
},
|
||||||
|
{} as Record<string, number>
|
||||||
|
),
|
||||||
send_notification: !order.no_notification,
|
send_notification: !order.no_notification,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(CreateFulfillmentSchema),
|
resolver: zodResolver(CreateFulfillmentSchema),
|
||||||
})
|
})
|
||||||
|
|
||||||
const { stock_locations = [] } = useStockLocations()
|
const { stock_locations = [] } = useStockLocations()
|
||||||
const { shipping_options = [] } = useShippingOptions()
|
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async (data) => {
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
try {
|
try {
|
||||||
@@ -84,12 +93,18 @@ export function OrderCreateFulfillmentForm({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const fulfilledQuantityArray = (order.items || []).map(
|
const fulfilledQuantityArray = (order.items || []).map(
|
||||||
(item) => item.detail.fulfilled_quantity
|
(item) =>
|
||||||
|
item.requires_shipping === requiresShipping &&
|
||||||
|
item.detail.fulfilled_quantity
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const itemsToFulfill =
|
const itemsToFulfill =
|
||||||
order?.items?.filter((item) => getFulfillableQuantity(item) > 0) || []
|
order?.items?.filter(
|
||||||
|
(item) =>
|
||||||
|
item.requires_shipping === requiresShipping &&
|
||||||
|
getFulfillableQuantity(item) > 0
|
||||||
|
) || []
|
||||||
|
|
||||||
setFulfillableItems(itemsToFulfill)
|
setFulfillableItems(itemsToFulfill)
|
||||||
|
|
||||||
@@ -102,13 +117,16 @@ export function OrderCreateFulfillmentForm({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const quantityMap = itemsToFulfill.reduce((acc, item) => {
|
const quantityMap = itemsToFulfill.reduce(
|
||||||
|
(acc, item) => {
|
||||||
acc[item.id] = getFulfillableQuantity(item as OrderLineItemDTO)
|
acc[item.id] = getFulfillableQuantity(item as OrderLineItemDTO)
|
||||||
return acc
|
return acc
|
||||||
}, {} as Record<string, number>)
|
},
|
||||||
|
{} as Record<string, number>
|
||||||
|
)
|
||||||
|
|
||||||
form.setValue("quantity", quantityMap)
|
form.setValue("quantity", quantityMap)
|
||||||
}, [...fulfilledQuantityArray])
|
}, [...fulfilledQuantityArray, requiresShipping])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RouteFocusModal.Form form={form}>
|
<RouteFocusModal.Form form={form}>
|
||||||
|
|||||||
+9
-2
@@ -1,4 +1,4 @@
|
|||||||
import { useParams } from "react-router-dom"
|
import { useParams, useSearchParams } from "react-router-dom"
|
||||||
|
|
||||||
import { RouteFocusModal } from "../../../components/modals"
|
import { RouteFocusModal } from "../../../components/modals"
|
||||||
import { useOrder } from "../../../hooks/api/orders"
|
import { useOrder } from "../../../hooks/api/orders"
|
||||||
@@ -6,6 +6,8 @@ import { OrderCreateFulfillmentForm } from "./components/order-create-fulfillmen
|
|||||||
|
|
||||||
export function OrderCreateFulfillment() {
|
export function OrderCreateFulfillment() {
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
|
const requiresShipping = searchParams.get("requires_shipping") === "true"
|
||||||
|
|
||||||
const { order, isLoading, isError, error } = useOrder(id!, {
|
const { order, isLoading, isError, error } = useOrder(id!, {
|
||||||
fields: "currency_code,*items,*items.variant,*shipping_address",
|
fields: "currency_code,*items,*items.variant,*shipping_address",
|
||||||
@@ -19,7 +21,12 @@ export function OrderCreateFulfillment() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RouteFocusModal>
|
<RouteFocusModal>
|
||||||
{ready && <OrderCreateFulfillmentForm order={order} />}
|
{ready && (
|
||||||
|
<OrderCreateFulfillmentForm
|
||||||
|
order={order}
|
||||||
|
requiresShipping={requiresShipping}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</RouteFocusModal>
|
</RouteFocusModal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+54
-11
@@ -2,6 +2,7 @@ import { Buildings, XCircle } from "@medusajs/icons"
|
|||||||
import {
|
import {
|
||||||
AdminOrder,
|
AdminOrder,
|
||||||
AdminOrderFulfillment,
|
AdminOrderFulfillment,
|
||||||
|
AdminOrderLineItem,
|
||||||
HttpTypes,
|
HttpTypes,
|
||||||
OrderLineItemDTO,
|
OrderLineItemDTO,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
@@ -108,25 +109,64 @@ const UnfulfilledItem = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const UnfulfilledItemBreakdown = ({ order }: { order: AdminOrder }) => {
|
const UnfulfilledItemBreakdown = ({ order }: { order: AdminOrder }) => {
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
// Create an array of order items that haven't been fulfilled or at least not fully fulfilled
|
// Create an array of order items that haven't been fulfilled or at least not fully fulfilled
|
||||||
const unfulfilledItems = order.items!.filter(
|
const unfulfilledItemsWithShipping = order.items!.filter(
|
||||||
(i) => i.detail.fulfilled_quantity < i.quantity
|
(i) => i.requires_shipping && i.detail.fulfilled_quantity < i.quantity
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!unfulfilledItems.length) {
|
const unfulfilledItemsWithoutShipping = order.items!.filter(
|
||||||
return null
|
(i) => !i.requires_shipping && i.detail.fulfilled_quantity < i.quantity
|
||||||
}
|
)
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!!unfulfilledItemsWithShipping.length && (
|
||||||
|
<UnfulfilledItemDisplay
|
||||||
|
order={order}
|
||||||
|
unfulfilledItems={unfulfilledItemsWithShipping}
|
||||||
|
requiresShipping={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!!unfulfilledItemsWithoutShipping.length && (
|
||||||
|
<UnfulfilledItemDisplay
|
||||||
|
order={order}
|
||||||
|
unfulfilledItems={unfulfilledItemsWithoutShipping}
|
||||||
|
requiresShipping={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const UnfulfilledItemDisplay = ({
|
||||||
|
order,
|
||||||
|
unfulfilledItems,
|
||||||
|
requiresShipping = false,
|
||||||
|
}: {
|
||||||
|
order: AdminOrder
|
||||||
|
unfulfilledItems: AdminOrderLineItem[]
|
||||||
|
requiresShipping: boolean
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className="divide-y p-0">
|
<Container className="divide-y p-0">
|
||||||
<div className="flex items-center justify-between px-6 py-4">
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
<Heading level="h2">{t("orders.fulfillment.unfulfilledItems")}</Heading>
|
<Heading level="h2">{t("orders.fulfillment.unfulfilledItems")}</Heading>
|
||||||
|
|
||||||
<div className="flex items-center gap-x-4">
|
<div className="flex items-center gap-x-4">
|
||||||
|
{requiresShipping && (
|
||||||
|
<StatusBadge color="red" className="text-nowrap">
|
||||||
|
{t("orders.fulfillment.requiresShipping")}
|
||||||
|
</StatusBadge>
|
||||||
|
)}
|
||||||
|
|
||||||
<StatusBadge color="red" className="text-nowrap">
|
<StatusBadge color="red" className="text-nowrap">
|
||||||
{t("orders.fulfillment.awaitingFulfillmentBadge")}
|
{t("orders.fulfillment.awaitingFulfillmentBadge")}
|
||||||
</StatusBadge>
|
</StatusBadge>
|
||||||
|
|
||||||
<ActionMenu
|
<ActionMenu
|
||||||
groups={[
|
groups={[
|
||||||
{
|
{
|
||||||
@@ -134,7 +174,7 @@ const UnfulfilledItemBreakdown = ({ order }: { order: AdminOrder }) => {
|
|||||||
{
|
{
|
||||||
label: t("orders.fulfillment.fulfillItems"),
|
label: t("orders.fulfillment.fulfillItems"),
|
||||||
icon: <Buildings />,
|
icon: <Buildings />,
|
||||||
to: `/orders/${order.id}/fulfillment`,
|
to: `/orders/${order.id}/fulfillment?requires_shipping=${requiresShipping}`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -143,7 +183,7 @@ const UnfulfilledItemBreakdown = ({ order }: { order: AdminOrder }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{unfulfilledItems.map((item) => (
|
{unfulfilledItems.map((item: AdminOrderLineItem) => (
|
||||||
<UnfulfilledItem
|
<UnfulfilledItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
item={item}
|
item={item}
|
||||||
@@ -178,7 +218,9 @@ const Fulfillment = ({
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
let statusText = "Awaiting shipping"
|
let statusText = fulfillment.requires_shipping
|
||||||
|
? "Awaiting shipping"
|
||||||
|
: "Awaiting delivery"
|
||||||
let statusColor: "blue" | "green" | "red" = "blue"
|
let statusColor: "blue" | "green" | "red" = "blue"
|
||||||
let statusTimestamp = fulfillment.created_at
|
let statusTimestamp = fulfillment.created_at
|
||||||
|
|
||||||
@@ -205,7 +247,8 @@ const Fulfillment = ({
|
|||||||
const showShippingButton =
|
const showShippingButton =
|
||||||
!fulfillment.canceled_at &&
|
!fulfillment.canceled_at &&
|
||||||
!fulfillment.shipped_at &&
|
!fulfillment.shipped_at &&
|
||||||
!fulfillment.delivered_at
|
!fulfillment.delivered_at &&
|
||||||
|
fulfillment.requires_shipping
|
||||||
const showDeliveryButton =
|
const showDeliveryButton =
|
||||||
!fulfillment.canceled_at && !fulfillment.delivered_at
|
!fulfillment.canceled_at && !fulfillment.delivered_at
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ export const completeCartFields = [
|
|||||||
"items.variant.allow_backorder",
|
"items.variant.allow_backorder",
|
||||||
"items.variant.inventory_items.inventory_item_id",
|
"items.variant.inventory_items.inventory_item_id",
|
||||||
"items.variant.inventory_items.required_quantity",
|
"items.variant.inventory_items.required_quantity",
|
||||||
|
"items.variant.inventory_items.inventory.requires_shipping",
|
||||||
"items.variant.inventory_items.inventory.location_levels.stock_locations.id",
|
"items.variant.inventory_items.inventory.location_levels.stock_locations.id",
|
||||||
"items.variant.inventory_items.inventory.location_levels.stock_locations.name",
|
"items.variant.inventory_items.inventory.location_levels.stock_locations.name",
|
||||||
"items.variant.inventory_items.inventory.location_levels.stock_locations.sales_channels.id",
|
"items.variant.inventory_items.inventory.location_levels.stock_locations.sales_channels.id",
|
||||||
@@ -116,6 +117,7 @@ export const productVariantsFields = [
|
|||||||
"calculated_price.is_calculated_price_tax_inclusive",
|
"calculated_price.is_calculated_price_tax_inclusive",
|
||||||
"inventory_items.inventory_item_id",
|
"inventory_items.inventory_item_id",
|
||||||
"inventory_items.required_quantity",
|
"inventory_items.required_quantity",
|
||||||
|
"inventory_items.inventory.requires_shipping",
|
||||||
"inventory_items.inventory.location_levels.stock_locations.id",
|
"inventory_items.inventory.location_levels.stock_locations.id",
|
||||||
"inventory_items.inventory.location_levels.stock_locations.name",
|
"inventory_items.inventory.location_levels.stock_locations.name",
|
||||||
"inventory_items.inventory.location_levels.stock_locations.sales_channels.id",
|
"inventory_items.inventory.location_levels.stock_locations.sales_channels.id",
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ import {
|
|||||||
CartLineItemDTO,
|
CartLineItemDTO,
|
||||||
CreateOrderAdjustmentDTO,
|
CreateOrderAdjustmentDTO,
|
||||||
CreateOrderLineItemTaxLineDTO,
|
CreateOrderLineItemTaxLineDTO,
|
||||||
|
InventoryItemDTO,
|
||||||
ProductVariantDTO,
|
ProductVariantDTO,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
|
import { isDefined } from "@medusajs/utils"
|
||||||
|
|
||||||
interface Input {
|
interface Input {
|
||||||
item?: CartLineItemDTO
|
item?: CartLineItemDTO
|
||||||
@@ -12,7 +14,9 @@ interface Input {
|
|||||||
metadata?: Record<string, any>
|
metadata?: Record<string, any>
|
||||||
unitPrice: BigNumberInput
|
unitPrice: BigNumberInput
|
||||||
isTaxInclusive?: boolean
|
isTaxInclusive?: boolean
|
||||||
variant: ProductVariantDTO
|
variant: ProductVariantDTO & {
|
||||||
|
inventory_items: { inventory: InventoryItemDTO }[]
|
||||||
|
}
|
||||||
taxLines?: CreateOrderLineItemTaxLineDTO[]
|
taxLines?: CreateOrderLineItemTaxLineDTO[]
|
||||||
adjustments?: CreateOrderAdjustmentDTO[]
|
adjustments?: CreateOrderAdjustmentDTO[]
|
||||||
cartId?: string
|
cartId?: string
|
||||||
@@ -35,6 +39,19 @@ export function prepareLineItemData(data: Input) {
|
|||||||
throw new Error("Variant does not have a product")
|
throw new Error("Variant does not have a product")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note: If any of the items require shipping, we enable fulfillment
|
||||||
|
// unless explicitly set to not require shipping by the item in the request
|
||||||
|
const { inventory_items: inventoryItems } = variant
|
||||||
|
const someInventoryRequiresShipping = inventoryItems.length
|
||||||
|
? inventoryItems.some(
|
||||||
|
(inventoryItem) => !!inventoryItem.inventory.requires_shipping
|
||||||
|
)
|
||||||
|
: true
|
||||||
|
|
||||||
|
const requiresShipping = isDefined(item?.requires_shipping)
|
||||||
|
? item.requires_shipping
|
||||||
|
: someInventoryRequiresShipping
|
||||||
|
|
||||||
const lineItem: any = {
|
const lineItem: any = {
|
||||||
quantity,
|
quantity,
|
||||||
title: variant.title ?? item?.title,
|
title: variant.title ?? item?.title,
|
||||||
@@ -58,7 +75,7 @@ export function prepareLineItemData(data: Input) {
|
|||||||
variant_option_values: item?.variant_option_values,
|
variant_option_values: item?.variant_option_values,
|
||||||
|
|
||||||
is_discountable: variant.product.discountable ?? item?.is_discountable,
|
is_discountable: variant.product.discountable ?? item?.is_discountable,
|
||||||
requires_shipping: variant.requires_shipping ?? item?.requires_shipping,
|
requires_shipping: requiresShipping,
|
||||||
|
|
||||||
unit_price: unitPrice,
|
unit_price: unitPrice,
|
||||||
is_tax_inclusive: !!isTaxInclusive,
|
is_tax_inclusive: !!isTaxInclusive,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const productVariantsFields = [
|
|||||||
"calculated_price.calculated_amount",
|
"calculated_price.calculated_amount",
|
||||||
"inventory_items.inventory_item_id",
|
"inventory_items.inventory_item_id",
|
||||||
"inventory_items.required_quantity",
|
"inventory_items.required_quantity",
|
||||||
|
"inventory_items.inventory.requires_shipping",
|
||||||
"inventory_items.inventory.location_levels.stock_locations.id",
|
"inventory_items.inventory.location_levels.stock_locations.id",
|
||||||
"inventory_items.inventory.location_levels.stock_locations.name",
|
"inventory_items.inventory.location_levels.stock_locations.name",
|
||||||
"inventory_items.inventory.location_levels.stock_locations.sales_channels.id",
|
"inventory_items.inventory.location_levels.stock_locations.sales_channels.id",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
OrderChangeDTO,
|
OrderChangeDTO,
|
||||||
OrderDTO,
|
OrderDTO,
|
||||||
|
OrderLineItemDTO,
|
||||||
OrderWorkflow,
|
OrderWorkflow,
|
||||||
ReturnDTO,
|
ReturnDTO,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
@@ -41,6 +42,37 @@ export function throwIfItemsDoesNotExistsInOrder({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function throwIfItemsAreNotGroupedByShippingRequirement({
|
||||||
|
order,
|
||||||
|
inputItems,
|
||||||
|
}: {
|
||||||
|
order: Pick<OrderDTO, "id" | "items">
|
||||||
|
inputItems: OrderWorkflow.CreateOrderFulfillmentWorkflowInput["items"]
|
||||||
|
}) {
|
||||||
|
const itemsWithShipping: string[] = []
|
||||||
|
const itemsWithoutShipping: string[] = []
|
||||||
|
const orderItemsMap = new Map<string, OrderLineItemDTO>(
|
||||||
|
(order.items || []).map((item) => [item.id, item])
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const inputItem of inputItems) {
|
||||||
|
const orderItem = orderItemsMap.get(inputItem.id)!
|
||||||
|
|
||||||
|
if (orderItem.requires_shipping) {
|
||||||
|
itemsWithShipping.push(orderItem.id)
|
||||||
|
} else {
|
||||||
|
itemsWithoutShipping.push(orderItem.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemsWithShipping.length && itemsWithoutShipping.length) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`Fulfillment can only be created entirely with items with shipping or items without shipping. Split this request into 2 fulfillments.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function throwIfIsCancelled(
|
export function throwIfIsCancelled(
|
||||||
obj: unknown & { id: string; canceled_at?: any },
|
obj: unknown & { id: string; canceled_at?: any },
|
||||||
type: string
|
type: string
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
} from "../../reservation"
|
} from "../../reservation"
|
||||||
import { registerOrderFulfillmentStep } from "../steps"
|
import { registerOrderFulfillmentStep } from "../steps"
|
||||||
import {
|
import {
|
||||||
|
throwIfItemsAreNotGroupedByShippingRequirement,
|
||||||
throwIfItemsDoesNotExistsInOrder,
|
throwIfItemsDoesNotExistsInOrder,
|
||||||
throwIfOrderIsCancelled,
|
throwIfOrderIsCancelled,
|
||||||
} from "../utils/order-validation"
|
} from "../utils/order-validation"
|
||||||
@@ -35,18 +36,16 @@ import {
|
|||||||
*/
|
*/
|
||||||
export const createFulfillmentValidateOrder = createStep(
|
export const createFulfillmentValidateOrder = createStep(
|
||||||
"create-fulfillment-validate-order",
|
"create-fulfillment-validate-order",
|
||||||
(
|
({
|
||||||
{
|
|
||||||
order,
|
order,
|
||||||
inputItems,
|
inputItems,
|
||||||
}: {
|
}: {
|
||||||
order: OrderDTO
|
order: OrderDTO
|
||||||
inputItems: OrderWorkflow.CreateOrderFulfillmentWorkflowInput["items"]
|
inputItems: OrderWorkflow.CreateOrderFulfillmentWorkflowInput["items"]
|
||||||
},
|
}) => {
|
||||||
context
|
|
||||||
) => {
|
|
||||||
throwIfOrderIsCancelled({ order })
|
throwIfOrderIsCancelled({ order })
|
||||||
throwIfItemsDoesNotExistsInOrder({ order, inputItems })
|
throwIfItemsDoesNotExistsInOrder({ order, inputItems })
|
||||||
|
throwIfItemsAreNotGroupedByShippingRequirement({ order, inputItems })
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -91,16 +90,29 @@ function prepareFulfillmentData({
|
|||||||
reservations: ReservationItemDTO[]
|
reservations: ReservationItemDTO[]
|
||||||
itemsList?: OrderLineItemDTO[]
|
itemsList?: OrderLineItemDTO[]
|
||||||
}) {
|
}) {
|
||||||
const inputItems = input.items
|
const fulfillableItems = input.items
|
||||||
const orderItemsMap = new Map<string, Required<OrderDTO>["items"][0]>(
|
const orderItemsMap = new Map<string, Required<OrderDTO>["items"][0]>(
|
||||||
(itemsList ?? order.items)!.map((i) => [i.id, i])
|
(itemsList ?? order.items)!.map((i) => [i.id, i])
|
||||||
)
|
)
|
||||||
|
|
||||||
const reservationItemMap = new Map<string, ReservationItemDTO>(
|
const reservationItemMap = new Map<string, ReservationItemDTO>(
|
||||||
reservations.map((r) => [r.line_item_id as string, r])
|
reservations.map((r) => [r.line_item_id as string, r])
|
||||||
)
|
)
|
||||||
const fulfillmentItems = inputItems.map((i) => {
|
|
||||||
|
// Note: If any of the items require shipping, we enable fulfillment
|
||||||
|
// unless explicitly set to not require shipping by the item in the request
|
||||||
|
const someItemsRequireShipping = fulfillableItems.length
|
||||||
|
? fulfillableItems.some((item) => {
|
||||||
|
const orderItem = orderItemsMap.get(item.id)!
|
||||||
|
|
||||||
|
return orderItem.requires_shipping
|
||||||
|
})
|
||||||
|
: true
|
||||||
|
|
||||||
|
const fulfillmentItems = fulfillableItems.map((i) => {
|
||||||
const orderItem = orderItemsMap.get(i.id)!
|
const orderItem = orderItemsMap.get(i.id)!
|
||||||
const reservation = reservationItemMap.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,
|
inventory_item_id: reservation?.inventory_item_id,
|
||||||
@@ -134,6 +146,7 @@ function prepareFulfillmentData({
|
|||||||
shipping_option_id: shippingOption.id,
|
shipping_option_id: shippingOption.id,
|
||||||
data: shippingMethod.data,
|
data: shippingMethod.data,
|
||||||
items: fulfillmentItems,
|
items: fulfillmentItems,
|
||||||
|
requires_shipping: someItemsRequireShipping,
|
||||||
labels: input.labels ?? [],
|
labels: input.labels ?? [],
|
||||||
delivery_address: shippingAddress as any,
|
delivery_address: shippingAddress as any,
|
||||||
packed_at: new Date(),
|
packed_at: new Date(),
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
parallelize,
|
parallelize,
|
||||||
transform,
|
transform,
|
||||||
} from "@medusajs/workflows-sdk"
|
} from "@medusajs/workflows-sdk"
|
||||||
import { useRemoteQueryStep } from "../../common"
|
|
||||||
import { findOneOrAnyRegionStep } from "../../cart/steps/find-one-or-any-region"
|
import { findOneOrAnyRegionStep } from "../../cart/steps/find-one-or-any-region"
|
||||||
import { findOrCreateCustomerStep } from "../../cart/steps/find-or-create-customer"
|
import { findOrCreateCustomerStep } from "../../cart/steps/find-or-create-customer"
|
||||||
import { findSalesChannelStep } from "../../cart/steps/find-sales-channel"
|
import { findSalesChannelStep } from "../../cart/steps/find-sales-channel"
|
||||||
@@ -16,6 +15,7 @@ import { getVariantPriceSetsStep } from "../../cart/steps/get-variant-price-sets
|
|||||||
import { validateVariantPricesStep } from "../../cart/steps/validate-variant-prices"
|
import { validateVariantPricesStep } from "../../cart/steps/validate-variant-prices"
|
||||||
import { prepareLineItemData } from "../../cart/utils/prepare-line-item-data"
|
import { prepareLineItemData } from "../../cart/utils/prepare-line-item-data"
|
||||||
import { confirmVariantInventoryWorkflow } from "../../cart/workflows/confirm-variant-inventory"
|
import { confirmVariantInventoryWorkflow } from "../../cart/workflows/confirm-variant-inventory"
|
||||||
|
import { useRemoteQueryStep } from "../../common"
|
||||||
import { createOrdersStep } from "../steps"
|
import { createOrdersStep } from "../steps"
|
||||||
import { productVariantsFields } from "../utils/fields"
|
import { productVariantsFields } from "../utils/fields"
|
||||||
import { prepareCustomLineItemData } from "../utils/prepare-custom-line-item-data"
|
import { prepareCustomLineItemData } from "../utils/prepare-custom-line-item-data"
|
||||||
|
|||||||
@@ -75,6 +75,11 @@ export interface FulfillmentDTO {
|
|||||||
*/
|
*/
|
||||||
shipping_option: ShippingOptionDTO | null
|
shipping_option: ShippingOptionDTO | null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag to indidcate whether shipping is required
|
||||||
|
*/
|
||||||
|
requires_shipping: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The associated fulfillment provider.
|
* The associated fulfillment provider.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -52,6 +52,11 @@ export interface CreateFulfillmentDTO {
|
|||||||
*/
|
*/
|
||||||
shipping_option_id?: string | null
|
shipping_option_id?: string | null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag to indicate whether shipping is required
|
||||||
|
*/
|
||||||
|
requires_shipping?: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holds custom data in key-value pairs.
|
* Holds custom data in key-value pairs.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export interface AdminOrder extends BaseOrder {
|
|||||||
customer?: AdminCustomer
|
customer?: AdminCustomer
|
||||||
shipping_address?: AdminOrderAddress | null
|
shipping_address?: AdminOrderAddress | null
|
||||||
billing_address?: AdminOrderAddress | null
|
billing_address?: AdminOrderAddress | null
|
||||||
|
items: AdminOrderLineItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdminOrderLineItem extends BaseOrderLineItem {
|
export interface AdminOrderLineItem extends BaseOrderLineItem {
|
||||||
|
|||||||
@@ -240,6 +240,7 @@ export interface BaseOrderFulfillment {
|
|||||||
shipped_at: Date | null
|
shipped_at: Date | null
|
||||||
delivered_at: Date | null
|
delivered_at: Date | null
|
||||||
canceled_at: Date | null
|
canceled_at: Date | null
|
||||||
|
requires_shipping: boolean
|
||||||
data: Record<string, unknown> | null
|
data: Record<string, unknown> | null
|
||||||
provider_id: string
|
provider_id: string
|
||||||
shipping_option_id: string | null
|
shipping_option_id: string | null
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createOrderFulfillmentWorkflow } from "@medusajs/core-flows"
|
import { createOrderFulfillmentWorkflow } from "@medusajs/core-flows"
|
||||||
|
import { AdditionalData, HttpTypes } from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
ContainerRegistrationKeys,
|
ContainerRegistrationKeys,
|
||||||
remoteQueryObjectFromString,
|
remoteQueryObjectFromString,
|
||||||
@@ -8,7 +9,6 @@ import {
|
|||||||
MedusaResponse,
|
MedusaResponse,
|
||||||
} from "../../../../../types/routing"
|
} from "../../../../../types/routing"
|
||||||
import { AdminOrderCreateFulfillmentType } from "../../validators"
|
import { AdminOrderCreateFulfillmentType } from "../../validators"
|
||||||
import { AdditionalData, HttpTypes } from "@medusajs/types"
|
|
||||||
|
|
||||||
export const POST = async (
|
export const POST = async (
|
||||||
req: AuthenticatedMedusaRequest<
|
req: AuthenticatedMedusaRequest<
|
||||||
@@ -18,20 +18,16 @@ export const POST = async (
|
|||||||
) => {
|
) => {
|
||||||
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
||||||
|
|
||||||
const variables = { id: req.params.id }
|
await createOrderFulfillmentWorkflow(req.scope).run({
|
||||||
|
input: {
|
||||||
const input = {
|
|
||||||
...req.validatedBody,
|
...req.validatedBody,
|
||||||
order_id: req.params.id,
|
order_id: req.params.id,
|
||||||
}
|
},
|
||||||
|
|
||||||
await createOrderFulfillmentWorkflow(req.scope).run({
|
|
||||||
input,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const queryObject = remoteQueryObjectFromString({
|
const queryObject = remoteQueryObjectFromString({
|
||||||
entryPoint: "order",
|
entryPoint: "order",
|
||||||
variables,
|
variables: { id: req.params.id },
|
||||||
fields: req.remoteQueryConfig.fields,
|
fields: req.remoteQueryConfig.fields,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export const defaultStoreCartFields = [
|
|||||||
"items.variant_sku",
|
"items.variant_sku",
|
||||||
"items.variant_barcode",
|
"items.variant_barcode",
|
||||||
"items.variant_title",
|
"items.variant_title",
|
||||||
|
"items.requires_shipping",
|
||||||
"items.metadata",
|
"items.metadata",
|
||||||
"items.created_at",
|
"items.created_at",
|
||||||
"items.updated_at",
|
"items.updated_at",
|
||||||
|
|||||||
@@ -1359,6 +1359,16 @@
|
|||||||
"nullable": true,
|
"nullable": true,
|
||||||
"mappedType": "text"
|
"mappedType": "text"
|
||||||
},
|
},
|
||||||
|
"requires_shipping": {
|
||||||
|
"name": "requires_shipping",
|
||||||
|
"type": "boolean",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": false,
|
||||||
|
"default": "true",
|
||||||
|
"mappedType": "boolean"
|
||||||
|
},
|
||||||
"created_at": {
|
"created_at": {
|
||||||
"name": "created_at",
|
"name": "created_at",
|
||||||
"type": "timestamptz",
|
"type": "timestamptz",
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Migration } from "@mikro-orm/migrations"
|
||||||
|
|
||||||
|
export class Migration20240917161003 extends Migration {
|
||||||
|
async up(): Promise<void> {
|
||||||
|
this.addSql(
|
||||||
|
'alter table if exists "fulfillment" add column if not exists "requires_shipping" boolean not null default true;'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(): Promise<void> {
|
||||||
|
this.addSql(
|
||||||
|
'alter table if exists "fulfillment" drop column if exists "requires_shipping";'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -135,6 +135,9 @@ export default class Fulfillment {
|
|||||||
})
|
})
|
||||||
delivery_address!: Rel<FulfillmentAddress>
|
delivery_address!: Rel<FulfillmentAddress>
|
||||||
|
|
||||||
|
@Property({ columnType: "boolean", default: true })
|
||||||
|
requires_shipping: boolean = true
|
||||||
|
|
||||||
@OneToMany(() => FulfillmentItem, (item) => item.fulfillment, {
|
@OneToMany(() => FulfillmentItem, (item) => item.fulfillment, {
|
||||||
cascade: [Cascade.PERSIST, "soft-remove"] as any,
|
cascade: [Cascade.PERSIST, "soft-remove"] as any,
|
||||||
orphanRemoval: true,
|
orphanRemoval: true,
|
||||||
|
|||||||
Reference in new issue
Block a user