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:
9
.changeset/thin-cars-sit.md
Normal file
9
.changeset/thin-cars-sit.md
Normal file
@@ -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
|
||||
119
integration-tests/http/__tests__/fixtures/shipping.ts
Normal file
119
integration-tests/http/__tests__/fixtures/shipping.ts
Normal file
@@ -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"
|
||||
import { setupTaxStructure } from "../../../../modules/__tests__/fixtures"
|
||||
import { createOrderSeeder } from "../../fixtures/order"
|
||||
import { createShippingOptionSeeder } from "../../fixtures/shipping"
|
||||
import { AdminShippingOption } from "@medusajs/types"
|
||||
|
||||
jest.setTimeout(300000)
|
||||
|
||||
@@ -73,7 +75,10 @@ medusaIntegrationTestRunner({
|
||||
})
|
||||
|
||||
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).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).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).toEqual([
|
||||
@@ -107,16 +118,10 @@ medusaIntegrationTestRunner({
|
||||
})
|
||||
|
||||
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)
|
||||
|
||||
expect(response.data.orders).toHaveLength(1)
|
||||
expect(response.data.orders).toEqual([
|
||||
expect.objectContaining({
|
||||
id: order.id,
|
||||
}),
|
||||
])
|
||||
|
||||
response = await api.get(`/admin/orders?fields=+billing_address.address_1,+billing_address.address_2&q=${order.billing_address.address_1}`, 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).toEqual([
|
||||
@@ -125,12 +130,27 @@ medusaIntegrationTestRunner({
|
||||
}),
|
||||
])
|
||||
|
||||
response = await api.get(`/admin/orders?q=${order.billing_address.address_2}`, 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).toEqual([
|
||||
expect.objectContaining({
|
||||
id: order.id,
|
||||
id: order.id,
|
||||
}),
|
||||
])
|
||||
|
||||
response = await api.get(
|
||||
`/admin/orders?q=${order.billing_address.address_2}`,
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(response.data.orders).toHaveLength(1)
|
||||
expect(response.data.orders).toEqual([
|
||||
expect.objectContaining({
|
||||
id: order.id,
|
||||
}),
|
||||
])
|
||||
})
|
||||
@@ -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", () => {
|
||||
beforeEach(async () => {
|
||||
seeder = await createOrderSeeder({ api, container: getContainer() })
|
||||
|
||||
@@ -18,6 +18,7 @@ const _orderKeys = queryKeysFactory(ORDERS_QUERY_KEY) as TQueryKey<"orders"> & {
|
||||
preview: (orderId: string) => any
|
||||
changes: (orderId: string) => any
|
||||
lineItems: (orderId: string) => any
|
||||
shippingOptions: (orderId: string) => any
|
||||
}
|
||||
|
||||
_orderKeys.preview = function (id: string) {
|
||||
@@ -32,6 +33,10 @@ _orderKeys.lineItems = function (id: string) {
|
||||
return [this.detail(id), "lineItems"]
|
||||
}
|
||||
|
||||
_orderKeys.shippingOptions = function (id: string) {
|
||||
return [this.detail(id), "shippingOptions"]
|
||||
}
|
||||
|
||||
export const ordersQueryKeys = _orderKeys
|
||||
|
||||
export const useOrder = (
|
||||
@@ -125,6 +130,28 @@ export const useOrders = (
|
||||
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 = (
|
||||
id: string,
|
||||
query?: HttpTypes.AdminOrderChangesFilters,
|
||||
|
||||
@@ -24,13 +24,14 @@ import {
|
||||
useRemoveClaimOutboundItem,
|
||||
useUpdateClaimOutboundItems,
|
||||
} from "../../../../../hooks/api/claims"
|
||||
import { useShippingOptions } from "../../../../../hooks/api/shipping-options"
|
||||
import { sdk } from "../../../../../lib/client"
|
||||
import { OutboundShippingPlaceholder } from "../../../common/placeholders"
|
||||
import { AddClaimOutboundItemsTable } from "../add-claim-outbound-items-table"
|
||||
import { ClaimOutboundItem } from "./claim-outbound-item"
|
||||
import { ItemPlaceholder } from "./item-placeholder"
|
||||
import { CreateClaimSchemaType } from "./schema"
|
||||
import { useOrderShippingOptions } from "../../../../../hooks/api/orders"
|
||||
import { getFormattedShippingOptionLocationName } from "../../../../../lib/shipping-options"
|
||||
|
||||
type ClaimOutboundSectionProps = {
|
||||
order: AdminOrder
|
||||
@@ -58,16 +59,12 @@ export const ClaimOutboundSection = ({
|
||||
/**
|
||||
* HOOKS
|
||||
*/
|
||||
const { shipping_options = [] } = useShippingOptions({
|
||||
limit: 999,
|
||||
fields: "*prices,+service_zone.fulfillment_set.location.id",
|
||||
})
|
||||
const { shipping_options = [] } = useOrderShippingOptions(order.id)
|
||||
|
||||
// TODO: filter in the API when boolean filter is supported and fulfillment module support partial rule SO filtering
|
||||
const outboundShippingOptions = shipping_options.filter(
|
||||
(shippingOption) =>
|
||||
!!shippingOption.rules.find(
|
||||
(r) => r.attribute === "is_return" && r.value === "false"
|
||||
)
|
||||
(so) =>
|
||||
!so.rules?.find((r) => r.attribute === "is_return" && r.value === "true")
|
||||
)
|
||||
|
||||
const { mutateAsync: addOutboundShipping } = useAddClaimOutboundShipping(
|
||||
@@ -415,7 +412,7 @@ export const ClaimOutboundSection = ({
|
||||
}}
|
||||
{...field}
|
||||
options={outboundShippingOptions.map((so) => ({
|
||||
label: so.name,
|
||||
label: `${so.name} (${getFormattedShippingOptionLocationName(so)})`,
|
||||
value: so.id,
|
||||
}))}
|
||||
disabled={!outboundShippingOptions.length}
|
||||
|
||||
@@ -23,13 +23,14 @@ import {
|
||||
useRemoveExchangeOutboundItem,
|
||||
useUpdateExchangeOutboundItems,
|
||||
} from "../../../../../hooks/api/exchanges"
|
||||
import { useShippingOptions } from "../../../../../hooks/api/shipping-options"
|
||||
import { sdk } from "../../../../../lib/client"
|
||||
import { OutboundShippingPlaceholder } from "../../../common/placeholders"
|
||||
import { ItemPlaceholder } from "../../../order-create-claim/components/claim-create-form/item-placeholder"
|
||||
import { AddExchangeOutboundItemsTable } from "../add-exchange-outbound-items-table"
|
||||
import { ExchangeOutboundItem } from "./exchange-outbound-item"
|
||||
import { useOrderShippingOptions } from "../../../../../hooks/api/orders"
|
||||
import { CreateExchangeSchemaType } from "./schema"
|
||||
import { getFormattedShippingOptionLocationName } from "../../../../../lib/shipping-options"
|
||||
|
||||
type ExchangeOutboundSectionProps = {
|
||||
order: AdminOrder
|
||||
@@ -57,16 +58,12 @@ export const ExchangeOutboundSection = ({
|
||||
/**
|
||||
* HOOKS
|
||||
*/
|
||||
const { shipping_options = [] } = useShippingOptions({
|
||||
limit: 999,
|
||||
fields: "*prices,+service_zone.fulfillment_set.location.id",
|
||||
})
|
||||
const { shipping_options = [] } = useOrderShippingOptions(order.id)
|
||||
|
||||
// TODO: filter in the API when boolean filter is supported and fulfillment module support partial rule SO filtering
|
||||
const outboundShippingOptions = shipping_options.filter(
|
||||
(shippingOption) =>
|
||||
!!shippingOption.rules.find(
|
||||
(r) => r.attribute === "is_return" && r.value === "false"
|
||||
)
|
||||
(so) =>
|
||||
!so.rules?.find((r) => r.attribute === "is_return" && r.value === "true")
|
||||
)
|
||||
|
||||
const { mutateAsync: addOutboundShipping } = useAddExchangeOutboundShipping(
|
||||
@@ -424,7 +421,7 @@ export const ExchangeOutboundSection = ({
|
||||
}}
|
||||
{...field}
|
||||
options={outboundShippingOptions.map((so) => ({
|
||||
label: so.name,
|
||||
label: `${so.name} (${getFormattedShippingOptionLocationName(so)})`,
|
||||
value: so.id,
|
||||
}))}
|
||||
disabled={!outboundShippingOptions.length}
|
||||
|
||||
@@ -43,11 +43,11 @@ export const listShippingOptionsForCartWorkflowId =
|
||||
* @summary
|
||||
*
|
||||
* List a cart's shipping options.
|
||||
*
|
||||
*
|
||||
* @property hooks.setPricingContext - This hook is executed before the shipping options are retrieved. You can consume this hook to return any custom context useful for the prices retrieval of shipping options.
|
||||
*
|
||||
*
|
||||
* For example, assuming you have the following custom pricing rule:
|
||||
*
|
||||
*
|
||||
* ```json
|
||||
* {
|
||||
* "attribute": "location_id",
|
||||
@@ -55,13 +55,13 @@ export const listShippingOptionsForCartWorkflowId =
|
||||
* "value": "sloc_123",
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* You can consume the `setPricingContext` hook to add the `location_id` context to the prices calculation:
|
||||
*
|
||||
*
|
||||
* ```ts
|
||||
* import { listShippingOptionsForCartWorkflow } from "@medusajs/medusa/core-flows";
|
||||
* import { StepResponse } from "@medusajs/workflows-sdk";
|
||||
*
|
||||
*
|
||||
* listShippingOptionsForCartWorkflow.hooks.setPricingContext((
|
||||
* { cart, fulfillmentSetIds, additional_data }, { container }
|
||||
* ) => {
|
||||
@@ -70,13 +70,13 @@ export const listShippingOptionsForCartWorkflowId =
|
||||
* });
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* The shipping options' prices will now be retrieved using the context you return.
|
||||
*
|
||||
*
|
||||
* :::note
|
||||
*
|
||||
*
|
||||
* Learn more about prices calculation context in the [Prices Calculation](https://docs.medusajs.com/resources/commerce-modules/pricing/price-calculation) documentation.
|
||||
*
|
||||
*
|
||||
* :::
|
||||
*/
|
||||
export const listShippingOptionsForCartWorkflow = createWorkflow(
|
||||
|
||||
@@ -88,6 +88,7 @@ export type FetchShippingOptionForOrderWorkflowOutput = ShippingOptionDTO & {
|
||||
is_calculated_price_tax_inclusive: boolean
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -95,7 +96,7 @@ export const fetchShippingOptionsForOrderWorkflowId = "fetch-shipping-option"
|
||||
*
|
||||
* You can use this workflow within your customizations or your own custom workflows, allowing you to wrap custom logic around fetching
|
||||
* shipping options for an order.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* const { result } = await fetchShippingOptionForOrderWorkflow(container)
|
||||
* .run({
|
||||
@@ -114,15 +115,15 @@ export const fetchShippingOptionsForOrderWorkflowId = "fetch-shipping-option"
|
||||
* }
|
||||
* }
|
||||
* })
|
||||
*
|
||||
*
|
||||
* @summary
|
||||
*
|
||||
*
|
||||
* Fetch a shipping option for an order.
|
||||
*
|
||||
*
|
||||
* @property hooks.setPricingContext - This hook is executed before the shipping option is fetched. You can consume this hook to set the pricing context for the shipping option. This is useful when you have custom pricing rules that depend on the context of the order.
|
||||
*
|
||||
*
|
||||
* For example, assuming you have the following custom pricing rule:
|
||||
*
|
||||
*
|
||||
* ```json
|
||||
* {
|
||||
* "attribute": "location_id",
|
||||
@@ -130,13 +131,13 @@ export const fetchShippingOptionsForOrderWorkflowId = "fetch-shipping-option"
|
||||
* "value": "sloc_123",
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* You can consume the `setPricingContext` hook to add the `location_id` context to the prices calculation:
|
||||
*
|
||||
*
|
||||
* ```ts
|
||||
* import { fetchShippingOptionForOrderWorkflow } from "@medusajs/medusa/core-flows";
|
||||
* import { StepResponse } from "@medusajs/workflows-sdk";
|
||||
*
|
||||
*
|
||||
* fetchShippingOptionForOrderWorkflow.hooks.setPricingContext((
|
||||
* { shipping_option_id, currency_code, order_id, context, additional_data }, { container }
|
||||
* ) => {
|
||||
@@ -145,13 +146,13 @@ export const fetchShippingOptionsForOrderWorkflowId = "fetch-shipping-option"
|
||||
* });
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* The shipping option's price will now be retrieved using the context you return.
|
||||
*
|
||||
*
|
||||
* :::note
|
||||
*
|
||||
*
|
||||
* Learn more about prices calculation context in the [Prices Calculation](https://docs.medusajs.com/resources/commerce-modules/pricing/price-calculation) documentation.
|
||||
*
|
||||
*
|
||||
* :::
|
||||
*
|
||||
* @privateRemarks
|
||||
|
||||
@@ -87,3 +87,4 @@ export * from "./update-order"
|
||||
export * from "./update-order-change-actions"
|
||||
export * from "./update-order-changes"
|
||||
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...
|
||||
*
|
||||
|
||||
@@ -47,15 +47,15 @@ export type ListShippingOptionsForCartWithPricingWorkflowInput = {
|
||||
* Specify the shipping options to retrieve their details and prices.
|
||||
* If not provided, all applicable shipping options are retrieved.
|
||||
*/
|
||||
options?: {
|
||||
options?: {
|
||||
/**
|
||||
* The shipping option's ID.
|
||||
*/
|
||||
id: string;
|
||||
id: string
|
||||
/**
|
||||
* Custom data relevant for the fulfillment provider that processes this shipping option.
|
||||
* It can be data relevant to calculate the shipping option's price.
|
||||
*
|
||||
*
|
||||
* Learn more in [this documentation](https://docs.medusajs.com/resources/commerce-modules/fulfillment/shipping-option#data-property).
|
||||
*/
|
||||
data?: Record<string, unknown>
|
||||
@@ -63,13 +63,13 @@ export type ListShippingOptionsForCartWithPricingWorkflowInput = {
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
*
|
||||
* @defaultValue true
|
||||
*/
|
||||
enabled_in_store?: boolean
|
||||
@@ -124,7 +124,7 @@ export type ListShippingOptionsForCartWithPricingWorkflowOutput = {
|
||||
/**
|
||||
* Custom additional data related to the shipping option, useful for the fulfillment provider
|
||||
* to process the shipping option and calculate its price.
|
||||
*
|
||||
*
|
||||
* Learn more in [this documentation](https://docs.medusajs.com/resources/commerce-modules/fulfillment/shipping-option#data-property).
|
||||
*/
|
||||
data: Record<string, unknown>
|
||||
@@ -132,7 +132,14 @@ export type ListShippingOptionsForCartWithPricingWorkflowOutput = {
|
||||
/**
|
||||
* 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.
|
||||
@@ -155,8 +162,8 @@ export type ListShippingOptionsForCartWithPricingWorkflowOutput = {
|
||||
rules: {
|
||||
/**
|
||||
* The name of a property or table that the rule applies to.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* @example
|
||||
* customer_group
|
||||
*/
|
||||
attribute: string
|
||||
@@ -168,8 +175,8 @@ export type ListShippingOptionsForCartWithPricingWorkflowOutput = {
|
||||
|
||||
/**
|
||||
* The operator of the rule.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* @example
|
||||
* in
|
||||
*/
|
||||
operator: string
|
||||
@@ -232,14 +239,35 @@ export type ListShippingOptionsForCartWorkflowInput = {
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
*
|
||||
* @defaultValue true
|
||||
*/
|
||||
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
|
||||
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,
|
||||
AdminCompleteOrder,
|
||||
AdminCreateOrderCreditLines,
|
||||
AdminGetOrderShippingOptionList,
|
||||
AdminGetOrdersOrderItemsParams,
|
||||
AdminGetOrdersOrderParams,
|
||||
AdminGetOrdersParams,
|
||||
@@ -62,6 +63,16 @@ export const adminOrderRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
method: ["GET"],
|
||||
matcher: "/admin/orders/:id/shipping-options",
|
||||
middlewares: [
|
||||
validateAndTransformQuery(
|
||||
AdminGetOrderShippingOptionList,
|
||||
QueryConfig.listShippingOptionsQueryConfig
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
method: ["GET"],
|
||||
matcher: "/admin/orders/:id/changes",
|
||||
|
||||
@@ -109,3 +109,8 @@ export const listOrderItemsQueryConfig = {
|
||||
defaultLimit: 100,
|
||||
isList: true,
|
||||
}
|
||||
|
||||
export const listShippingOptionsQueryConfig = {
|
||||
defaultLimit: 100,
|
||||
isList: true,
|
||||
}
|
||||
|
||||
@@ -34,6 +34,12 @@ export type AdminGetOrdersOrderItemsParamsType = z.infer<
|
||||
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.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user