fix(medusa,dashboard): don't send price updates for deleted regions/currencies (#9361)

**What**
- Fixes an issue where the admin dashboard would send region prices for deleted regions.
- Also, includes the implementation for `GET /admin/shipping-options/:id` and its corresponding SDK function.

**Why**
- When a region price for a deleted region was sent to the backend it would result in the insert hitting a not null constraint on the currency_code for prices. To avoid this the dashboard should not send region prices for deleted regions. 

**Additional context**
- Prices for deleted regions should ideally not be returned when fetching shipping option prices. However, we don't yet have a mechanism for cleaning up region prices after a region is deleted.

Fixes CC-540
This commit is contained in:
Sebastian Rindom authored and GitHub committed 2024-09-30 11:51:55 +00:00
1 parent 7484cef0fa
commit 852df3f764
8 files changed
+207 -55

No files matched your search

@@ -134,6 +134,105 @@ medusaIntegrationTestRunner({
}) })
}) })
describe("GET /admin/shipping-options/:id", () => {
it("should filters options by stock_location_id", async () => {
const shippingOptionPayload = {
name: "Test shipping option",
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 }],
}
const {
data: { shipping_option: shippingOption },
} = await api.post(
`/admin/shipping-options`,
shippingOptionPayload,
adminHeaders
)
const shippingOptionRes = await api.get(
`/admin/shipping-options/${shippingOption.id}`,
adminHeaders
)
expect(shippingOptionRes.data.shipping_option).toEqual({
id: expect.any(String),
name: "Test shipping option",
price_type: "flat",
prices: [
{
id: expect.any(String),
amount: 1000,
currency_code: "usd",
max_quantity: null,
min_quantity: null,
price_list: null,
price_set_id: expect.any(String),
raw_amount: {
precision: 20,
value: "1000",
},
rules_count: 0,
title: null,
created_at: expect.any(String),
updated_at: expect.any(String),
deleted_at: null,
},
],
provider_id: "manual_test-provider",
provider: {
id: "manual_test-provider",
is_enabled: true,
},
rules: [],
service_zone_id: expect.any(String),
service_zone: {
id: expect.any(String),
name: "Test",
fulfillment_set: {
id: expect.any(String),
},
fulfillment_set_id: expect.any(String),
metadata: null,
created_at: expect.any(String),
updated_at: expect.any(String),
deleted_at: null,
},
shipping_profile_id: expect.any(String),
shipping_profile: {
id: expect.any(String),
metadata: null,
name: "Test",
type: "default",
created_at: expect.any(String),
updated_at: expect.any(String),
deleted_at: null,
},
type: {
code: "test-code",
description: "Test description",
id: expect.any(String),
label: "Test type",
created_at: expect.any(String),
updated_at: expect.any(String),
deleted_at: null,
},
created_at: expect.any(String),
updated_at: expect.any(String),
data: null,
metadata: null,
})
})
})
describe("POST /admin/shipping-options", () => { describe("POST /admin/shipping-options", () => {
it("should throw error when required params are missing", async () => { it("should throw error when required params are missing", async () => {
const shippingOptionPayload = { const shippingOptionPayload = {
@@ -18,24 +18,24 @@ export const shippingOptionsQueryKeys = queryKeysFactory(
SHIPPING_OPTIONS_QUERY_KEY SHIPPING_OPTIONS_QUERY_KEY
) )
// TODO: Endpoint is not implemented yet export const useShippingOption = (
// export const useShippingOption = ( id: string,
// id: string, query?: Record<string, any>,
// options?: UseQueryOptions< options?: UseQueryOptions<
// HttpTypes.AdminShippingOptionResponse, HttpTypes.AdminShippingOptionResponse,
// Error, Error,
// HttpTypes.AdminShippingOptionResponse, HttpTypes.AdminShippingOptionResponse,
// QueryKey QueryKey
// > >
// ) => { ) => {
// const { data, ...rest } = useQuery({ const { data, ...rest } = useQuery({
// queryFn: () => sdk.admin.shippingOption.retrieve(id), queryFn: () => sdk.admin.shippingOption.retrieve(id, query),
// queryKey: shippingOptionsQueryKeys.retrieve(id), queryKey: shippingOptionsQueryKeys.detail(id),
// ...options, ...options,
// }) })
// return { ...data, ...rest } return { ...data, ...rest }
// } }
export const useShippingOptions = ( export const useShippingOptions = (
query?: HttpTypes.AdminShippingOptionListParams, query?: HttpTypes.AdminShippingOptionListParams,
@@ -129,6 +129,11 @@ export function EditShippingOptionsPricingForm({
return undefined return undefined
} }
const currencyExists = currencies.some(currencyCode => currencyCode.toLowerCase() == code.toLowerCase())
if (!currencyExists) {
return undefined
}
const amount = castNumber(value) const amount = castNumber(value)
const priceRecord: PriceRecord = { const priceRecord: PriceRecord = {
@@ -155,6 +160,14 @@ export function EditShippingOptionsPricingForm({
return undefined return undefined
} }
// Check if the region_id exists in the regions array to avoid
// sending updates of region prices where the region has been
// deleted
const regionExists = regions?.some(region => region.id === region_id)
if (!regionExists) {
return undefined
}
const amount = castNumber(value) const amount = castNumber(value)
const priceRecord: PriceRecord = { const priceRecord: PriceRecord = {
@@ -1,25 +1,24 @@
import { json, useParams } from "react-router-dom" import { json, useParams } from "react-router-dom"
import { RouteFocusModal } from "../../../components/modals" import { RouteFocusModal } from "../../../components/modals"
import { useShippingOptions } from "../../../hooks/api/shipping-options" import { useShippingOption } from "../../../hooks/api/shipping-options"
import { EditShippingOptionsPricingForm } from "./components/create-shipping-options-form" import { EditShippingOptionsPricingForm } from "./components/create-shipping-options-form"
export function LocationServiceZoneShippingOptionPricing() { export function LocationServiceZoneShippingOptionPricing() {
const { so_id, location_id } = useParams() const { so_id, location_id } = useParams()
const { shipping_options, isPending, isFetching, isError, error } = if (!so_id) {
useShippingOptions({ throw json({
// TODO: change this when GET option by id endpoint is implemented message: "Shipping Option ID paramater is missing",
id: [so_id!], status: 404,
})
}
const { shipping_option: shippingOption, isError, error } =
useShippingOption(so_id, {
fields: "*prices,*prices.price_rules", fields: "*prices,*prices.price_rules",
}) })
const shippingOption = shipping_options?.find((so) => so.id === so_id)
if (!isPending && !isFetching && !shippingOption) {
throw json(`Shipping option with id: ${so_id} not found`, { status: 404 })
}
if (isError) { if (isError) {
throw error throw error
} }
@@ -24,6 +24,21 @@ export class ShippingOption {
) )
} }
async retrieve(
id: string,
query?: HttpTypes.SelectParams,
headers?: ClientHeaders
) {
return await this.client.fetch<HttpTypes.AdminShippingOptionResponse>(
`/admin/shipping-options/${id}`,
{
method: "GET",
headers,
query,
}
)
}
async update( async update(
id: string, id: string,
body: HttpTypes.AdminUpdateShippingOption, body: HttpTypes.AdminUpdateShippingOption,
@@ -8,7 +8,31 @@ import {
MedusaResponse, MedusaResponse,
} from "../../../../types/routing" } from "../../../../types/routing"
import { refetchShippingOption } from "../helpers" import { refetchShippingOption } from "../helpers"
import { AdminUpdateShippingOptionType } from "../validators" import {
AdminGetShippingOptionParamsType,
AdminUpdateShippingOptionType,
} from "../validators"
import { MedusaError } from "@medusajs/framework/utils"
export const GET = async (
req: AuthenticatedMedusaRequest<AdminGetShippingOptionParamsType>,
res: MedusaResponse<HttpTypes.AdminShippingOptionResponse>
) => {
const shippingOption = await refetchShippingOption(
req.params.id,
req.scope,
req.remoteQueryConfig.fields
)
if (!shippingOption) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Shipping Option with id: ${req.params.id} not found`
)
}
res.json({ shipping_option: shippingOption })
}
export const POST = async ( export const POST = async (
req: AuthenticatedMedusaRequest<AdminUpdateShippingOptionType>, req: AuthenticatedMedusaRequest<AdminUpdateShippingOptionType>,
@@ -6,7 +6,6 @@ import {
import { import {
ContainerRegistrationKeys, ContainerRegistrationKeys,
promiseAll, promiseAll,
remoteQueryObjectFromString,
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
export const refetchShippingOption = async ( export const refetchShippingOption = async (
@@ -14,17 +13,14 @@ export const refetchShippingOption = async (
scope: MedusaContainer, scope: MedusaContainer,
fields: string[] fields: string[]
) => { ) => {
const remoteQuery = scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) const query = scope.resolve(ContainerRegistrationKeys.QUERY)
const queryObject = remoteQueryObjectFromString({ const { data } = await query.graph({
entryPoint: "shipping_option", entity: "shipping_option",
variables: { filters: { id: shippingOptionId },
filters: { id: shippingOptionId },
},
fields: fields, fields: fields,
}) })
const shippingOptions = await remoteQuery(queryObject) return data[0]
return shippingOptions[0]
} }
export const refetchBatchRules = async ( export const refetchBatchRules = async (
@@ -32,32 +28,28 @@ export const refetchBatchRules = async (
scope: MedusaContainer, scope: MedusaContainer,
fields: string[] fields: string[]
) => { ) => {
const remoteQuery = scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) const query = scope.resolve(ContainerRegistrationKeys.QUERY)
let created = Promise.resolve<ShippingOptionRuleDTO[]>([]) let created = Promise.resolve<ShippingOptionRuleDTO[]>([])
let updated = Promise.resolve<ShippingOptionRuleDTO[]>([]) let updated = Promise.resolve<ShippingOptionRuleDTO[]>([])
if (batchResult.created.length) { if (batchResult.created.length) {
const createdQuery = remoteQueryObjectFromString({ created = query
entryPoint: "shipping_option_rule", .graph({
variables: { entity: "shipping_option_rule",
filters: { id: batchResult.created.map((p) => p.id) }, filters: { id: batchResult.created.map((p) => p.id) },
}, fields: fields,
fields: fields, })
}) .then(({ data }) => data)
created = remoteQuery(createdQuery)
} }
if (batchResult.updated.length) { if (batchResult.updated.length) {
const updatedQuery = remoteQueryObjectFromString({ updated = query
entryPoint: "shipping_option_rule", .graph({
variables: { entity: "shipping_option_rule",
filters: { id: batchResult.updated.map((p) => p.id) }, filters: { id: batchResult.updated.map((p) => p.id) },
}, fields: fields,
fields: fields, })
}) .then(({ data }) => data)
updated = remoteQuery(updatedQuery)
} }
const [createdRes, updatedRes] = await promiseAll([created, updated]) const [createdRes, updatedRes] = await promiseAll([created, updated])
@@ -35,6 +35,16 @@ export const adminShippingOptionRoutesMiddlewares: MiddlewareRoute[] = [
}), }),
], ],
}, },
{
method: ["GET"],
matcher: "/admin/shipping-options/:id",
middlewares: [
validateAndTransformQuery(
AdminGetShippingOptionParams,
retrieveTransformQueryConfig
),
],
},
{ {
method: ["POST"], method: ["POST"],
matcher: "/admin/shipping-options", matcher: "/admin/shipping-options",