feat(dashboard, core-flows, js-sdk, types, medusa): listing order's shipping options (#13242)
* feat(dashboard, core-flows,js-sdk,types,medusa): listing order's shipping option * fix: typo * chore: migrate claim form * fix: cleanup rule logic * feat: add test case, rm params * fix: expand location name
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/dashboard": patch
|
||||||
|
"@medusajs/core-flows": patch
|
||||||
|
"@medusajs/js-sdk": patch
|
||||||
|
"@medusajs/types": patch
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(dashboard,core-flows,js-sdk,types,medusa): listing order's shipping option
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import {
|
||||||
|
AdminShippingProfile,
|
||||||
|
AdminStockLocation,
|
||||||
|
AdminSalesChannel,
|
||||||
|
MedusaContainer,
|
||||||
|
} from "@medusajs/types"
|
||||||
|
import { adminHeaders } from "../../../helpers/create-admin-user"
|
||||||
|
|
||||||
|
export async function createShippingOptionSeeder({
|
||||||
|
api,
|
||||||
|
container,
|
||||||
|
salesChannelOverride,
|
||||||
|
stockLocationOverride,
|
||||||
|
shippingProfileOverride,
|
||||||
|
countries = ["us"],
|
||||||
|
}: {
|
||||||
|
api: any
|
||||||
|
container: MedusaContainer
|
||||||
|
salesChannelOverride?: AdminSalesChannel
|
||||||
|
stockLocationOverride?: AdminStockLocation
|
||||||
|
shippingProfileOverride?: AdminShippingProfile
|
||||||
|
countries?: string[]
|
||||||
|
}) {
|
||||||
|
const salesChannel =
|
||||||
|
salesChannelOverride ??
|
||||||
|
(
|
||||||
|
await api.post(
|
||||||
|
"/admin/sales-channels",
|
||||||
|
{ name: "first channel", description: "channel" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.sales_channel
|
||||||
|
|
||||||
|
const stockLocation =
|
||||||
|
stockLocationOverride ??
|
||||||
|
(
|
||||||
|
await api.post(
|
||||||
|
`/admin/stock-locations`,
|
||||||
|
{ name: "test location" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.stock_location
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/stock-locations/${stockLocation.id}/sales-channels`,
|
||||||
|
{ add: [salesChannel.id] },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const shippingProfile =
|
||||||
|
shippingProfileOverride ??
|
||||||
|
(
|
||||||
|
await api.post(
|
||||||
|
`/admin/shipping-profiles`,
|
||||||
|
{ name: `test-${stockLocation.id}`, type: "default" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.shipping_profile
|
||||||
|
|
||||||
|
const fulfillmentSets = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/stock-locations/${stockLocation.id}/fulfillment-sets?fields=*fulfillment_sets`,
|
||||||
|
{
|
||||||
|
name: `Test-${shippingProfile.id}`,
|
||||||
|
type: "test-type",
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.stock_location.fulfillment_sets
|
||||||
|
|
||||||
|
const fulfillmentSet = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/fulfillment-sets/${fulfillmentSets[0].id}/service-zones`,
|
||||||
|
{
|
||||||
|
name: `Test-${shippingProfile.id}`,
|
||||||
|
geo_zones: countries.map((country) => ({
|
||||||
|
type: "country",
|
||||||
|
country_code: country,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.fulfillment_set
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/stock-locations/${stockLocation.id}/fulfillment-providers`,
|
||||||
|
{ add: ["manual_test-provider"] },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const shippingOption = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/shipping-options?fields=+service_zone.fulfillment_set.*,service_zone.geo_zones.*,service_zone.fulfillment_set.location*`,
|
||||||
|
{
|
||||||
|
name: `Test shipping option ${fulfillmentSet.id}`,
|
||||||
|
service_zone_id: fulfillmentSet.service_zones[0].id,
|
||||||
|
shipping_profile_id: shippingProfile.id,
|
||||||
|
provider_id: "manual_test-provider",
|
||||||
|
price_type: "flat",
|
||||||
|
type: {
|
||||||
|
label: "Test type",
|
||||||
|
description: "Test description",
|
||||||
|
code: "test-code",
|
||||||
|
},
|
||||||
|
prices: [{ currency_code: "usd", amount: 1000 }],
|
||||||
|
rules: [],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.shipping_option
|
||||||
|
|
||||||
|
return {
|
||||||
|
salesChannel,
|
||||||
|
stockLocation,
|
||||||
|
shippingProfile,
|
||||||
|
fulfillmentSet,
|
||||||
|
shippingOption,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
} from "../../../../helpers/create-admin-user"
|
} from "../../../../helpers/create-admin-user"
|
||||||
import { setupTaxStructure } from "../../../../modules/__tests__/fixtures"
|
import { setupTaxStructure } from "../../../../modules/__tests__/fixtures"
|
||||||
import { createOrderSeeder } from "../../fixtures/order"
|
import { createOrderSeeder } from "../../fixtures/order"
|
||||||
|
import { createShippingOptionSeeder } from "../../fixtures/shipping"
|
||||||
|
import { AdminShippingOption } from "@medusajs/types"
|
||||||
|
|
||||||
jest.setTimeout(300000)
|
jest.setTimeout(300000)
|
||||||
|
|
||||||
@@ -73,7 +75,10 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should search orders by shipping address", async () => {
|
it("should search orders by shipping address", async () => {
|
||||||
let response = await api.get(`/admin/orders?fields=+shipping_address.address_1,+shipping_address.address_2`, adminHeaders)
|
let response = await api.get(
|
||||||
|
`/admin/orders?fields=+shipping_address.address_1,+shipping_address.address_2`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
expect(response.data.orders).toHaveLength(1)
|
expect(response.data.orders).toHaveLength(1)
|
||||||
expect(response.data.orders).toEqual([
|
expect(response.data.orders).toEqual([
|
||||||
@@ -82,7 +87,10 @@ medusaIntegrationTestRunner({
|
|||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
response = await api.get(`/admin/orders?fields=+shipping_address.address_1,+shipping_address.address_2&q=${order.shipping_address.address_1}`, adminHeaders)
|
response = await api.get(
|
||||||
|
`/admin/orders?fields=+shipping_address.address_1,+shipping_address.address_2&q=${order.shipping_address.address_1}`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
expect(response.data.orders).toHaveLength(1)
|
expect(response.data.orders).toHaveLength(1)
|
||||||
expect(response.data.orders).toEqual([
|
expect(response.data.orders).toEqual([
|
||||||
@@ -91,7 +99,10 @@ medusaIntegrationTestRunner({
|
|||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
response = await api.get(`/admin/orders?q=${order.shipping_address.address_2}`, adminHeaders)
|
response = await api.get(
|
||||||
|
`/admin/orders?q=${order.shipping_address.address_2}`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
expect(response.data.orders).toHaveLength(1)
|
expect(response.data.orders).toHaveLength(1)
|
||||||
expect(response.data.orders).toEqual([
|
expect(response.data.orders).toEqual([
|
||||||
@@ -107,7 +118,10 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should search orders by billing address", async () => {
|
it("should search orders by billing address", async () => {
|
||||||
let response = await api.get(`/admin/orders?fields=+billing_address.address_1,+billing_address.address_2`, adminHeaders)
|
let response = await api.get(
|
||||||
|
`/admin/orders?fields=+billing_address.address_1,+billing_address.address_2`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
expect(response.data.orders).toHaveLength(1)
|
expect(response.data.orders).toHaveLength(1)
|
||||||
expect(response.data.orders).toEqual([
|
expect(response.data.orders).toEqual([
|
||||||
@@ -116,7 +130,10 @@ medusaIntegrationTestRunner({
|
|||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
response = await api.get(`/admin/orders?fields=+billing_address.address_1,+billing_address.address_2&q=${order.billing_address.address_1}`, adminHeaders)
|
response = await api.get(
|
||||||
|
`/admin/orders?fields=+billing_address.address_1,+billing_address.address_2&q=${order.billing_address.address_1}`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
expect(response.data.orders).toHaveLength(1)
|
expect(response.data.orders).toHaveLength(1)
|
||||||
expect(response.data.orders).toEqual([
|
expect(response.data.orders).toEqual([
|
||||||
@@ -125,7 +142,10 @@ medusaIntegrationTestRunner({
|
|||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
response = await api.get(`/admin/orders?q=${order.billing_address.address_2}`, adminHeaders)
|
response = await api.get(
|
||||||
|
`/admin/orders?q=${order.billing_address.address_2}`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
expect(response.data.orders).toHaveLength(1)
|
expect(response.data.orders).toHaveLength(1)
|
||||||
expect(response.data.orders).toEqual([
|
expect(response.data.orders).toEqual([
|
||||||
@@ -2471,6 +2491,76 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("GET /orders/:id/shipping-options", () => {
|
||||||
|
let so1: AdminShippingOption
|
||||||
|
let so2: AdminShippingOption
|
||||||
|
let so3: AdminShippingOption
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
seeder = await createOrderSeeder({ api, container: getContainer() })
|
||||||
|
order = seeder.order
|
||||||
|
order = (await api.get(`/admin/orders/${order.id}`, adminHeaders)).data
|
||||||
|
.order
|
||||||
|
|
||||||
|
so1 = (
|
||||||
|
await createShippingOptionSeeder({
|
||||||
|
api,
|
||||||
|
container: getContainer(),
|
||||||
|
salesChannelOverride: seeder.salesChannel,
|
||||||
|
countries: ["us"],
|
||||||
|
})
|
||||||
|
).shippingOption
|
||||||
|
|
||||||
|
so2 = (
|
||||||
|
await createShippingOptionSeeder({
|
||||||
|
api,
|
||||||
|
container: getContainer(),
|
||||||
|
salesChannelOverride: seeder.salesChannel,
|
||||||
|
countries: ["us", "ca"],
|
||||||
|
})
|
||||||
|
).shippingOption
|
||||||
|
|
||||||
|
so3 = (
|
||||||
|
await createShippingOptionSeeder({
|
||||||
|
api,
|
||||||
|
container: getContainer(),
|
||||||
|
salesChannelOverride: seeder.salesChannel,
|
||||||
|
countries: ["de"],
|
||||||
|
})
|
||||||
|
).shippingOption
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return the shipping options applicable for the order", async () => {
|
||||||
|
const { data } = await api.get(
|
||||||
|
`/admin/orders/${order.id}/shipping-options`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const originalShippingOptionId =
|
||||||
|
order.shipping_methods[0].shipping_option_id
|
||||||
|
|
||||||
|
expect(order.shipping_address.country_code).toEqual("us")
|
||||||
|
|
||||||
|
expect(data.shipping_options.length).toEqual(3)
|
||||||
|
expect(data.shipping_options).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: so1.id,
|
||||||
|
insufficient_inventory: true,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: so2.id,
|
||||||
|
insufficient_inventory: true, // new SO without location levels for the order item, should have insufficient inventory
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: originalShippingOptionId,
|
||||||
|
insufficient_inventory: false, // order is created with this SO, location has to have enough inventory
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("POST /orders/:id/fulfillments/:id/mark-as-delivered", () => {
|
describe("POST /orders/:id/fulfillments/:id/mark-as-delivered", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
seeder = await createOrderSeeder({ api, container: getContainer() })
|
seeder = await createOrderSeeder({ api, container: getContainer() })
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const _orderKeys = queryKeysFactory(ORDERS_QUERY_KEY) as TQueryKey<"orders"> & {
|
|||||||
preview: (orderId: string) => any
|
preview: (orderId: string) => any
|
||||||
changes: (orderId: string) => any
|
changes: (orderId: string) => any
|
||||||
lineItems: (orderId: string) => any
|
lineItems: (orderId: string) => any
|
||||||
|
shippingOptions: (orderId: string) => any
|
||||||
}
|
}
|
||||||
|
|
||||||
_orderKeys.preview = function (id: string) {
|
_orderKeys.preview = function (id: string) {
|
||||||
@@ -32,6 +33,10 @@ _orderKeys.lineItems = function (id: string) {
|
|||||||
return [this.detail(id), "lineItems"]
|
return [this.detail(id), "lineItems"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_orderKeys.shippingOptions = function (id: string) {
|
||||||
|
return [this.detail(id), "shippingOptions"]
|
||||||
|
}
|
||||||
|
|
||||||
export const ordersQueryKeys = _orderKeys
|
export const ordersQueryKeys = _orderKeys
|
||||||
|
|
||||||
export const useOrder = (
|
export const useOrder = (
|
||||||
@@ -125,6 +130,28 @@ export const useOrders = (
|
|||||||
return { ...data, ...rest }
|
return { ...data, ...rest }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useOrderShippingOptions = (
|
||||||
|
id: string,
|
||||||
|
query?: HttpTypes.AdminGetOrderShippingOptionList,
|
||||||
|
options?: Omit<
|
||||||
|
UseQueryOptions<
|
||||||
|
{ shipping_options: HttpTypes.AdminShippingOption[] },
|
||||||
|
FetchError,
|
||||||
|
{ shipping_options: HttpTypes.AdminShippingOption[] },
|
||||||
|
QueryKey
|
||||||
|
>,
|
||||||
|
"queryFn" | "queryKey"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
const { data, ...rest } = useQuery({
|
||||||
|
queryFn: async () => sdk.admin.order.listShippingOptions(id, query),
|
||||||
|
queryKey: ordersQueryKeys.shippingOptions(id),
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
|
||||||
|
return { ...data, ...rest }
|
||||||
|
}
|
||||||
|
|
||||||
export const useOrderChanges = (
|
export const useOrderChanges = (
|
||||||
id: string,
|
id: string,
|
||||||
query?: HttpTypes.AdminOrderChangesFilters,
|
query?: HttpTypes.AdminOrderChangesFilters,
|
||||||
|
|||||||
+7
-10
@@ -24,13 +24,14 @@ import {
|
|||||||
useRemoveClaimOutboundItem,
|
useRemoveClaimOutboundItem,
|
||||||
useUpdateClaimOutboundItems,
|
useUpdateClaimOutboundItems,
|
||||||
} from "../../../../../hooks/api/claims"
|
} from "../../../../../hooks/api/claims"
|
||||||
import { useShippingOptions } from "../../../../../hooks/api/shipping-options"
|
|
||||||
import { sdk } from "../../../../../lib/client"
|
import { sdk } from "../../../../../lib/client"
|
||||||
import { OutboundShippingPlaceholder } from "../../../common/placeholders"
|
import { OutboundShippingPlaceholder } from "../../../common/placeholders"
|
||||||
import { AddClaimOutboundItemsTable } from "../add-claim-outbound-items-table"
|
import { AddClaimOutboundItemsTable } from "../add-claim-outbound-items-table"
|
||||||
import { ClaimOutboundItem } from "./claim-outbound-item"
|
import { ClaimOutboundItem } from "./claim-outbound-item"
|
||||||
import { ItemPlaceholder } from "./item-placeholder"
|
import { ItemPlaceholder } from "./item-placeholder"
|
||||||
import { CreateClaimSchemaType } from "./schema"
|
import { CreateClaimSchemaType } from "./schema"
|
||||||
|
import { useOrderShippingOptions } from "../../../../../hooks/api/orders"
|
||||||
|
import { getFormattedShippingOptionLocationName } from "../../../../../lib/shipping-options"
|
||||||
|
|
||||||
type ClaimOutboundSectionProps = {
|
type ClaimOutboundSectionProps = {
|
||||||
order: AdminOrder
|
order: AdminOrder
|
||||||
@@ -58,16 +59,12 @@ export const ClaimOutboundSection = ({
|
|||||||
/**
|
/**
|
||||||
* HOOKS
|
* HOOKS
|
||||||
*/
|
*/
|
||||||
const { shipping_options = [] } = useShippingOptions({
|
const { shipping_options = [] } = useOrderShippingOptions(order.id)
|
||||||
limit: 999,
|
|
||||||
fields: "*prices,+service_zone.fulfillment_set.location.id",
|
|
||||||
})
|
|
||||||
|
|
||||||
|
// TODO: filter in the API when boolean filter is supported and fulfillment module support partial rule SO filtering
|
||||||
const outboundShippingOptions = shipping_options.filter(
|
const outboundShippingOptions = shipping_options.filter(
|
||||||
(shippingOption) =>
|
(so) =>
|
||||||
!!shippingOption.rules.find(
|
!so.rules?.find((r) => r.attribute === "is_return" && r.value === "true")
|
||||||
(r) => r.attribute === "is_return" && r.value === "false"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const { mutateAsync: addOutboundShipping } = useAddClaimOutboundShipping(
|
const { mutateAsync: addOutboundShipping } = useAddClaimOutboundShipping(
|
||||||
@@ -415,7 +412,7 @@ export const ClaimOutboundSection = ({
|
|||||||
}}
|
}}
|
||||||
{...field}
|
{...field}
|
||||||
options={outboundShippingOptions.map((so) => ({
|
options={outboundShippingOptions.map((so) => ({
|
||||||
label: so.name,
|
label: `${so.name} (${getFormattedShippingOptionLocationName(so)})`,
|
||||||
value: so.id,
|
value: so.id,
|
||||||
}))}
|
}))}
|
||||||
disabled={!outboundShippingOptions.length}
|
disabled={!outboundShippingOptions.length}
|
||||||
|
|||||||
+7
-10
@@ -23,13 +23,14 @@ import {
|
|||||||
useRemoveExchangeOutboundItem,
|
useRemoveExchangeOutboundItem,
|
||||||
useUpdateExchangeOutboundItems,
|
useUpdateExchangeOutboundItems,
|
||||||
} from "../../../../../hooks/api/exchanges"
|
} from "../../../../../hooks/api/exchanges"
|
||||||
import { useShippingOptions } from "../../../../../hooks/api/shipping-options"
|
|
||||||
import { sdk } from "../../../../../lib/client"
|
import { sdk } from "../../../../../lib/client"
|
||||||
import { OutboundShippingPlaceholder } from "../../../common/placeholders"
|
import { OutboundShippingPlaceholder } from "../../../common/placeholders"
|
||||||
import { ItemPlaceholder } from "../../../order-create-claim/components/claim-create-form/item-placeholder"
|
import { ItemPlaceholder } from "../../../order-create-claim/components/claim-create-form/item-placeholder"
|
||||||
import { AddExchangeOutboundItemsTable } from "../add-exchange-outbound-items-table"
|
import { AddExchangeOutboundItemsTable } from "../add-exchange-outbound-items-table"
|
||||||
import { ExchangeOutboundItem } from "./exchange-outbound-item"
|
import { ExchangeOutboundItem } from "./exchange-outbound-item"
|
||||||
|
import { useOrderShippingOptions } from "../../../../../hooks/api/orders"
|
||||||
import { CreateExchangeSchemaType } from "./schema"
|
import { CreateExchangeSchemaType } from "./schema"
|
||||||
|
import { getFormattedShippingOptionLocationName } from "../../../../../lib/shipping-options"
|
||||||
|
|
||||||
type ExchangeOutboundSectionProps = {
|
type ExchangeOutboundSectionProps = {
|
||||||
order: AdminOrder
|
order: AdminOrder
|
||||||
@@ -57,16 +58,12 @@ export const ExchangeOutboundSection = ({
|
|||||||
/**
|
/**
|
||||||
* HOOKS
|
* HOOKS
|
||||||
*/
|
*/
|
||||||
const { shipping_options = [] } = useShippingOptions({
|
const { shipping_options = [] } = useOrderShippingOptions(order.id)
|
||||||
limit: 999,
|
|
||||||
fields: "*prices,+service_zone.fulfillment_set.location.id",
|
|
||||||
})
|
|
||||||
|
|
||||||
|
// TODO: filter in the API when boolean filter is supported and fulfillment module support partial rule SO filtering
|
||||||
const outboundShippingOptions = shipping_options.filter(
|
const outboundShippingOptions = shipping_options.filter(
|
||||||
(shippingOption) =>
|
(so) =>
|
||||||
!!shippingOption.rules.find(
|
!so.rules?.find((r) => r.attribute === "is_return" && r.value === "true")
|
||||||
(r) => r.attribute === "is_return" && r.value === "false"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const { mutateAsync: addOutboundShipping } = useAddExchangeOutboundShipping(
|
const { mutateAsync: addOutboundShipping } = useAddExchangeOutboundShipping(
|
||||||
@@ -424,7 +421,7 @@ export const ExchangeOutboundSection = ({
|
|||||||
}}
|
}}
|
||||||
{...field}
|
{...field}
|
||||||
options={outboundShippingOptions.map((so) => ({
|
options={outboundShippingOptions.map((so) => ({
|
||||||
label: so.name,
|
label: `${so.name} (${getFormattedShippingOptionLocationName(so)})`,
|
||||||
value: so.id,
|
value: so.id,
|
||||||
}))}
|
}))}
|
||||||
disabled={!outboundShippingOptions.length}
|
disabled={!outboundShippingOptions.length}
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ export type FetchShippingOptionForOrderWorkflowOutput = ShippingOptionDTO & {
|
|||||||
is_calculated_price_tax_inclusive: boolean
|
is_calculated_price_tax_inclusive: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetchShippingOptionsForOrderWorkflowId = "fetch-shipping-option"
|
export const fetchShippingOptionsForOrderWorkflowId = "fetch-shipping-option"
|
||||||
/**
|
/**
|
||||||
* This workflow fetches a shipping option for an order. It's used in Return Merchandise Authorization (RMA) flows. It's used
|
* This workflow fetches a shipping option for an order. It's used in Return Merchandise Authorization (RMA) flows. It's used
|
||||||
|
|||||||
@@ -87,3 +87,4 @@ export * from "./update-order"
|
|||||||
export * from "./update-order-change-actions"
|
export * from "./update-order-change-actions"
|
||||||
export * from "./update-order-changes"
|
export * from "./update-order-changes"
|
||||||
export * from "./update-tax-lines"
|
export * from "./update-tax-lines"
|
||||||
|
export * from "./list-shipping-options-for-order"
|
||||||
|
|||||||
@@ -0,0 +1,205 @@
|
|||||||
|
import {
|
||||||
|
createWorkflow,
|
||||||
|
transform,
|
||||||
|
WorkflowData,
|
||||||
|
WorkflowResponse,
|
||||||
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
|
import {
|
||||||
|
AdditionalData,
|
||||||
|
ListShippingOptionsForOrderWorkflowInput,
|
||||||
|
} from "@medusajs/types"
|
||||||
|
|
||||||
|
import { useQueryGraphStep, validatePresenceOfStep } from "../../common"
|
||||||
|
import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
|
||||||
|
|
||||||
|
export const listShippingOptionsForOrderWorkflowId =
|
||||||
|
"list-shipping-options-for-order"
|
||||||
|
/**
|
||||||
|
* This workflow lists the shipping options of an order. It's executed by the
|
||||||
|
* [List Shipping Options Store API Route](https://docs.medusajs.com/api/store#shipping-options_getshippingoptions).
|
||||||
|
*
|
||||||
|
* :::note
|
||||||
|
*
|
||||||
|
* This workflow doesn't retrieve the calculated prices of the shipping options. If you need to retrieve the prices of the shipping options,
|
||||||
|
* use the {@link listShippingOptionsForOrderWithPricingWorkflow} workflow.
|
||||||
|
*
|
||||||
|
* :::
|
||||||
|
*
|
||||||
|
* You can use this workflow within your own customizations or custom workflows, allowing you to wrap custom logic around to retrieve the shipping options of an order
|
||||||
|
* in your custom flows.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { result } = await listShippingOptionsForOrderWorkflow(container)
|
||||||
|
* .run({
|
||||||
|
* input: {
|
||||||
|
* order_id: "order_123",
|
||||||
|
* }
|
||||||
|
* })
|
||||||
|
*
|
||||||
|
* @summary
|
||||||
|
*
|
||||||
|
* List a order's shipping options.
|
||||||
|
*
|
||||||
|
* :::
|
||||||
|
*/
|
||||||
|
export const listShippingOptionsForOrderWorkflow = createWorkflow(
|
||||||
|
listShippingOptionsForOrderWorkflowId,
|
||||||
|
(
|
||||||
|
input: WorkflowData<
|
||||||
|
ListShippingOptionsForOrderWorkflowInput & AdditionalData
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
const orderQuery = useQueryGraphStep({
|
||||||
|
entity: "order",
|
||||||
|
filters: { id: input.order_id },
|
||||||
|
fields: [
|
||||||
|
"id",
|
||||||
|
|
||||||
|
"sales_channel_id",
|
||||||
|
"region_id",
|
||||||
|
"shipping_address.city",
|
||||||
|
"shipping_address.country_code",
|
||||||
|
"shipping_address.province",
|
||||||
|
"shipping_address.postal_code",
|
||||||
|
|
||||||
|
"items.*",
|
||||||
|
"items.variant.manage_inventory",
|
||||||
|
"items.variant.inventory_items.inventory_item_id",
|
||||||
|
"items.variant.inventory_items.inventory.requires_shipping",
|
||||||
|
"items.variant.inventory_items.inventory.location_levels.*",
|
||||||
|
],
|
||||||
|
options: { throwIfKeyNotFound: true },
|
||||||
|
}).config({ name: "get-order" })
|
||||||
|
|
||||||
|
const order = transform(
|
||||||
|
{ orderQuery },
|
||||||
|
({ orderQuery }) => orderQuery.data[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
validatePresenceOfStep({
|
||||||
|
entity: order,
|
||||||
|
fields: ["sales_channel_id", "region_id"],
|
||||||
|
})
|
||||||
|
|
||||||
|
const scFulfillmentSetQuery = useQueryGraphStep({
|
||||||
|
entity: "sales_channels",
|
||||||
|
filters: { id: order.sales_channel_id },
|
||||||
|
fields: [
|
||||||
|
"stock_locations.fulfillment_sets.id",
|
||||||
|
"stock_locations.id",
|
||||||
|
"stock_locations.name",
|
||||||
|
"stock_locations.address.*",
|
||||||
|
],
|
||||||
|
}).config({ name: "sales_channels-fulfillment-query" })
|
||||||
|
|
||||||
|
const scFulfillmentSets = transform(
|
||||||
|
{ scFulfillmentSetQuery },
|
||||||
|
({ scFulfillmentSetQuery }) => scFulfillmentSetQuery.data[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
const { fulfillmentSetIds } = transform(
|
||||||
|
{ scFulfillmentSets },
|
||||||
|
({ scFulfillmentSets }) => {
|
||||||
|
const fulfillmentSetIds = new Set<string>()
|
||||||
|
|
||||||
|
scFulfillmentSets.stock_locations.forEach((stockLocation) => {
|
||||||
|
stockLocation.fulfillment_sets.forEach((fulfillmentSet) => {
|
||||||
|
fulfillmentSetIds.add(fulfillmentSet.id)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
fulfillmentSetIds: Array.from(fulfillmentSetIds),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const queryVariables = transform(
|
||||||
|
{ fulfillmentSetIds, order },
|
||||||
|
({ fulfillmentSetIds, order }) => {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
fulfillment_set_id: fulfillmentSetIds,
|
||||||
|
|
||||||
|
address: {
|
||||||
|
country_code: order.shipping_address?.country_code,
|
||||||
|
province_code: order.shipping_address?.province,
|
||||||
|
city: order.shipping_address?.city,
|
||||||
|
postal_expression: order.shipping_address?.postal_code,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const shippingOptions = useRemoteQueryStep({
|
||||||
|
entry_point: "shipping_options",
|
||||||
|
fields: [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"price_type",
|
||||||
|
"service_zone_id",
|
||||||
|
"shipping_profile_id",
|
||||||
|
"provider_id",
|
||||||
|
"data",
|
||||||
|
"service_zone.fulfillment_set_id",
|
||||||
|
"service_zone.fulfillment_set.type",
|
||||||
|
"service_zone.fulfillment_set.location.id",
|
||||||
|
"service_zone.fulfillment_set.location.name",
|
||||||
|
"service_zone.fulfillment_set.location.address.*",
|
||||||
|
|
||||||
|
"type.id",
|
||||||
|
"type.label",
|
||||||
|
"type.description",
|
||||||
|
"type.code",
|
||||||
|
|
||||||
|
"provider.id",
|
||||||
|
"provider.is_enabled",
|
||||||
|
|
||||||
|
"rules.attribute",
|
||||||
|
"rules.value",
|
||||||
|
"rules.operator",
|
||||||
|
],
|
||||||
|
variables: queryVariables,
|
||||||
|
}).config({ name: "shipping-options-query" })
|
||||||
|
|
||||||
|
const shippingOptionsWithInventory = transform(
|
||||||
|
{ shippingOptions, order },
|
||||||
|
({ shippingOptions, order }) =>
|
||||||
|
shippingOptions.map((shippingOption) => {
|
||||||
|
const locationId =
|
||||||
|
shippingOption.service_zone.fulfillment_set.location.id
|
||||||
|
|
||||||
|
const itemsAtLocationWithoutAvailableQuantity = order.items.filter(
|
||||||
|
(item) => {
|
||||||
|
if (!item.variant?.manage_inventory) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return item.variant.inventory_items.some((inventoryItem) => {
|
||||||
|
if (!inventoryItem.inventory.requires_shipping) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const level = inventoryItem.inventory.location_levels.find(
|
||||||
|
(locationLevel) => {
|
||||||
|
return locationLevel.location_id === locationId
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return !level ? true : level.available_quantity < item.quantity
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...shippingOption,
|
||||||
|
insufficient_inventory:
|
||||||
|
itemsAtLocationWithoutAvailableQuantity.length > 0,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
return new WorkflowResponse(shippingOptionsWithInventory)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -504,6 +504,36 @@ export class Order {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method retrieves a list of shipping options for an order based on the order's shipping address.
|
||||||
|
*
|
||||||
|
* This method sends a request to the [List Shipping Options](https://docs.medusajs.com/api/admin#orders_getordersidshipping-options)
|
||||||
|
* API route.
|
||||||
|
*
|
||||||
|
* @param id - The order's ID.
|
||||||
|
* @param queryParams - Configure the fields to retrieve in each shipping option.
|
||||||
|
* @param headers - Headers to pass in the request
|
||||||
|
* @returns The list of shipping options.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* sdk.admin.order.listShippingOptions("order_123")
|
||||||
|
* .then(({ shipping_options }) => {
|
||||||
|
* console.log(shipping_options)
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
async listShippingOptions(
|
||||||
|
id: string,
|
||||||
|
queryParams?: FindParams & HttpTypes.AdminGetOrderShippingOptionList,
|
||||||
|
headers?: ClientHeaders
|
||||||
|
) {
|
||||||
|
return await this.client.fetch<{
|
||||||
|
shipping_options: HttpTypes.AdminShippingOption[]
|
||||||
|
}>(`/admin/orders/${id}/shipping-options`, {
|
||||||
|
query: queryParams,
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method retrieves a list of changes made on an order, including returns, exchanges, etc...
|
* This method retrieves a list of changes made on an order, including returns, exchanges, etc...
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export type ListShippingOptionsForCartWithPricingWorkflowInput = {
|
|||||||
/**
|
/**
|
||||||
* The shipping option's ID.
|
* The shipping option's ID.
|
||||||
*/
|
*/
|
||||||
id: string;
|
id: string
|
||||||
/**
|
/**
|
||||||
* Custom data relevant for the fulfillment provider that processes this shipping option.
|
* Custom data relevant for the fulfillment provider that processes this shipping option.
|
||||||
* It can be data relevant to calculate the shipping option's price.
|
* It can be data relevant to calculate the shipping option's price.
|
||||||
@@ -132,7 +132,14 @@ export type ListShippingOptionsForCartWithPricingWorkflowOutput = {
|
|||||||
/**
|
/**
|
||||||
* The shipping option's type.
|
* The shipping option's type.
|
||||||
*/
|
*/
|
||||||
type: Omit<ShippingOptionTypeDTO, "shipping_option_id" | "shipping_option" | "created_at" | "updated_at" | "deleted_at">
|
type: Omit<
|
||||||
|
ShippingOptionTypeDTO,
|
||||||
|
| "shipping_option_id"
|
||||||
|
| "shipping_option"
|
||||||
|
| "created_at"
|
||||||
|
| "updated_at"
|
||||||
|
| "deleted_at"
|
||||||
|
>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The associated fulfillment provider details.
|
* The associated fulfillment provider details.
|
||||||
@@ -243,3 +250,24 @@ export type ListShippingOptionsForCartWorkflowInput = {
|
|||||||
*/
|
*/
|
||||||
enabled_in_store?: boolean
|
enabled_in_store?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The context for retrieving the shipping options.
|
||||||
|
*/
|
||||||
|
export type ListShippingOptionsForOrderWorkflowInput = {
|
||||||
|
/**
|
||||||
|
* The order's ID.
|
||||||
|
*/
|
||||||
|
order_id: string
|
||||||
|
/**
|
||||||
|
* Whether to retrieve return shipping options.
|
||||||
|
* By default, non-return shipping options are retrieved.
|
||||||
|
*
|
||||||
|
* @defaultValue false
|
||||||
|
*/
|
||||||
|
is_return?: boolean
|
||||||
|
/**
|
||||||
|
* Whether to retrieve the shipping option's enabled in the store, which is the default.
|
||||||
|
*/
|
||||||
|
enabled_in_store?: boolean
|
||||||
|
}
|
||||||
|
|||||||
@@ -56,3 +56,5 @@ export interface AdminOrderItemsFilters extends FindParams {
|
|||||||
order_id?: string[] | string
|
order_id?: string[] | string
|
||||||
version?: number[] | number
|
version?: number[] | number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdminGetOrderShippingOptionList {}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { listShippingOptionsForOrderWorkflow } from "@medusajs/core-flows"
|
||||||
|
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
||||||
|
import { AdminShippingOption, HttpTypes } from "@medusajs/framework/types"
|
||||||
|
|
||||||
|
export const GET = async (
|
||||||
|
req: MedusaRequest<{}, HttpTypes.AdminGetOrderShippingOptionList>,
|
||||||
|
res: MedusaResponse<{ shipping_options: AdminShippingOption[] }>
|
||||||
|
) => {
|
||||||
|
const { id } = req.params
|
||||||
|
|
||||||
|
const workflow = listShippingOptionsForOrderWorkflow(req.scope)
|
||||||
|
const { result: shipping_options } = await workflow.run({
|
||||||
|
input: {
|
||||||
|
order_id: id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
res.json({ shipping_options })
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
AdminCancelOrderTransferRequest,
|
AdminCancelOrderTransferRequest,
|
||||||
AdminCompleteOrder,
|
AdminCompleteOrder,
|
||||||
AdminCreateOrderCreditLines,
|
AdminCreateOrderCreditLines,
|
||||||
|
AdminGetOrderShippingOptionList,
|
||||||
AdminGetOrdersOrderItemsParams,
|
AdminGetOrdersOrderItemsParams,
|
||||||
AdminGetOrdersOrderParams,
|
AdminGetOrdersOrderParams,
|
||||||
AdminGetOrdersParams,
|
AdminGetOrdersParams,
|
||||||
@@ -62,6 +63,16 @@ export const adminOrderRoutesMiddlewares: MiddlewareRoute[] = [
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
method: ["GET"],
|
||||||
|
matcher: "/admin/orders/:id/shipping-options",
|
||||||
|
middlewares: [
|
||||||
|
validateAndTransformQuery(
|
||||||
|
AdminGetOrderShippingOptionList,
|
||||||
|
QueryConfig.listShippingOptionsQueryConfig
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
method: ["GET"],
|
method: ["GET"],
|
||||||
matcher: "/admin/orders/:id/changes",
|
matcher: "/admin/orders/:id/changes",
|
||||||
|
|||||||
@@ -109,3 +109,8 @@ export const listOrderItemsQueryConfig = {
|
|||||||
defaultLimit: 100,
|
defaultLimit: 100,
|
||||||
isList: true,
|
isList: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const listShippingOptionsQueryConfig = {
|
||||||
|
defaultLimit: 100,
|
||||||
|
isList: true,
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,6 +34,12 @@ export type AdminGetOrdersOrderItemsParamsType = z.infer<
|
|||||||
typeof AdminGetOrdersOrderParams
|
typeof AdminGetOrdersOrderParams
|
||||||
>
|
>
|
||||||
|
|
||||||
|
export const AdminGetOrderShippingOptionList = z.object({})
|
||||||
|
|
||||||
|
export type AdminGetOrderShippingOptionListType = z.infer<
|
||||||
|
typeof AdminGetOrderShippingOptionList
|
||||||
|
>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parameters used to filter and configure the pagination of the retrieved order.
|
* Parameters used to filter and configure the pagination of the retrieved order.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user