Feat: Add tax inclusivity to admin (#8003)

* feat: Add price preference to sdk

* feat: Plug tax inclusivity settings for region in UI

* feat: Add price inclusivity indicator to variant and shipping price table columns

* fix: Rename price title to correct variable name

* feat: Add support for tax inclusive crud on region

* fix: Use the region endpoint for updating tax inclusivity

* chore: Factor out price columns from hooks
This commit is contained in:
Stevche Radevski
2024-07-09 09:26:20 +02:00
committed by GitHub
parent 4b391fc3cf
commit cbf2fcd559
34 changed files with 920 additions and 433 deletions

View File

@@ -8,41 +8,39 @@ jest.setTimeout(30000)
medusaIntegrationTestRunner({
testSuite: ({ dbConnection, getContainer, api }) => {
beforeAll(() => {})
let region1
let region2
beforeEach(async () => {
const container = getContainer()
await createAdminUser(dbConnection, adminHeaders, container)
region1 = (
await api.post(
"/admin/regions",
{
name: "United Kingdom",
currency_code: "gbp",
},
adminHeaders
)
).data.region
region2 = (
await api.post(
"/admin/regions",
{
name: "United States",
currency_code: "usd",
},
adminHeaders
)
).data.region
})
// BREAKING: There is no more `tax_rate` field on the region.
// BREAKING: There are no more fulfillment providers list on a region.
describe("GET /admin/regions", () => {
let region1
let region2
beforeEach(async () => {
region1 = (
await api.post(
"/admin/regions",
{
name: "United Kingdom",
currency_code: "gbp",
},
adminHeaders
)
).data.region
region2 = (
await api.post(
"/admin/regions",
{
name: "United States",
currency_code: "usd",
},
adminHeaders
)
).data.region
})
it("should list regions", async () => {
const response = await api.get("/admin/regions", adminHeaders)
@@ -79,20 +77,6 @@ medusaIntegrationTestRunner({
})
describe("GET /admin/regions/:id", () => {
let region1
beforeEach(async () => {
region1 = (
await api.post(
"/admin/regions",
{
name: "United Kingdom",
currency_code: "gbp",
},
adminHeaders
)
).data.region
})
it("should retrieve the region from ID", async () => {
const response = await api.get(
`/admin/regions/${region1.id}`,
@@ -121,18 +105,6 @@ medusaIntegrationTestRunner({
})
describe("POST /admin/regions", () => {
beforeEach(async () => {
await api.post(
"/admin/regions",
{
name: "United States",
currency_code: "usd",
countries: ["us"],
},
adminHeaders
)
})
it("should create a region", async () => {
const region = (
await api.post(
@@ -153,6 +125,33 @@ medusaIntegrationTestRunner({
)
})
it("should create a region with tax inclusivity setting", async () => {
const region = (
await api.post(
"/admin/regions",
{
name: "Test",
currency_code: "usd",
// BREAKING: The property used to be called `includes_tax`
is_tax_inclusive: true,
},
adminHeaders
)
).data.region
const response = await api.get(`/admin/price-preferences`, adminHeaders)
expect(response.data.price_preferences).toEqual(
expect.arrayContaining([
expect.objectContaining({
attribute: "region_id",
value: region.id,
is_tax_inclusive: true,
}),
])
)
})
it("should fails to create when countries exists in different region", async () => {
try {
await api.post(
@@ -173,102 +172,64 @@ medusaIntegrationTestRunner({
})
})
// TODO: Migrate when tax_inclusive_pricing is implemented
// describe("[MEDUSA_FF_TAX_INCLUSIVE_PRICING] /admin/regions", () => {
// let medusaProcess
// let dbConnection
describe("POST /admin/regions/:id", () => {
it("should update a region", async () => {
const region = (
await api.post(
`/admin/regions/${region1.id}`,
{
name: "New test",
currency_code: "eur",
},
adminHeaders
)
).data.region
// beforeAll(async () => {
// const cwd = path.resolve(path.join(__dirname, "..", ".."))
// const [process, connection] = await startServerWithEnvironment({
// cwd,
// env: { MEDUSA_FF_TAX_INCLUSIVE_PRICING: true },
// })
// dbConnection = connection
// medusaProcess = process
// })
expect(region).toEqual(
expect.objectContaining({
name: "New test",
currency_code: "eur",
})
)
})
// afterAll(async () => {
// const db = useDb()
// await db.shutdown()
it("should update a region with tax inclusivity setting", async () => {
const beforeResponse = await api.get(
`/admin/price-preferences`,
adminHeaders
)
expect(beforeResponse.data.price_preferences).toEqual(
expect.arrayContaining([
expect.objectContaining({
attribute: "region_id",
value: region1.id,
is_tax_inclusive: false,
}),
])
)
// medusaProcess.kill()
// })
await api.post(
`/admin/regions/${region1.id}`,
{
is_tax_inclusive: true,
},
adminHeaders
)
// describe("POST /admin/regions/:id", () => {
// const region1TaxInclusiveId = "region-1-tax-inclusive"
// beforeEach(async () => {
// try {
// await adminSeeder(dbConnection)
// await simpleRegionFactory(dbConnection, {
// id: region1TaxInclusiveId,
// countries: ["fr"],
// })
// } catch (err) {
// console.log(err)
// throw err
// }
// })
// afterEach(async () => {
// const db = useDb()
// await db.teardown()
// })
// it("should allow to create a region that includes tax", async function () {
// const api = useApi()
// const payload = {
// name: "region-including-taxes",
// currency_code: "usd",
// tax_rate: 0,
// payment_providers: ["test-pay"],
// fulfillment_providers: ["test-ful"],
// countries: ["us"],
// includes_tax: true,
// }
// const response = await api
// .post(`/admin/regions`, payload, adminReqConfig)
// .catch((err) => {
// console.log(err)
// })
// expect(response.data.region).toEqual(
// expect.objectContaining({
// id: expect.any(String),
// includes_tax: true,
// name: "region-including-taxes",
// })
// )
// })
// it("should allow to update a region that includes tax", async function () {
// const api = useApi()
// let response = await api
// .get(`/admin/regions/${region1TaxInclusiveId}`, adminReqConfig)
// .catch((err) => {
// console.log(err)
// })
// expect(response.data.region.includes_tax).toBe(false)
// response = await api
// .post(
// `/admin/regions/${region1TaxInclusiveId}`,
// {
// includes_tax: true,
// },
// adminReqConfig
// )
// .catch((err) => {
// console.log(err)
// })
// expect(response.data.region.includes_tax).toBe(true)
// })
// })
// })
const afterResponse = await api.get(
`/admin/price-preferences`,
adminHeaders
)
expect(afterResponse.data.price_preferences).toEqual(
expect.arrayContaining([
expect.objectContaining({
attribute: "region_id",
value: region1.id,
is_tax_inclusive: true,
}),
])
)
})
})
},
})

View File

@@ -1,5 +1,5 @@
import { BuildingTax } from "@medusajs/icons"
import { Tooltip } from "@medusajs/ui"
import { Tooltip, clx } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
type IncludesTaxTooltipProps = {
@@ -11,13 +11,15 @@ export const IncludesTaxTooltip = ({
}: IncludesTaxTooltipProps) => {
const { t } = useTranslation()
if (!includesTax) {
return null
}
return (
<Tooltip content={t("general.includesTaxTooltip")}>
<BuildingTax className="text-ui-fg-muted" />
<Tooltip
content={
includesTax
? t("general.includesTaxTooltip")
: t("general.excludesTaxTooltip")
}
>
<BuildingTax className={clx({ "text-ui-fg-muted": !includesTax })} />
</Tooltip>
)
}

View File

@@ -0,0 +1,104 @@
import { HttpTypes } from "@medusajs/types"
import { DataGridCurrencyCell } from "../data-grid-cells/data-grid-currency-cell"
import { createDataGridHelper } from "../utils"
import { IncludesTaxTooltip } from "../../../components/common/tax-badge/tax-badge"
import { TFunction } from "i18next"
import { CellContext } from "@tanstack/react-table"
import { DataGridReadOnlyCell } from "../data-grid-cells/data-grid-readonly-cell"
const columnHelper = createDataGridHelper<string | HttpTypes.AdminRegion>()
export const getPriceColumns = ({
currencies,
regions,
pricePreferences,
isReadyOnly,
getFieldName,
t,
}: {
currencies?: string[]
regions?: HttpTypes.AdminRegion[]
pricePreferences?: HttpTypes.AdminPricePreference[]
isReadyOnly?: (
context: CellContext<string | HttpTypes.AdminRegion, unknown>
) => boolean
getFieldName: (
context: CellContext<string | HttpTypes.AdminRegion, unknown>,
value: string
) => string
t: TFunction
}) => {
return [
...(currencies?.map((currency) => {
const preference = pricePreferences?.find(
(p) => p.attribute === "currency_code" && p.value === currency
)
return columnHelper.column({
id: `currency_prices.${currency}`,
name: t("fields.priceTemplate", {
regionOrCurrency: currency.toUpperCase(),
}),
header: () => (
<div className="flex w-full items-center justify-between gap-3">
{t("fields.priceTemplate", {
regionOrCurrency: currency.toUpperCase(),
})}
<IncludesTaxTooltip includesTax={preference?.is_tax_inclusive} />
</div>
),
cell: (context) => {
if (isReadyOnly?.(context)) {
return <DataGridReadOnlyCell />
}
return (
<DataGridCurrencyCell
code={currency}
context={context}
field={getFieldName(context, currency)}
/>
)
},
})
}) ?? []),
...(regions?.map((region) => {
const preference = pricePreferences?.find(
(p) => p.attribute === "region_id" && p.value === region.id
)
return columnHelper.column({
id: `region_prices.${region.id}`,
name: t("fields.priceTemplate", {
regionOrCurrency: region.name,
}),
header: () => (
<div className="flex w-full items-center justify-between gap-3">
{t("fields.priceTemplate", {
regionOrCurrency: region.name,
})}
<IncludesTaxTooltip includesTax={preference?.is_tax_inclusive} />
</div>
),
cell: (context) => {
if (isReadyOnly?.(context)) {
return <DataGridReadOnlyCell />
}
const currency = currencies?.find((c) => c === region.currency_code)
if (!currency) {
return null
}
return (
<DataGridCurrencyCell
code={region.currency_code}
context={context}
field={getFieldName(context, region.id)}
/>
)
},
})
}) ?? []),
]
}

View File

@@ -0,0 +1,113 @@
import { FetchError } from "@medusajs/js-sdk"
import { HttpTypes } from "@medusajs/types"
import {
QueryKey,
UseMutationOptions,
UseQueryOptions,
useMutation,
useQuery,
} from "@tanstack/react-query"
import { sdk } from "../../lib/client"
import { queryClient } from "../../lib/query-client"
import { queryKeysFactory } from "../../lib/query-key-factory"
const PRICE_PREFERENCES_QUERY_KEY = "price-preferences" as const
export const pricePreferencesQueryKeys = queryKeysFactory(
PRICE_PREFERENCES_QUERY_KEY
)
export const usePricePreference = (
id: string,
query?: HttpTypes.AdminPricePreferenceParams,
options?: Omit<
UseQueryOptions<
HttpTypes.AdminPricePreferenceResponse,
FetchError,
HttpTypes.AdminPricePreferenceResponse,
QueryKey
>,
"queryKey" | "queryFn"
>
) => {
const { data, ...rest } = useQuery({
queryFn: () => sdk.admin.pricePreference.retrieve(id, query),
queryKey: pricePreferencesQueryKeys.detail(id),
...options,
})
return { ...data, ...rest }
}
export const usePricePreferences = (
query?: HttpTypes.AdminPricePreferenceListParams,
options?: Omit<
UseQueryOptions<
HttpTypes.AdminPricePreferenceListResponse,
FetchError,
HttpTypes.AdminPricePreferenceListResponse,
QueryKey
>,
"queryKey" | "queryFn"
>
) => {
const { data, ...rest } = useQuery({
queryFn: () => sdk.admin.pricePreference.list(query),
queryKey: pricePreferencesQueryKeys.list(query),
...options,
})
return { ...data, ...rest }
}
export const useUpsertPricePreference = (
id?: string | undefined,
query?: HttpTypes.AdminPricePreferenceParams,
options?: UseMutationOptions<
HttpTypes.AdminPricePreferenceResponse,
FetchError,
HttpTypes.AdminUpdatePricePreference | HttpTypes.AdminCreatePricePreference
>
) => {
return useMutation({
mutationFn: (payload) => {
if (id) {
return sdk.admin.pricePreference.update(id, payload, query)
}
return sdk.admin.pricePreference.create(payload, query)
},
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({
queryKey: pricePreferencesQueryKeys.list(),
})
if (id) {
queryClient.invalidateQueries({
queryKey: pricePreferencesQueryKeys.detail(id),
})
}
options?.onSuccess?.(data, variables, context)
},
...options,
})
}
export const useDeletePricePreference = (
id: string,
options?: UseMutationOptions<
HttpTypes.AdminPricePreferenceDeleteResponse,
FetchError,
void
>
) => {
return useMutation({
mutationFn: () => sdk.admin.pricePreference.delete(id),
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({
queryKey: pricePreferencesQueryKeys.list(),
})
options?.onSuccess?.(data, variables, context)
},
...options,
})
}

View File

@@ -49,7 +49,8 @@
"noRecordsMessage": "There are no records to show",
"unsavedChangesTitle": "Are you sure you want to leave this form?",
"unsavedChangesDescription": "You have unsaved changes that will be lost if you exit this form.",
"includesTaxTooltip": "Enter the total amount including tax. The net amount excluding tax will be automatically calculated and saved."
"includesTaxTooltip": "Prices in this column are tax inclusive.",
"excludesTaxTooltip": "Prices in this column are tax exclusive."
},
"validation": {
"mustBeInt": "The value must be a whole number.",
@@ -2010,7 +2011,7 @@
"issuedDate": "Issued date",
"expiryDate": "Expiry date",
"price": "Price",
"priceTemplate": "Price {{regionOrCountry}}",
"priceTemplate": "Price {{regionOrCurrency}}",
"height": "Height",
"width": "Width",
"length": "Length",

View File

@@ -1,63 +1,32 @@
import { HttpTypes } from "@medusajs/types"
import { ColumnDef } from "@tanstack/react-table"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { DataGridCurrencyCell } from "../../../../components/data-grid/data-grid-cells/data-grid-currency-cell"
import { createDataGridHelper } from "../../../../components/data-grid/utils"
const columnHelper = createDataGridHelper<string | HttpTypes.AdminRegion>()
import { getPriceColumns } from "../../../../components/data-grid/data-grid-columns/price-columns"
export const useShippingOptionPriceColumns = ({
currencies = [],
regions = [],
pricePreferences = [],
}: {
currencies?: string[]
regions?: HttpTypes.AdminRegion[]
pricePreferences?: HttpTypes.AdminPricePreference[]
}) => {
const { t } = useTranslation()
return useMemo(() => {
return [
...currencies.map((currency) => {
return columnHelper.column({
id: `currency_prices.${currency}`,
name: t("fields.priceTemplate", {
regionOrCountry: currency.toUpperCase(),
}),
header: t("fields.priceTemplate", {
regionOrCountry: currency.toUpperCase(),
}),
cell: (context) => {
return (
<DataGridCurrencyCell
code={currency}
context={context}
field={`currency_prices.${currency}`}
/>
)
},
})
}),
...regions.map((region) => {
return columnHelper.column({
id: `region_prices.${region.id}`,
name: t("fields.priceTemplate", {
regionOrCountry: region.name,
}),
header: t("fields.priceTemplate", {
regionOrCountry: region.name,
}),
cell: (context) => {
return (
<DataGridCurrencyCell
code={region.currency_code}
context={context}
field={`region_prices.${region.id}`}
/>
)
},
})
}),
] as ColumnDef<(string | HttpTypes.AdminRegion)[]>[]
}, [t, currencies, regions])
return getPriceColumns({
currencies,
regions,
pricePreferences,
getFieldName: (context, value) => {
if (context.column.id.startsWith("currency_prices")) {
return `currency_prices.${value}`
}
return `region_prices.${value}`
},
t,
})
}, [t, currencies, regions, pricePreferences])
}

View File

@@ -6,6 +6,7 @@ import { useRegions } from "../../../../../hooks/api/regions"
import { useStore } from "../../../../../hooks/api/store"
import { useShippingOptionPriceColumns } from "../../../common/hooks/use-shipping-option-price-columns"
import { CreateShippingOptionSchema } from "./schema"
import { usePricePreferences } from "../../../../../hooks/api/price-preferences"
type PricingPricesFormProps = {
form: UseFormReturn<CreateShippingOptionSchema>
@@ -36,9 +37,12 @@ export const CreateShippingOptionsPricesForm = ({
limit: 999,
})
const { price_preferences: pricePreferences } = usePricePreferences({})
const columns = useShippingOptionPriceColumns({
currencies,
regions,
pricePreferences,
})
const initializing = isStoreLoading || !store || isRegionsLoading || !regions

View File

@@ -17,6 +17,7 @@ import { useUpdateShippingOptions } from "../../../../../hooks/api/shipping-opti
import { useStore } from "../../../../../hooks/api/store"
import { castNumber } from "../../../../../lib/cast-number"
import { useShippingOptionPriceColumns } from "../../../common/hooks/use-shipping-option-price-columns"
import { usePricePreferences } from "../../../../../hooks/api/price-preferences"
const getInitialCurrencyPrices = (
prices: HttpTypes.AdminShippingOptionPrice[]
@@ -108,9 +109,12 @@ export function EditShippingOptionsPricingForm({
limit: 999,
})
const { price_preferences: pricePreferences } = usePricePreferences({})
const columns = useShippingOptionPriceColumns({
currencies,
regions,
pricePreferences,
})
const data = useMemo(

View File

@@ -1,13 +1,20 @@
import { HttpTypes } from "@medusajs/types"
import { useRegions } from "../../../../hooks/api/regions"
import { useStore } from "../../../../hooks/api/store"
import { usePricePreferences } from "../../../../hooks/api/price-preferences"
type UsePriceListCurrencyDataReturn =
| { isReady: false; currencies: undefined; regions: undefined }
| {
isReady: false
currencies: undefined
regions: undefined
pricePreferences: undefined
}
| {
isReady: true
currencies: HttpTypes.AdminStoreCurrency[]
regions: HttpTypes.AdminRegion[]
pricePreferences: HttpTypes.AdminPricePreference[]
}
export const usePriceListCurrencyData = (): UsePriceListCurrencyDataReturn => {
@@ -32,8 +39,20 @@ export const usePriceListCurrencyData = (): UsePriceListCurrencyDataReturn => {
limit: 999,
})
const {
price_preferences: pricePreferences,
isPending: isPreferencesPending,
isError: isPreferencesError,
error: preferencesError,
} = usePricePreferences({})
const isReady =
!!currencies && !!regions && !isStorePending && !isRegionsPending
!!currencies &&
!!regions &&
!!pricePreferences &&
!isStorePending &&
!isRegionsPending &&
!isPreferencesPending
if (isRegionsError) {
throw regionsError
@@ -43,9 +62,18 @@ export const usePriceListCurrencyData = (): UsePriceListCurrencyDataReturn => {
throw storeError
}
if (!isReady) {
return { regions: undefined, currencies: undefined, isReady: false }
if (isPreferencesError) {
throw preferencesError
}
return { regions, currencies, isReady }
if (!isReady) {
return {
regions: undefined,
currencies: undefined,
pricePreferences: undefined,
isReady: false,
}
}
return { regions, currencies, pricePreferences, isReady }
}

View File

@@ -4,10 +4,10 @@ import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { Thumbnail } from "../../../../components/common/thumbnail"
import { DataGridCurrencyCell } from "../../../../components/data-grid/data-grid-cells/data-grid-currency-cell"
import { DataGridReadOnlyCell } from "../../../../components/data-grid/data-grid-cells/data-grid-readonly-cell"
import { createDataGridHelper } from "../../../../components/data-grid/utils"
import { isProductRow } from "../utils"
import { getPriceColumns } from "../../../../components/data-grid/data-grid-columns/price-columns"
const columnHelper = createDataGridHelper<
HttpTypes.AdminProduct | HttpTypes.AdminProductVariant
@@ -16,9 +16,11 @@ const columnHelper = createDataGridHelper<
export const usePriceListGridColumns = ({
currencies = [],
regions = [],
pricePreferences = [],
}: {
currencies?: StoreCurrencyDTO[]
regions?: HttpTypes.AdminRegion[]
pricePreferences?: HttpTypes.AdminPricePreference[]
}) => {
const { t } = useTranslation()
@@ -53,52 +55,25 @@ export const usePriceListGridColumns = ({
},
disableHiding: true,
}),
...currencies.map((currency) => {
return columnHelper.column({
id: `currency-price-${currency.currency_code}`,
name: `Price ${currency.currency_code.toUpperCase()}`,
header: `Price ${currency.currency_code.toUpperCase()}`,
cell: (context) => {
const entity = context.row.original
if (isProductRow(entity)) {
return <DataGridReadOnlyCell />
}
return (
<DataGridCurrencyCell
context={context}
code={currency.currency_code}
field={`products.${entity.product_id}.variants.${entity.id}.currency_prices.${currency.currency_code}.amount`}
/>
)
},
})
}),
...regions.map((region) => {
return columnHelper.column({
id: `region-price-${region.id}`,
name: `Price ${region.name}`,
header: `Price ${region.name}`,
cell: (context) => {
const entity = context.row.original
if (isProductRow(entity)) {
return <DataGridReadOnlyCell />
}
return (
<DataGridCurrencyCell
context={context}
code={region.currency_code}
field={`products.${entity.product_id}.variants.${entity.id}.region_prices.${region.id}.amount`}
/>
)
},
})
...getPriceColumns({
currencies: currencies.map((c) => c.currency_code),
regions,
pricePreferences,
isReadyOnly: (context) => {
const entity = context.row.original
return isProductRow(entity)
},
getFieldName: (context, value) => {
const entity = context.row.original as any
if (context.column.id.startsWith("currency_prices")) {
return `products.${entity.product_id}.variants.${entity.id}.currency_prices.${value}.amount`
}
return `products.${entity.product_id}.variants.${entity.id}.region_prices.${value}.amount`
},
t,
}),
]
}, [t, currencies, regions])
}, [t, currencies, regions, pricePreferences])
return colDefs
}

View File

@@ -44,11 +44,13 @@ const initialTabState: TabState = {
type PriceListCreateFormProps = {
regions: HttpTypes.AdminRegion[]
currencies: HttpTypes.AdminStoreCurrency[]
pricePreferences: HttpTypes.AdminPricePreference[]
}
export const PriceListCreateForm = ({
regions,
currencies,
pricePreferences,
}: PriceListCreateFormProps) => {
const [tab, setTab] = useState<Tab>(Tab.DETAIL)
const [tabState, setTabState] = useState<TabState>(initialTabState)
@@ -117,10 +119,13 @@ export const PriceListCreateForm = ({
) => {
form.clearErrors(fields)
const values = fields.reduce((acc, key) => {
acc[key] = form.getValues(key)
return acc
}, {} as Record<string, unknown>)
const values = fields.reduce(
(acc, key) => {
acc[key] = form.getValues(key)
return acc
},
{} as Record<string, unknown>
)
const validationResult = schema.safeParse(values)
@@ -295,6 +300,7 @@ export const PriceListCreateForm = ({
form={form}
regions={regions}
currencies={currencies}
pricePreferences={pricePreferences}
/>
</ProgressTabs.Content>
</RouteFocusModal.Body>

View File

@@ -12,12 +12,14 @@ type PriceListPricesFormProps = {
form: UseFormReturn<PricingCreateSchemaType>
currencies: HttpTypes.AdminStoreCurrency[]
regions: HttpTypes.AdminRegion[]
pricePreferences: HttpTypes.AdminPricePreference[]
}
export const PriceListPricesForm = ({
form,
currencies,
regions,
pricePreferences,
}: PriceListPricesFormProps) => {
const ids = useWatch({
control: form.control,
@@ -63,6 +65,7 @@ export const PriceListPricesForm = ({
const columns = usePriceListGridColumns({
currencies,
regions,
pricePreferences,
})
if (isError) {

View File

@@ -3,12 +3,17 @@ import { usePriceListCurrencyData } from "../common/hooks/use-price-list-currenc
import { PriceListCreateForm } from "./components/price-list-create-form"
export const PriceListCreate = () => {
const { isReady, regions, currencies } = usePriceListCurrencyData()
const { isReady, regions, currencies, pricePreferences } =
usePriceListCurrencyData()
return (
<RouteFocusModal>
{isReady && (
<PriceListCreateForm regions={regions} currencies={currencies} />
<PriceListCreateForm
regions={regions}
currencies={currencies}
pricePreferences={pricePreferences}
/>
)}
</RouteFocusModal>
)

View File

@@ -25,6 +25,7 @@ type PriceListPricesAddFormProps = {
priceList: HttpTypes.AdminPriceList
currencies: HttpTypes.AdminStoreCurrency[]
regions: HttpTypes.AdminRegion[]
pricePreferences: HttpTypes.AdminPricePreference[]
}
enum Tab {
@@ -45,6 +46,7 @@ export const PriceListPricesAddForm = ({
priceList,
regions,
currencies,
pricePreferences,
}: PriceListPricesAddFormProps) => {
const [tab, setTab] = useState<Tab>(Tab.PRODUCT)
const [tabState, setTabState] = useState<TabState>(initialTabState)
@@ -87,10 +89,13 @@ export const PriceListPricesAddForm = ({
) => {
form.clearErrors(fields)
const values = fields.reduce((acc, key) => {
acc[key] = form.getValues(key)
return acc
}, {} as Record<string, unknown>)
const values = fields.reduce(
(acc, key) => {
acc[key] = form.getValues(key)
return acc
},
{} as Record<string, unknown>
)
const validationResult = schema.safeParse(values)
@@ -236,6 +241,7 @@ export const PriceListPricesAddForm = ({
form={form}
regions={regions}
currencies={currencies}
pricePreferences={pricePreferences}
/>
</ProgressTabs.Content>
</RouteFocusModal.Body>

View File

@@ -13,12 +13,14 @@ type PriceListPricesAddPricesFormProps = {
form: UseFormReturn<PriceListPricesAddSchema>
currencies: HttpTypes.AdminStoreCurrency[]
regions: HttpTypes.AdminRegion[]
pricePreferences: HttpTypes.AdminPricePreference[]
}
export const PriceListPricesAddPricesForm = ({
form,
currencies,
regions,
pricePreferences,
}: PriceListPricesAddPricesFormProps) => {
const ids = useWatch({
control: form.control,
@@ -64,6 +66,7 @@ export const PriceListPricesAddPricesForm = ({
const columns = usePriceListGridColumns({
currencies,
regions,
pricePreferences,
})
if (isError) {

View File

@@ -8,7 +8,8 @@ export const PriceListProductsAdd = () => {
const { id } = useParams<{ id: string }>()
const { price_list, isPending, isError, error } = usePriceList(id!)
const { currencies, regions, isReady } = usePriceListCurrencyData()
const { currencies, regions, pricePreferences, isReady } =
usePriceListCurrencyData()
const ready = isReady && !isPending && !!price_list
@@ -23,6 +24,7 @@ export const PriceListProductsAdd = () => {
priceList={price_list}
currencies={currencies}
regions={regions}
pricePreferences={pricePreferences}
/>
)}
</RouteFocusModal>

View File

@@ -25,6 +25,7 @@ type PriceListPricesEditFormProps = {
products: HttpTypes.AdminProduct[]
regions: HttpTypes.AdminRegion[]
currencies: HttpTypes.AdminStoreCurrency[]
pricePreferences: HttpTypes.AdminPricePreference[]
}
const PricingProductPricesSchema = z.object({
@@ -36,6 +37,7 @@ export const PriceListPricesEditForm = ({
products,
regions,
currencies,
pricePreferences,
}: PriceListPricesEditFormProps) => {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
@@ -79,7 +81,11 @@ export const PriceListPricesEditForm = ({
)
})
const columns = usePriceListGridColumns({ currencies, regions })
const columns = usePriceListGridColumns({
currencies,
regions,
pricePreferences,
})
return (
<RouteFocusModal.Form form={form}>
@@ -178,10 +184,13 @@ function convertToPriceArray(
) {
const prices: PriceObject[] = []
const regionCurrencyMap = regions.reduce((map, region) => {
map[region.id] = region.currency_code
return map
}, {} as Record<string, string>)
const regionCurrencyMap = regions.reduce(
(map, region) => {
map[region.id] = region.currency_code
return map
},
{} as Record<string, string>
)
for (const [_productId, product] of Object.entries(data || {})) {
const { variants } = product || {}
@@ -233,15 +242,21 @@ function comparePrices(initialPrices: PriceObject[], newPrices: PriceObject[]) {
const pricesToCreate: HttpTypes.AdminCreatePriceListPrice[] = []
const pricesToDelete: string[] = []
const initialPriceMap = initialPrices.reduce((map, price) => {
map[createMapKey(price)] = price
return map
}, {} as Record<string, (typeof initialPrices)[0]>)
const initialPriceMap = initialPrices.reduce(
(map, price) => {
map[createMapKey(price)] = price
return map
},
{} as Record<string, (typeof initialPrices)[0]>
)
const newPriceMap = newPrices.reduce((map, price) => {
map[createMapKey(price)] = price
return map
}, {} as Record<string, (typeof newPrices)[0]>)
const newPriceMap = newPrices.reduce(
(map, price) => {
map[createMapKey(price)] = price
return map
},
{} as Record<string, (typeof newPrices)[0]>
)
const keys = new Set([
...Object.keys(initialPriceMap),

View File

@@ -25,7 +25,8 @@ export const PriceListPricesEdit = () => {
fields: "title,thumbnail,*variants",
})
const { isReady, regions, currencies } = usePriceListCurrencyData()
const { isReady, regions, currencies, pricePreferences } =
usePriceListCurrencyData()
const ready =
!isLoading && !!price_list && !isProductsLoading && !!products && isReady
@@ -46,6 +47,7 @@ export const PriceListPricesEdit = () => {
products={products}
regions={regions}
currencies={currencies}
pricePreferences={pricePreferences}
/>
)}
</RouteFocusModal>

View File

@@ -1,16 +1,16 @@
import { CurrencyDTO, HttpTypes, RegionDTO } from "@medusajs/types"
import { CurrencyDTO, HttpTypes } from "@medusajs/types"
import { ColumnDef, createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react"
import { UseFormReturn, useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { DataGrid } from "../../../components/grid/data-grid"
import { CurrencyCell } from "../../../components/grid/grid-cells/common/currency-cell"
import { ReadonlyCell } from "../../../components/grid/grid-cells/common/readonly-cell"
import { DataGridMeta } from "../../../components/grid/types"
import { useCurrencies } from "../../../hooks/api/currencies"
import { useStore } from "../../../hooks/api/store"
import { ProductCreateSchema } from "../product-create/constants"
import { useRegions } from "../../../hooks/api/regions.tsx"
import { usePricePreferences } from "../../../hooks/api/price-preferences.tsx"
import { getPriceColumns } from "../../../components/data-grid/data-grid-columns/price-columns.tsx"
import { DataGridRoot } from "../../../components/data-grid/data-grid-root/data-grid-root.tsx"
type VariantPricingFormProps = {
form: UseFormReturn<ProductCreateSchema>
@@ -30,9 +30,12 @@ export const VariantPricingForm = ({ form }: VariantPricingFormProps) => {
const { regions } = useRegions({ limit: 9999 })
const { price_preferences: pricePreferences } = usePricePreferences({})
const columns = useVariantPriceGridColumns({
currencies,
regions,
pricePreferences,
})
const variants = useWatch({
@@ -42,7 +45,7 @@ export const VariantPricingForm = ({ form }: VariantPricingFormProps) => {
return (
<div className="flex size-full flex-col divide-y overflow-hidden">
<DataGrid
<DataGridRoot
columns={columns}
data={variants}
isLoading={isStoreLoading || isCurrenciesLoading}
@@ -57,9 +60,11 @@ const columnHelper = createColumnHelper<HttpTypes.AdminProductVariant>()
export const useVariantPriceGridColumns = ({
currencies = [],
regions = [],
pricePreferences = [],
}: {
currencies?: CurrencyDTO[]
regions?: RegionDTO[]
regions?: HttpTypes.AdminRegion[]
pricePreferences?: HttpTypes.AdminPricePreference[]
}) => {
const { t } = useTranslation()
@@ -79,43 +84,20 @@ export const useVariantPriceGridColumns = ({
)
},
}),
...currencies.map((currency) => {
return columnHelper.display({
header: `Price ${currency.code.toUpperCase()}`,
cell: ({ row, table }) => {
return (
<CurrencyCell
currency={currency}
meta={table.options.meta as DataGridMeta}
field={`variants.${row.index}.prices.${currency.code}`}
/>
)
},
})
}),
...regions.map((region) => {
return columnHelper.display({
header: `Price ${region.name}`,
cell: ({ row, table }) => {
const currency = currencies.find(
(c) => c.code === region.currency_code
)
if (!currency) {
return null
}
return (
<CurrencyCell
currency={currency}
meta={table.options.meta as DataGridMeta}
field={`variants.${row.index}.prices.${region.id}`}
/>
)
},
})
...getPriceColumns({
currencies: currencies.map((c) => c.code),
regions,
pricePreferences,
getFieldName: (context, value) => {
if (context.column.id.startsWith("currency_prices")) {
return `variants.${context.row.index}.prices.${value}`
}
return `variants.${context.row.index}.prices.${value}`
},
t,
}),
]
}, [t, currencies, regions])
}, [t, currencies, regions, pricePreferences])
return colDefs
}

View File

@@ -1,4 +1,4 @@
import { HttpTypes, RegionDTO } from "@medusajs/types"
import { HttpTypes } from "@medusajs/types"
import { useMemo } from "react"
import { UseFormReturn, useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next"
@@ -8,9 +8,10 @@ import { DataGridRoot } from "../../../../../components/data-grid/data-grid-root
import { DataGridReadOnlyCell } from "../../../../../components/data-grid/data-grid-cells/data-grid-readonly-cell"
import { DataGridTextCell } from "../../../../../components/data-grid/data-grid-cells/data-grid-text-cell"
import { createDataGridHelper } from "../../../../../components/data-grid/utils"
import { DataGridCurrencyCell } from "../../../../../components/data-grid/data-grid-cells/data-grid-currency-cell"
import { DataGridBooleanCell } from "../../../../../components/data-grid/data-grid-cells/data-grid-boolean-cell"
import { useRegions } from "../../../../../hooks/api/regions"
import { usePricePreferences } from "../../../../../hooks/api/price-preferences"
import { getPriceColumns } from "../../../../../components/data-grid/data-grid-columns/price-columns"
type ProductCreateVariantsFormProps = {
form: UseFormReturn<ProductCreateSchemaType>
@@ -23,6 +24,8 @@ export const ProductCreateVariantsForm = ({
const { store, isPending, isError, error } = useStore()
const { price_preferences: pricePreferences } = usePricePreferences({})
const variants = useWatch({
control: form.control,
name: "variants",
@@ -39,6 +42,7 @@ export const ProductCreateVariantsForm = ({
options,
currencies: store?.supported_currencies?.map((c) => c.currency_code) || [],
regions,
pricePreferences,
})
const variantData = useMemo(
@@ -67,10 +71,12 @@ const useColumns = ({
options,
currencies = [],
regions = [],
pricePreferences = [],
}: {
options: any // CreateProductOptionSchemaType[]
currencies?: string[]
regions: RegionDTO[]
regions?: HttpTypes.AdminRegion[]
pricePreferences?: HttpTypes.AdminPricePreference[]
}) => {
const { t } = useTranslation()
@@ -166,40 +172,19 @@ const useColumns = ({
type: "boolean",
}),
...currencies.map((currency) => {
return columnHelper.column({
id: `price_${currency}`,
name: `Price ${currency.toUpperCase()}`,
header: `Price ${currency.toUpperCase()}`,
cell: (context) => {
return (
<DataGridCurrencyCell
code={currency}
context={context}
field={`variants.${context.row.index}.prices.${currency}`}
/>
)
},
})
}),
...regions.map((region) => {
return columnHelper.column({
id: `price_${region.id}`,
name: `Price ${region.name}`,
header: `Price ${region.name}`,
cell: (context) => {
return (
<DataGridCurrencyCell
code={region.currency_code}
context={context}
field={`variants.${context.row.index}.prices.${region.id}`}
/>
)
},
})
...getPriceColumns({
currencies,
regions,
pricePreferences,
getFieldName: (context, value) => {
if (context.column.id.startsWith("currency_prices")) {
return `variants.${context.row.index}.prices.${value}`
}
return `variants.${context.row.index}.prices.${value}`
},
t,
}),
],
[currencies, regions, options, t]
[currencies, regions, options, pricePreferences, t]
)
}

View File

@@ -45,6 +45,7 @@ const CreateRegionSchema = zod.object({
name: zod.string().min(1),
currency_code: zod.string().min(2, "Select a currency"),
automatic_taxes: zod.boolean(),
is_tax_inclusive: zod.boolean(),
countries: zod.array(zod.object({ code: zod.string(), name: zod.string() })),
payment_providers: zod.array(zod.string()).min(1),
})
@@ -65,6 +66,7 @@ export const CreateRegionForm = ({
name: "",
currency_code: "",
automatic_taxes: true,
is_tax_inclusive: false,
countries: [],
payment_providers: [],
},
@@ -79,31 +81,34 @@ export const CreateRegionForm = ({
const { t } = useTranslation()
const { mutateAsync, isPending } = useCreateRegion()
const { mutateAsync: createRegion, isPending: isPendingRegion } =
useCreateRegion()
const handleSubmit = form.handleSubmit(async (values) => {
await mutateAsync(
await createRegion(
{
name: values.name,
countries: values.countries.map((c) => c.code),
currency_code: values.currency_code,
payment_providers: values.payment_providers,
automatic_taxes: values.automatic_taxes,
is_tax_inclusive: values.is_tax_inclusive,
},
{
onSuccess: ({ region }) => {
toast.success(t("general.success"), {
description: t("regions.toast.create"),
dismissLabel: t("actions.close"),
})
handleSuccess(`../${region.id}`)
},
onError: (e) => {
toast.error(t("general.error"), {
description: e.message,
dismissLabel: t("actions.close"),
})
},
onSuccess: ({ region }) => {
toast.success(t("general.success"), {
description: t("regions.toast.create"),
dismissLabel: t("actions.close"),
})
handleSuccess(`../${region.id}`)
},
}
)
})
@@ -206,7 +211,7 @@ export const CreateRegionForm = ({
{t("actions.cancel")}
</Button>
</RouteFocusModal.Close>
<Button size="small" type="submit" isLoading={isPending}>
<Button size="small" type="submit" isLoading={isPendingRegion}>
{t("actions.save")}
</Button>
</div>
@@ -304,6 +309,35 @@ export const CreateRegionForm = ({
}}
/>
<Form.Field
control={form.control}
name="is_tax_inclusive"
render={({ field: { value, onChange, ...field } }) => {
return (
<Form.Item>
<div>
<div className="flex items-start justify-between">
<Form.Label>
{t("fields.taxInclusivePricing")}
</Form.Label>
<Form.Control>
<Switch
{...field}
checked={value}
onCheckedChange={onChange}
/>
</Form.Control>
</div>
<Form.Hint>
{t("regions.taxInclusiveHint")}
</Form.Hint>
<Form.ErrorMessage />
</div>
</Form.Item>
)
}}
/>
<div className="bg-ui-border-base h-px w-full" />
<div className="flex flex-col gap-y-4">
<div>

View File

@@ -9,13 +9,22 @@ import { ListSummary } from "../../../../../components/common/list-summary/index
import { useDeleteRegion } from "../../../../../hooks/api/regions.tsx"
import { currencies } from "../../../../../lib/currencies.ts"
import { formatProvider } from "../../../../../lib/format-provider.ts"
import { SectionRow } from "../../../../../components/common/section/section-row.tsx"
type RegionGeneralSectionProps = {
region: HttpTypes.AdminRegion
pricePreferences: HttpTypes.AdminPricePreference[]
}
export const RegionGeneralSection = ({ region }: RegionGeneralSectionProps) => {
export const RegionGeneralSection = ({
region,
pricePreferences,
}: RegionGeneralSectionProps) => {
const { t } = useTranslation()
const pricePreferenceForRegion = pricePreferences?.find(
(preference) =>
preference.attribute === "region_id" && preference.value === region.id
)
return (
<Container className="divide-y p-0">
@@ -23,33 +32,48 @@ export const RegionGeneralSection = ({ region }: RegionGeneralSectionProps) => {
<Heading>{region.name}</Heading>
<RegionActions region={region} />
</div>
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
<Text size="small" leading="compact" weight="plus">
{t("fields.currency")}
</Text>
<div className="flex items-center gap-x-2">
<Badge size="2xsmall" className="uppercase">
{region.currency_code}
</Badge>
<Text size="small" leading="compact">
{currencies[region.currency_code.toUpperCase()].name}
</Text>
</div>
</div>
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
<Text size="small" leading="compact" weight="plus">
{t("fields.paymentProviders")}
</Text>
<div className="inline-flex">
{region.payment_providers?.length > 0 ? (
<ListSummary
list={region.payment_providers.map((p) => formatProvider(p.id))}
/>
) : (
"-"
)}
</div>
</div>
<SectionRow
title={t("fields.currency")}
value={
<div className="flex items-center gap-x-2">
<Badge size="2xsmall" className="uppercase">
{region.currency_code}
</Badge>
<Text size="small" leading="compact">
{currencies[region.currency_code.toUpperCase()].name}
</Text>
</div>
}
/>
<SectionRow
title={t("fields.automaticTaxes")}
value={region.automatic_taxes ? t("fields.true") : t("fields.false")}
/>
<SectionRow
title={t("fields.taxInclusivePricing")}
value={
pricePreferenceForRegion?.is_tax_inclusive
? t("fields.true")
: t("fields.false")
}
/>
<SectionRow
title={t("fields.paymentProviders")}
value={
<div className="inline-flex">
{region.payment_providers?.length ? (
<ListSummary
list={region.payment_providers.map((p) => formatProvider(p.id))}
/>
) : (
"-"
)}
</div>
}
/>
</Container>
)
}

View File

@@ -8,6 +8,7 @@ import { regionLoader } from "./loader"
import after from "virtual:medusa/widgets/region/details/after"
import before from "virtual:medusa/widgets/region/details/before"
import { usePricePreferences } from "../../../hooks/api/price-preferences"
export const RegionDetail = () => {
const initialData = useLoaderData() as Awaited<
@@ -18,8 +19,8 @@ export const RegionDetail = () => {
const {
region,
isPending: isLoading,
isError,
error,
isError: isRegionError,
error: regionError,
} = useRegion(
id!,
{ fields: "*payment_providers,*countries" },
@@ -28,13 +29,30 @@ export const RegionDetail = () => {
}
)
const {
price_preferences: pricePreferences,
isPending: isLoadingPreferences,
isError: isPreferencesError,
error: preferencesError,
} = usePricePreferences(
{
attribute: "region_id",
value: id,
},
{ enabled: !!region }
)
// TODO: Move to loading.tsx and set as Suspense fallback for the route
if (isLoading || !region) {
if (isLoading || isLoadingPreferences || !region) {
return <div>Loading...</div>
}
if (isError) {
throw error
if (isRegionError) {
throw regionError
}
if (isPreferencesError) {
throw preferencesError
}
return (
@@ -46,7 +64,10 @@ export const RegionDetail = () => {
</div>
)
})}
<RegionGeneralSection region={region} />
<RegionGeneralSection
region={region}
pricePreferences={pricePreferences ?? []}
/>
<RegionCountrySection region={region} />
{after.widgets.map((w, i) => {
return (

View File

@@ -1,5 +1,5 @@
import { HttpTypes, PaymentProviderDTO } from "@medusajs/types"
import { Button, Input, Select, Text, toast } from "@medusajs/ui"
import { Button, Input, Select, Switch, Text, toast } from "@medusajs/ui"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
@@ -18,40 +18,58 @@ type EditRegionFormProps = {
region: HttpTypes.AdminRegion
currencies: CurrencyInfo[]
paymentProviders: PaymentProviderDTO[]
pricePreferences: HttpTypes.AdminPricePreference[]
}
const EditRegionSchema = zod.object({
name: zod.string().min(1),
currency_code: zod.string(),
payment_providers: zod.array(zod.string()),
automatic_taxes: zod.boolean(),
is_tax_inclusive: zod.boolean(),
})
export const EditRegionForm = ({
region,
currencies,
paymentProviders,
pricePreferences,
}: EditRegionFormProps) => {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const pricePreferenceForRegion = pricePreferences?.find(
(preference) =>
preference.attribute === "region_id" && preference.value === region.id
)
const form = useForm<zod.infer<typeof EditRegionSchema>>({
defaultValues: {
name: region.name,
currency_code: region.currency_code.toUpperCase(),
payment_providers: region.payment_providers?.map((pp) => pp.id) || [],
automatic_taxes: region.automatic_taxes,
is_tax_inclusive: pricePreferenceForRegion?.is_tax_inclusive || false,
},
})
const { mutateAsync, isPending: isLoading } = useUpdateRegion(region.id)
const { mutateAsync: updateRegion, isPending: isPendingRegion } =
useUpdateRegion(region.id)
const handleSubmit = form.handleSubmit(async (values) => {
await mutateAsync(
await updateRegion(
{
name: values.name,
currency_code: values.currency_code.toLowerCase(),
payment_providers: values.payment_providers,
is_tax_inclusive: values.is_tax_inclusive,
},
{
onError: (e) => {
toast.error(t("general.error"), {
description: e.message,
dismissLabel: t("actions.close"),
})
},
onSuccess: () => {
toast.success(t("general.success"), {
description: t("regions.toast.edit"),
@@ -59,12 +77,6 @@ export const EditRegionForm = ({
})
handleSuccess()
},
onError: (e) => {
toast.error(t("general.error"), {
description: e.message,
dismissLabel: t("actions.close"),
})
},
}
)
})
@@ -117,6 +129,59 @@ export const EditRegionForm = ({
}}
/>
</div>
<div className="flex flex-col gap-y-4">
<Form.Field
control={form.control}
name="automatic_taxes"
render={({ field: { value, onChange, ...field } }) => {
return (
<Form.Item>
<div>
<div className="flex items-start justify-between">
<Form.Label>{t("fields.automaticTaxes")}</Form.Label>
<Form.Control>
<Switch
{...field}
checked={value}
onCheckedChange={onChange}
/>
</Form.Control>
</div>
<Form.Hint>{t("regions.automaticTaxesHint")}</Form.Hint>
<Form.ErrorMessage />
</div>
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="is_tax_inclusive"
render={({ field: { value, onChange, ...field } }) => {
return (
<Form.Item>
<div>
<div className="flex items-start justify-between">
<Form.Label>
{t("fields.taxInclusivePricing")}
</Form.Label>
<Form.Control>
<Switch
{...field}
checked={value}
onCheckedChange={onChange}
/>
</Form.Control>
</div>
<Form.Hint>{t("regions.taxInclusiveHint")}</Form.Hint>
<Form.ErrorMessage />
</div>
</Form.Item>
)
}}
/>
</div>
<div className="flex flex-col gap-y-4">
<div>
<Text size="small" leading="compact" weight="plus">
@@ -157,7 +222,7 @@ export const EditRegionForm = ({
{t("actions.cancel")}
</Button>
</RouteDrawer.Close>
<Button size="small" type="submit" isLoading={isLoading}>
<Button size="small" type="submit" isLoading={isPendingRegion}>
{t("actions.save")}
</Button>
</div>

View File

@@ -8,6 +8,7 @@ import { useRegion } from "../../../hooks/api/regions"
import { useStore } from "../../../hooks/api/store"
import { currencies } from "../../../lib/currencies"
import { EditRegionForm } from "./components/edit-region-form"
import { usePricePreferences } from "../../../hooks/api/price-preferences"
export const RegionEdit = () => {
const { t } = useTranslation()
@@ -27,7 +28,20 @@ export const RegionEdit = () => {
error: storeError,
} = useStore()
const isLoading = isRegionLoading || isStoreLoading
const {
price_preferences: pricePreferences = [],
isPending: isPreferenceLoading,
isError: isPreferenceError,
error: preferenceError,
} = usePricePreferences(
{
attribute: "region_id",
value: id,
},
{ enabled: !!region }
)
const isLoading = isRegionLoading || isStoreLoading || isPreferenceLoading
const storeCurrencies = (store?.supported_currencies ?? []).map(
(c) => currencies[c.currency_code.toUpperCase()]
@@ -44,6 +58,10 @@ export const RegionEdit = () => {
throw storeError
}
if (isPreferenceError) {
throw preferenceError
}
return (
<RouteDrawer>
<RouteDrawer.Header>
@@ -54,6 +72,7 @@ export const RegionEdit = () => {
region={region}
currencies={storeCurrencies}
paymentProviders={paymentProviders}
pricePreferences={pricePreferences}
/>
)}
</RouteDrawer>

View File

@@ -1,11 +1,13 @@
import { WorkflowTypes } from "@medusajs/types"
import { CreateRegionDTO, WorkflowTypes } from "@medusajs/types"
import {
createWorkflow,
parallelize,
transform,
WorkflowData,
} from "@medusajs/workflows-sdk"
import { createRegionsStep } from "../steps"
import { setRegionsPaymentProvidersStep } from "../steps/set-regions-payment-providers"
import { createPricePreferencesWorkflow } from "../../pricing"
export const createRegionsWorkflowId = "create-regions"
export const createRegionsWorkflow = createWorkflow(
@@ -14,18 +16,22 @@ export const createRegionsWorkflow = createWorkflow(
input: WorkflowData<WorkflowTypes.RegionWorkflow.CreateRegionsWorkflowInput>
): WorkflowData<WorkflowTypes.RegionWorkflow.CreateRegionsWorkflowOutput> => {
const data = transform(input, (data) => {
const regionIndexToPaymentProviders = data.regions.map(
(region, index) => {
return {
region_index: index,
payment_providers: region.payment_providers,
}
const regionIndexToAdditionalData = data.regions.map((region, index) => {
return {
region_index: index,
payment_providers: region.payment_providers,
is_tax_inclusive: region.is_tax_inclusive,
}
)
})
return {
regions: data.regions,
regionIndexToPaymentProviders,
regions: data.regions.map((r) => {
const resp = { ...r }
delete resp.is_tax_inclusive
delete resp.payment_providers
return resp
}),
regionIndexToAdditionalData,
}
})
@@ -33,11 +39,11 @@ export const createRegionsWorkflow = createWorkflow(
const normalizedRegionProviderData = transform(
{
regionIndexToPaymentProviders: data.regionIndexToPaymentProviders,
regionIndexToAdditionalData: data.regionIndexToAdditionalData,
regions,
},
(data) => {
return data.regionIndexToPaymentProviders.map(
return data.regionIndexToAdditionalData.map(
({ region_index, payment_providers }) => {
return {
id: data.regions[region_index].id,
@@ -48,9 +54,32 @@ export const createRegionsWorkflow = createWorkflow(
}
)
setRegionsPaymentProvidersStep({
input: normalizedRegionProviderData,
})
const normalizedRegionPricePreferencesData = transform(
{
regionIndexToAdditionalData: data.regionIndexToAdditionalData,
regions,
},
(data) => {
return data.regionIndexToAdditionalData.map(
({ region_index, is_tax_inclusive }) => {
return {
attribute: "region_id",
value: data.regions[region_index].id,
is_tax_inclusive,
} as WorkflowTypes.PricingWorkflow.CreatePricePreferencesWorkflowInput
}
)
}
)
parallelize(
setRegionsPaymentProvidersStep({
input: normalizedRegionProviderData,
}),
createPricePreferencesWorkflow.runAsStep({
input: normalizedRegionPricePreferencesData,
})
)
return regions
}

View File

@@ -1,11 +1,14 @@
import { WorkflowTypes } from "@medusajs/types"
import {
createWorkflow,
parallelize,
transform,
when,
WorkflowData,
} from "@medusajs/workflows-sdk"
import { updateRegionsStep } from "../steps"
import { setRegionsPaymentProvidersStep } from "../steps/set-regions-payment-providers"
import { updatePricePreferencesWorkflow } from "../../pricing"
export const updateRegionsWorkflowId = "update-regions"
export const updateRegionsWorkflow = createWorkflow(
@@ -13,30 +16,58 @@ export const updateRegionsWorkflow = createWorkflow(
(
input: WorkflowData<WorkflowTypes.RegionWorkflow.UpdateRegionsWorkflowInput>
): WorkflowData<WorkflowTypes.RegionWorkflow.UpdateRegionsWorkflowOutput> => {
const data = transform(input, (data) => {
const normalizedInput = transform(input, (data) => {
const { selector, update } = data
const { payment_providers = [], ...rest } = update
const { payment_providers = [], is_tax_inclusive, ...rest } = update
return {
selector,
update: rest,
payment_providers,
is_tax_inclusive,
}
})
const regions = updateRegionsStep(data)
const regions = updateRegionsStep(normalizedInput)
const upsertProvidersNormalizedInput = transform(
{ data, regions },
{ normalizedInput, regions },
(data) => {
return data.regions.map((region) => {
return {
id: region.id,
payment_providers: data.data.payment_providers,
payment_providers: data.normalizedInput.payment_providers,
}
})
}
)
when({ normalizedInput }, (data) => {
return data.normalizedInput.is_tax_inclusive !== undefined
}).then(() => {
const updatePricePreferencesInput = transform(
{ normalizedInput, regions },
(data) => {
return {
selector: {
$or: data.regions.map((region) => {
return {
attribute: "region_id",
value: region.id,
}
}),
},
update: {
is_tax_inclusive: data.normalizedInput.is_tax_inclusive,
},
} as WorkflowTypes.PricingWorkflow.UpdatePricePreferencesWorkflowInput
}
)
updatePricePreferencesWorkflow.runAsStep({
input: updatePricePreferencesInput,
})
})
setRegionsPaymentProvidersStep({
input: upsertProvidersNormalizedInput,
})

View File

@@ -7,6 +7,7 @@ import { InventoryItem } from "./inventory-item"
import { Invite } from "./invite"
import { Order } from "./order"
import { PriceList } from "./price-list"
import { PricePreference } from "./price-preference"
import { Product } from "./product"
import { ProductCategory } from "./product-category"
import { ProductCollection } from "./product-collection"
@@ -27,6 +28,7 @@ export class Admin {
public productCollection: ProductCollection
public productCategory: ProductCategory
public priceList: PriceList
public pricePreference: PricePreference
public product: Product
public productType: ProductType
public upload: Upload
@@ -50,6 +52,7 @@ export class Admin {
this.productCollection = new ProductCollection(client)
this.productCategory = new ProductCategory(client)
this.priceList = new PriceList(client)
this.pricePreference = new PricePreference(client)
this.product = new Product(client)
this.productType = new ProductType(client)
this.upload = new Upload(client)

View File

@@ -0,0 +1,83 @@
import { HttpTypes } from "@medusajs/types"
import { Client } from "../client"
import { ClientHeaders } from "../types"
export class PricePreference {
private client: Client
constructor(client: Client) {
this.client = client
}
async retrieve(
id: string,
query?: HttpTypes.AdminPricePreferenceParams,
headers?: ClientHeaders
) {
return this.client.fetch<HttpTypes.AdminPricePreferenceResponse>(
`/admin/price-preferences/${id}`,
{
method: "GET",
headers,
query,
}
)
}
async list(
query?: HttpTypes.AdminPricePreferenceListParams,
headers?: ClientHeaders
) {
return this.client.fetch<HttpTypes.AdminPricePreferenceListResponse>(
`/admin/price-preferences`,
{
method: "GET",
headers,
query,
}
)
}
async create(
body: HttpTypes.AdminCreatePricePreference,
query?: HttpTypes.AdminPricePreferenceParams,
headers?: ClientHeaders
) {
return this.client.fetch<HttpTypes.AdminPricePreferenceResponse>(
`/admin/price-preferences`,
{
method: "POST",
headers,
body,
query,
}
)
}
async update(
id: string,
body: HttpTypes.AdminUpdatePricePreference,
query?: HttpTypes.AdminPricePreferenceParams,
headers?: ClientHeaders
) {
return this.client.fetch<HttpTypes.AdminPricePreferenceResponse>(
`/admin/price-preferences/${id}`,
{
method: "POST",
headers,
body,
query,
}
)
}
async delete(id: string, headers?: ClientHeaders) {
return this.client.fetch<HttpTypes.AdminPricePreferenceDeleteResponse>(
`/admin/price-preferences/${id}`,
{
method: "DELETE",
headers,
}
)
}
}

View File

@@ -15,6 +15,7 @@ export interface AdminCreateRegion {
currency_code: string
countries?: string[]
automatic_taxes?: boolean
is_tax_inclusive?: boolean
payment_providers?: string[]
metadata?: Record<string, any>
}
@@ -24,6 +25,7 @@ export interface AdminUpdateRegion {
currency_code?: string
countries?: string[]
automatic_taxes?: boolean
is_tax_inclusive?: boolean
payment_providers?: string[]
metadata?: Record<string, any>
}

View File

@@ -1,7 +1,10 @@
import { CreateRegionDTO, RegionDTO } from "../../region"
export interface CreateRegionsWorkflowInput {
regions: CreateRegionDTO[]
regions: (CreateRegionDTO & {
payment_providers?: string[]
is_tax_inclusive?: boolean
})[]
}
export type CreateRegionsWorkflowOutput = RegionDTO[]

View File

@@ -3,6 +3,7 @@ import { FilterableRegionProps, RegionDTO, UpdateRegionDTO } from "../../region"
export interface UpdateRegionsWorkflowInput {
selector: FilterableRegionProps
update: UpdateRegionDTO & {
is_tax_inclusive?: boolean
payment_providers?: string[]
}
}

View File

@@ -4,7 +4,7 @@ import { createFindParams, createSelectParams } from "../../utils/validators"
export const AdminGetPricePreferenceParams = createSelectParams()
export const AdminGetPricePreferencesParams = createFindParams({
offset: 0,
limit: 50,
limit: 300,
}).merge(
z.object({
q: z.string().optional(),

View File

@@ -33,6 +33,7 @@ export const AdminCreateRegion = z
currency_code: z.string(),
countries: z.array(z.string()).optional(),
automatic_taxes: z.boolean().optional(),
is_tax_inclusive: z.boolean().optional(),
payment_providers: z.array(z.string()).optional(),
metadata: z.record(z.unknown()).nullish(),
})
@@ -45,6 +46,7 @@ export const AdminUpdateRegion = z
currency_code: z.string().optional(),
countries: z.array(z.string()).optional(),
automatic_taxes: z.boolean().optional(),
is_tax_inclusive: z.boolean().optional(),
payment_providers: z.array(z.string()).optional(),
metadata: z.record(z.unknown()).nullish(),
})