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 authored and GitHub committed 2024-07-09 09:26:20 +02:00
1 parent 4b391fc3cf
commit cbf2fcd559
34 files changed
+920 -433

No files matched your search

@@ -8,41 +8,39 @@ jest.setTimeout(30000)
medusaIntegrationTestRunner({ medusaIntegrationTestRunner({
testSuite: ({ dbConnection, getContainer, api }) => { testSuite: ({ dbConnection, getContainer, api }) => {
beforeAll(() => {}) let region1
let region2
beforeEach(async () => { beforeEach(async () => {
const container = getContainer() const container = getContainer()
await createAdminUser(dbConnection, adminHeaders, container) 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", () => { 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 () => { it("should list regions", async () => {
const response = await api.get("/admin/regions", adminHeaders) const response = await api.get("/admin/regions", adminHeaders)
@@ -79,20 +77,6 @@ medusaIntegrationTestRunner({
}) })
describe("GET /admin/regions/:id", () => { 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 () => { it("should retrieve the region from ID", async () => {
const response = await api.get( const response = await api.get(
`/admin/regions/${region1.id}`, `/admin/regions/${region1.id}`,
@@ -121,18 +105,6 @@ medusaIntegrationTestRunner({
}) })
describe("POST /admin/regions", () => { 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 () => { it("should create a region", async () => {
const region = ( const region = (
await api.post( 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 () => { it("should fails to create when countries exists in different region", async () => {
try { try {
await api.post( await api.post(
@@ -173,102 +172,64 @@ medusaIntegrationTestRunner({
}) })
}) })
// TODO: Migrate when tax_inclusive_pricing is implemented describe("POST /admin/regions/:id", () => {
// describe("[MEDUSA_FF_TAX_INCLUSIVE_PRICING] /admin/regions", () => { it("should update a region", async () => {
// let medusaProcess const region = (
// let dbConnection await api.post(
`/admin/regions/${region1.id}`,
{
name: "New test",
currency_code: "eur",
},
adminHeaders
)
).data.region
// beforeAll(async () => { expect(region).toEqual(
// const cwd = path.resolve(path.join(__dirname, "..", "..")) expect.objectContaining({
// const [process, connection] = await startServerWithEnvironment({ name: "New test",
// cwd, currency_code: "eur",
// env: { MEDUSA_FF_TAX_INCLUSIVE_PRICING: true }, })
// }) )
// dbConnection = connection })
// medusaProcess = process
// })
// afterAll(async () => { it("should update a region with tax inclusivity setting", async () => {
// const db = useDb() const beforeResponse = await api.get(
// await db.shutdown() `/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 afterResponse = await api.get(
// const region1TaxInclusiveId = "region-1-tax-inclusive" `/admin/price-preferences`,
adminHeaders
// beforeEach(async () => { )
// try { expect(afterResponse.data.price_preferences).toEqual(
// await adminSeeder(dbConnection) expect.arrayContaining([
// await simpleRegionFactory(dbConnection, { expect.objectContaining({
// id: region1TaxInclusiveId, attribute: "region_id",
// countries: ["fr"], value: region1.id,
// }) is_tax_inclusive: true,
// } 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)
// })
// })
// })
}, },
}) })
@@ -1,5 +1,5 @@
import { BuildingTax } from "@medusajs/icons" import { BuildingTax } from "@medusajs/icons"
import { Tooltip } from "@medusajs/ui" import { Tooltip, clx } from "@medusajs/ui"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
type IncludesTaxTooltipProps = { type IncludesTaxTooltipProps = {
@@ -11,13 +11,15 @@ export const IncludesTaxTooltip = ({
}: IncludesTaxTooltipProps) => { }: IncludesTaxTooltipProps) => {
const { t } = useTranslation() const { t } = useTranslation()
if (!includesTax) {
return null
}
return ( return (
<Tooltip content={t("general.includesTaxTooltip")}> <Tooltip
<BuildingTax className="text-ui-fg-muted" /> content={
includesTax
? t("general.includesTaxTooltip")
: t("general.excludesTaxTooltip")
}
>
<BuildingTax className={clx({ "text-ui-fg-muted": !includesTax })} />
</Tooltip> </Tooltip>
) )
} }
@@ -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)}
/>
)
},
})
}) ?? []),
]
}
@@ -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,
})
}
@@ -49,7 +49,8 @@
"noRecordsMessage": "There are no records to show", "noRecordsMessage": "There are no records to show",
"unsavedChangesTitle": "Are you sure you want to leave this form?", "unsavedChangesTitle": "Are you sure you want to leave this form?",
"unsavedChangesDescription": "You have unsaved changes that will be lost if you exit 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": { "validation": {
"mustBeInt": "The value must be a whole number.", "mustBeInt": "The value must be a whole number.",
@@ -2010,7 +2011,7 @@
"issuedDate": "Issued date", "issuedDate": "Issued date",
"expiryDate": "Expiry date", "expiryDate": "Expiry date",
"price": "Price", "price": "Price",
"priceTemplate": "Price {{regionOrCountry}}", "priceTemplate": "Price {{regionOrCurrency}}",
"height": "Height", "height": "Height",
"width": "Width", "width": "Width",
"length": "Length", "length": "Length",
@@ -1,63 +1,32 @@
import { HttpTypes } from "@medusajs/types" import { HttpTypes } from "@medusajs/types"
import { ColumnDef } from "@tanstack/react-table"
import { useMemo } from "react" import { useMemo } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { DataGridCurrencyCell } from "../../../../components/data-grid/data-grid-cells/data-grid-currency-cell" import { getPriceColumns } from "../../../../components/data-grid/data-grid-columns/price-columns"
import { createDataGridHelper } from "../../../../components/data-grid/utils"
const columnHelper = createDataGridHelper<string | HttpTypes.AdminRegion>()
export const useShippingOptionPriceColumns = ({ export const useShippingOptionPriceColumns = ({
currencies = [], currencies = [],
regions = [], regions = [],
pricePreferences = [],
}: { }: {
currencies?: string[] currencies?: string[]
regions?: HttpTypes.AdminRegion[] regions?: HttpTypes.AdminRegion[]
pricePreferences?: HttpTypes.AdminPricePreference[]
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
return useMemo(() => { return useMemo(() => {
return [ return getPriceColumns({
...currencies.map((currency) => { currencies,
return columnHelper.column({ regions,
id: `currency_prices.${currency}`, pricePreferences,
name: t("fields.priceTemplate", { getFieldName: (context, value) => {
regionOrCountry: currency.toUpperCase(), if (context.column.id.startsWith("currency_prices")) {
}), return `currency_prices.${value}`
header: t("fields.priceTemplate", { }
regionOrCountry: currency.toUpperCase(),
}), return `region_prices.${value}`
cell: (context) => { },
return ( t,
<DataGridCurrencyCell })
code={currency} }, [t, currencies, regions, pricePreferences])
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])
} }
@@ -6,6 +6,7 @@ import { useRegions } from "../../../../../hooks/api/regions"
import { useStore } from "../../../../../hooks/api/store" import { useStore } from "../../../../../hooks/api/store"
import { useShippingOptionPriceColumns } from "../../../common/hooks/use-shipping-option-price-columns" import { useShippingOptionPriceColumns } from "../../../common/hooks/use-shipping-option-price-columns"
import { CreateShippingOptionSchema } from "./schema" import { CreateShippingOptionSchema } from "./schema"
import { usePricePreferences } from "../../../../../hooks/api/price-preferences"
type PricingPricesFormProps = { type PricingPricesFormProps = {
form: UseFormReturn<CreateShippingOptionSchema> form: UseFormReturn<CreateShippingOptionSchema>
@@ -36,9 +37,12 @@ export const CreateShippingOptionsPricesForm = ({
limit: 999, limit: 999,
}) })
const { price_preferences: pricePreferences } = usePricePreferences({})
const columns = useShippingOptionPriceColumns({ const columns = useShippingOptionPriceColumns({
currencies, currencies,
regions, regions,
pricePreferences,
}) })
const initializing = isStoreLoading || !store || isRegionsLoading || !regions const initializing = isStoreLoading || !store || isRegionsLoading || !regions
@@ -17,6 +17,7 @@ import { useUpdateShippingOptions } from "../../../../../hooks/api/shipping-opti
import { useStore } from "../../../../../hooks/api/store" import { useStore } from "../../../../../hooks/api/store"
import { castNumber } from "../../../../../lib/cast-number" import { castNumber } from "../../../../../lib/cast-number"
import { useShippingOptionPriceColumns } from "../../../common/hooks/use-shipping-option-price-columns" import { useShippingOptionPriceColumns } from "../../../common/hooks/use-shipping-option-price-columns"
import { usePricePreferences } from "../../../../../hooks/api/price-preferences"
const getInitialCurrencyPrices = ( const getInitialCurrencyPrices = (
prices: HttpTypes.AdminShippingOptionPrice[] prices: HttpTypes.AdminShippingOptionPrice[]
@@ -108,9 +109,12 @@ export function EditShippingOptionsPricingForm({
limit: 999, limit: 999,
}) })
const { price_preferences: pricePreferences } = usePricePreferences({})
const columns = useShippingOptionPriceColumns({ const columns = useShippingOptionPriceColumns({
currencies, currencies,
regions, regions,
pricePreferences,
}) })
const data = useMemo( const data = useMemo(
@@ -1,13 +1,20 @@
import { HttpTypes } from "@medusajs/types" import { HttpTypes } from "@medusajs/types"
import { useRegions } from "../../../../hooks/api/regions" import { useRegions } from "../../../../hooks/api/regions"
import { useStore } from "../../../../hooks/api/store" import { useStore } from "../../../../hooks/api/store"
import { usePricePreferences } from "../../../../hooks/api/price-preferences"
type UsePriceListCurrencyDataReturn = type UsePriceListCurrencyDataReturn =
| { isReady: false; currencies: undefined; regions: undefined } | {
isReady: false
currencies: undefined
regions: undefined
pricePreferences: undefined
}
| { | {
isReady: true isReady: true
currencies: HttpTypes.AdminStoreCurrency[] currencies: HttpTypes.AdminStoreCurrency[]
regions: HttpTypes.AdminRegion[] regions: HttpTypes.AdminRegion[]
pricePreferences: HttpTypes.AdminPricePreference[]
} }
export const usePriceListCurrencyData = (): UsePriceListCurrencyDataReturn => { export const usePriceListCurrencyData = (): UsePriceListCurrencyDataReturn => {
@@ -32,8 +39,20 @@ export const usePriceListCurrencyData = (): UsePriceListCurrencyDataReturn => {
limit: 999, limit: 999,
}) })
const {
price_preferences: pricePreferences,
isPending: isPreferencesPending,
isError: isPreferencesError,
error: preferencesError,
} = usePricePreferences({})
const isReady = const isReady =
!!currencies && !!regions && !isStorePending && !isRegionsPending !!currencies &&
!!regions &&
!!pricePreferences &&
!isStorePending &&
!isRegionsPending &&
!isPreferencesPending
if (isRegionsError) { if (isRegionsError) {
throw regionsError throw regionsError
@@ -43,9 +62,18 @@ export const usePriceListCurrencyData = (): UsePriceListCurrencyDataReturn => {
throw storeError throw storeError
} }
if (!isReady) { if (isPreferencesError) {
return { regions: undefined, currencies: undefined, isReady: false } throw preferencesError
} }
return { regions, currencies, isReady } if (!isReady) {
return {
regions: undefined,
currencies: undefined,
pricePreferences: undefined,
isReady: false,
}
}
return { regions, currencies, pricePreferences, isReady }
} }
@@ -4,10 +4,10 @@ import { useMemo } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { Thumbnail } from "../../../../components/common/thumbnail" 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 { DataGridReadOnlyCell } from "../../../../components/data-grid/data-grid-cells/data-grid-readonly-cell"
import { createDataGridHelper } from "../../../../components/data-grid/utils" import { createDataGridHelper } from "../../../../components/data-grid/utils"
import { isProductRow } from "../utils" import { isProductRow } from "../utils"
import { getPriceColumns } from "../../../../components/data-grid/data-grid-columns/price-columns"
const columnHelper = createDataGridHelper< const columnHelper = createDataGridHelper<
HttpTypes.AdminProduct | HttpTypes.AdminProductVariant HttpTypes.AdminProduct | HttpTypes.AdminProductVariant
@@ -16,9 +16,11 @@ const columnHelper = createDataGridHelper<
export const usePriceListGridColumns = ({ export const usePriceListGridColumns = ({
currencies = [], currencies = [],
regions = [], regions = [],
pricePreferences = [],
}: { }: {
currencies?: StoreCurrencyDTO[] currencies?: StoreCurrencyDTO[]
regions?: HttpTypes.AdminRegion[] regions?: HttpTypes.AdminRegion[]
pricePreferences?: HttpTypes.AdminPricePreference[]
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
@@ -53,52 +55,25 @@ export const usePriceListGridColumns = ({
}, },
disableHiding: true, disableHiding: true,
}), }),
...currencies.map((currency) => { ...getPriceColumns({
return columnHelper.column({ currencies: currencies.map((c) => c.currency_code),
id: `currency-price-${currency.currency_code}`, regions,
name: `Price ${currency.currency_code.toUpperCase()}`, pricePreferences,
header: `Price ${currency.currency_code.toUpperCase()}`, isReadyOnly: (context) => {
cell: (context) => { const entity = context.row.original
const entity = context.row.original return isProductRow(entity)
},
if (isProductRow(entity)) { getFieldName: (context, value) => {
return <DataGridReadOnlyCell /> 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 ( }
<DataGridCurrencyCell return `products.${entity.product_id}.variants.${entity.id}.region_prices.${value}.amount`
context={context} },
code={currency.currency_code} t,
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`}
/>
)
},
})
}), }),
] ]
}, [t, currencies, regions]) }, [t, currencies, regions, pricePreferences])
return colDefs return colDefs
} }
@@ -44,11 +44,13 @@ const initialTabState: TabState = {
type PriceListCreateFormProps = { type PriceListCreateFormProps = {
regions: HttpTypes.AdminRegion[] regions: HttpTypes.AdminRegion[]
currencies: HttpTypes.AdminStoreCurrency[] currencies: HttpTypes.AdminStoreCurrency[]
pricePreferences: HttpTypes.AdminPricePreference[]
} }
export const PriceListCreateForm = ({ export const PriceListCreateForm = ({
regions, regions,
currencies, currencies,
pricePreferences,
}: PriceListCreateFormProps) => { }: PriceListCreateFormProps) => {
const [tab, setTab] = useState<Tab>(Tab.DETAIL) const [tab, setTab] = useState<Tab>(Tab.DETAIL)
const [tabState, setTabState] = useState<TabState>(initialTabState) const [tabState, setTabState] = useState<TabState>(initialTabState)
@@ -117,10 +119,13 @@ export const PriceListCreateForm = ({
) => { ) => {
form.clearErrors(fields) form.clearErrors(fields)
const values = fields.reduce((acc, key) => { const values = fields.reduce(
acc[key] = form.getValues(key) (acc, key) => {
return acc acc[key] = form.getValues(key)
}, {} as Record<string, unknown>) return acc
},
{} as Record<string, unknown>
)
const validationResult = schema.safeParse(values) const validationResult = schema.safeParse(values)
@@ -295,6 +300,7 @@ export const PriceListCreateForm = ({
form={form} form={form}
regions={regions} regions={regions}
currencies={currencies} currencies={currencies}
pricePreferences={pricePreferences}
/> />
</ProgressTabs.Content> </ProgressTabs.Content>
</RouteFocusModal.Body> </RouteFocusModal.Body>
@@ -12,12 +12,14 @@ type PriceListPricesFormProps = {
form: UseFormReturn<PricingCreateSchemaType> form: UseFormReturn<PricingCreateSchemaType>
currencies: HttpTypes.AdminStoreCurrency[] currencies: HttpTypes.AdminStoreCurrency[]
regions: HttpTypes.AdminRegion[] regions: HttpTypes.AdminRegion[]
pricePreferences: HttpTypes.AdminPricePreference[]
} }
export const PriceListPricesForm = ({ export const PriceListPricesForm = ({
form, form,
currencies, currencies,
regions, regions,
pricePreferences,
}: PriceListPricesFormProps) => { }: PriceListPricesFormProps) => {
const ids = useWatch({ const ids = useWatch({
control: form.control, control: form.control,
@@ -63,6 +65,7 @@ export const PriceListPricesForm = ({
const columns = usePriceListGridColumns({ const columns = usePriceListGridColumns({
currencies, currencies,
regions, regions,
pricePreferences,
}) })
if (isError) { if (isError) {
@@ -3,12 +3,17 @@ import { usePriceListCurrencyData } from "../common/hooks/use-price-list-currenc
import { PriceListCreateForm } from "./components/price-list-create-form" import { PriceListCreateForm } from "./components/price-list-create-form"
export const PriceListCreate = () => { export const PriceListCreate = () => {
const { isReady, regions, currencies } = usePriceListCurrencyData() const { isReady, regions, currencies, pricePreferences } =
usePriceListCurrencyData()
return ( return (
<RouteFocusModal> <RouteFocusModal>
{isReady && ( {isReady && (
<PriceListCreateForm regions={regions} currencies={currencies} /> <PriceListCreateForm
regions={regions}
currencies={currencies}
pricePreferences={pricePreferences}
/>
)} )}
</RouteFocusModal> </RouteFocusModal>
) )
@@ -25,6 +25,7 @@ type PriceListPricesAddFormProps = {
priceList: HttpTypes.AdminPriceList priceList: HttpTypes.AdminPriceList
currencies: HttpTypes.AdminStoreCurrency[] currencies: HttpTypes.AdminStoreCurrency[]
regions: HttpTypes.AdminRegion[] regions: HttpTypes.AdminRegion[]
pricePreferences: HttpTypes.AdminPricePreference[]
} }
enum Tab { enum Tab {
@@ -45,6 +46,7 @@ export const PriceListPricesAddForm = ({
priceList, priceList,
regions, regions,
currencies, currencies,
pricePreferences,
}: PriceListPricesAddFormProps) => { }: PriceListPricesAddFormProps) => {
const [tab, setTab] = useState<Tab>(Tab.PRODUCT) const [tab, setTab] = useState<Tab>(Tab.PRODUCT)
const [tabState, setTabState] = useState<TabState>(initialTabState) const [tabState, setTabState] = useState<TabState>(initialTabState)
@@ -87,10 +89,13 @@ export const PriceListPricesAddForm = ({
) => { ) => {
form.clearErrors(fields) form.clearErrors(fields)
const values = fields.reduce((acc, key) => { const values = fields.reduce(
acc[key] = form.getValues(key) (acc, key) => {
return acc acc[key] = form.getValues(key)
}, {} as Record<string, unknown>) return acc
},
{} as Record<string, unknown>
)
const validationResult = schema.safeParse(values) const validationResult = schema.safeParse(values)
@@ -236,6 +241,7 @@ export const PriceListPricesAddForm = ({
form={form} form={form}
regions={regions} regions={regions}
currencies={currencies} currencies={currencies}
pricePreferences={pricePreferences}
/> />
</ProgressTabs.Content> </ProgressTabs.Content>
</RouteFocusModal.Body> </RouteFocusModal.Body>
@@ -13,12 +13,14 @@ type PriceListPricesAddPricesFormProps = {
form: UseFormReturn<PriceListPricesAddSchema> form: UseFormReturn<PriceListPricesAddSchema>
currencies: HttpTypes.AdminStoreCurrency[] currencies: HttpTypes.AdminStoreCurrency[]
regions: HttpTypes.AdminRegion[] regions: HttpTypes.AdminRegion[]
pricePreferences: HttpTypes.AdminPricePreference[]
} }
export const PriceListPricesAddPricesForm = ({ export const PriceListPricesAddPricesForm = ({
form, form,
currencies, currencies,
regions, regions,
pricePreferences,
}: PriceListPricesAddPricesFormProps) => { }: PriceListPricesAddPricesFormProps) => {
const ids = useWatch({ const ids = useWatch({
control: form.control, control: form.control,
@@ -64,6 +66,7 @@ export const PriceListPricesAddPricesForm = ({
const columns = usePriceListGridColumns({ const columns = usePriceListGridColumns({
currencies, currencies,
regions, regions,
pricePreferences,
}) })
if (isError) { if (isError) {
@@ -8,7 +8,8 @@ export const PriceListProductsAdd = () => {
const { id } = useParams<{ id: string }>() const { id } = useParams<{ id: string }>()
const { price_list, isPending, isError, error } = usePriceList(id!) 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 const ready = isReady && !isPending && !!price_list
@@ -23,6 +24,7 @@ export const PriceListProductsAdd = () => {
priceList={price_list} priceList={price_list}
currencies={currencies} currencies={currencies}
regions={regions} regions={regions}
pricePreferences={pricePreferences}
/> />
)} )}
</RouteFocusModal> </RouteFocusModal>
@@ -25,6 +25,7 @@ type PriceListPricesEditFormProps = {
products: HttpTypes.AdminProduct[] products: HttpTypes.AdminProduct[]
regions: HttpTypes.AdminRegion[] regions: HttpTypes.AdminRegion[]
currencies: HttpTypes.AdminStoreCurrency[] currencies: HttpTypes.AdminStoreCurrency[]
pricePreferences: HttpTypes.AdminPricePreference[]
} }
const PricingProductPricesSchema = z.object({ const PricingProductPricesSchema = z.object({
@@ -36,6 +37,7 @@ export const PriceListPricesEditForm = ({
products, products,
regions, regions,
currencies, currencies,
pricePreferences,
}: PriceListPricesEditFormProps) => { }: PriceListPricesEditFormProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const { handleSuccess } = useRouteModal() const { handleSuccess } = useRouteModal()
@@ -79,7 +81,11 @@ export const PriceListPricesEditForm = ({
) )
}) })
const columns = usePriceListGridColumns({ currencies, regions }) const columns = usePriceListGridColumns({
currencies,
regions,
pricePreferences,
})
return ( return (
<RouteFocusModal.Form form={form}> <RouteFocusModal.Form form={form}>
@@ -178,10 +184,13 @@ function convertToPriceArray(
) { ) {
const prices: PriceObject[] = [] const prices: PriceObject[] = []
const regionCurrencyMap = regions.reduce((map, region) => { const regionCurrencyMap = regions.reduce(
map[region.id] = region.currency_code (map, region) => {
return map map[region.id] = region.currency_code
}, {} as Record<string, string>) return map
},
{} as Record<string, string>
)
for (const [_productId, product] of Object.entries(data || {})) { for (const [_productId, product] of Object.entries(data || {})) {
const { variants } = product || {} const { variants } = product || {}
@@ -233,15 +242,21 @@ function comparePrices(initialPrices: PriceObject[], newPrices: PriceObject[]) {
const pricesToCreate: HttpTypes.AdminCreatePriceListPrice[] = [] const pricesToCreate: HttpTypes.AdminCreatePriceListPrice[] = []
const pricesToDelete: string[] = [] const pricesToDelete: string[] = []
const initialPriceMap = initialPrices.reduce((map, price) => { const initialPriceMap = initialPrices.reduce(
map[createMapKey(price)] = price (map, price) => {
return map map[createMapKey(price)] = price
}, {} as Record<string, (typeof initialPrices)[0]>) return map
},
{} as Record<string, (typeof initialPrices)[0]>
)
const newPriceMap = newPrices.reduce((map, price) => { const newPriceMap = newPrices.reduce(
map[createMapKey(price)] = price (map, price) => {
return map map[createMapKey(price)] = price
}, {} as Record<string, (typeof newPrices)[0]>) return map
},
{} as Record<string, (typeof newPrices)[0]>
)
const keys = new Set([ const keys = new Set([
...Object.keys(initialPriceMap), ...Object.keys(initialPriceMap),
@@ -25,7 +25,8 @@ export const PriceListPricesEdit = () => {
fields: "title,thumbnail,*variants", fields: "title,thumbnail,*variants",
}) })
const { isReady, regions, currencies } = usePriceListCurrencyData() const { isReady, regions, currencies, pricePreferences } =
usePriceListCurrencyData()
const ready = const ready =
!isLoading && !!price_list && !isProductsLoading && !!products && isReady !isLoading && !!price_list && !isProductsLoading && !!products && isReady
@@ -46,6 +47,7 @@ export const PriceListPricesEdit = () => {
products={products} products={products}
regions={regions} regions={regions}
currencies={currencies} currencies={currencies}
pricePreferences={pricePreferences}
/> />
)} )}
</RouteFocusModal> </RouteFocusModal>
@@ -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 { ColumnDef, createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react" import { useMemo } from "react"
import { UseFormReturn, useWatch } from "react-hook-form" import { UseFormReturn, useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next" 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 { ReadonlyCell } from "../../../components/grid/grid-cells/common/readonly-cell"
import { DataGridMeta } from "../../../components/grid/types"
import { useCurrencies } from "../../../hooks/api/currencies" import { useCurrencies } from "../../../hooks/api/currencies"
import { useStore } from "../../../hooks/api/store" import { useStore } from "../../../hooks/api/store"
import { ProductCreateSchema } from "../product-create/constants" import { ProductCreateSchema } from "../product-create/constants"
import { useRegions } from "../../../hooks/api/regions.tsx" 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 = { type VariantPricingFormProps = {
form: UseFormReturn<ProductCreateSchema> form: UseFormReturn<ProductCreateSchema>
@@ -30,9 +30,12 @@ export const VariantPricingForm = ({ form }: VariantPricingFormProps) => {
const { regions } = useRegions({ limit: 9999 }) const { regions } = useRegions({ limit: 9999 })
const { price_preferences: pricePreferences } = usePricePreferences({})
const columns = useVariantPriceGridColumns({ const columns = useVariantPriceGridColumns({
currencies, currencies,
regions, regions,
pricePreferences,
}) })
const variants = useWatch({ const variants = useWatch({
@@ -42,7 +45,7 @@ export const VariantPricingForm = ({ form }: VariantPricingFormProps) => {
return ( return (
<div className="flex size-full flex-col divide-y overflow-hidden"> <div className="flex size-full flex-col divide-y overflow-hidden">
<DataGrid <DataGridRoot
columns={columns} columns={columns}
data={variants} data={variants}
isLoading={isStoreLoading || isCurrenciesLoading} isLoading={isStoreLoading || isCurrenciesLoading}
@@ -57,9 +60,11 @@ const columnHelper = createColumnHelper<HttpTypes.AdminProductVariant>()
export const useVariantPriceGridColumns = ({ export const useVariantPriceGridColumns = ({
currencies = [], currencies = [],
regions = [], regions = [],
pricePreferences = [],
}: { }: {
currencies?: CurrencyDTO[] currencies?: CurrencyDTO[]
regions?: RegionDTO[] regions?: HttpTypes.AdminRegion[]
pricePreferences?: HttpTypes.AdminPricePreference[]
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
@@ -79,43 +84,20 @@ export const useVariantPriceGridColumns = ({
) )
}, },
}), }),
...currencies.map((currency) => { ...getPriceColumns({
return columnHelper.display({ currencies: currencies.map((c) => c.code),
header: `Price ${currency.code.toUpperCase()}`, regions,
cell: ({ row, table }) => { pricePreferences,
return ( getFieldName: (context, value) => {
<CurrencyCell if (context.column.id.startsWith("currency_prices")) {
currency={currency} return `variants.${context.row.index}.prices.${value}`
meta={table.options.meta as DataGridMeta} }
field={`variants.${row.index}.prices.${currency.code}`} return `variants.${context.row.index}.prices.${value}`
/> },
) t,
},
})
}),
...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}`}
/>
)
},
})
}), }),
] ]
}, [t, currencies, regions]) }, [t, currencies, regions, pricePreferences])
return colDefs return colDefs
} }
@@ -1,4 +1,4 @@
import { HttpTypes, RegionDTO } from "@medusajs/types" import { HttpTypes } from "@medusajs/types"
import { useMemo } from "react" import { useMemo } from "react"
import { UseFormReturn, useWatch } from "react-hook-form" import { UseFormReturn, useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next" 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 { 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 { DataGridTextCell } from "../../../../../components/data-grid/data-grid-cells/data-grid-text-cell"
import { createDataGridHelper } from "../../../../../components/data-grid/utils" 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 { DataGridBooleanCell } from "../../../../../components/data-grid/data-grid-cells/data-grid-boolean-cell"
import { useRegions } from "../../../../../hooks/api/regions" 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 = { type ProductCreateVariantsFormProps = {
form: UseFormReturn<ProductCreateSchemaType> form: UseFormReturn<ProductCreateSchemaType>
@@ -23,6 +24,8 @@ export const ProductCreateVariantsForm = ({
const { store, isPending, isError, error } = useStore() const { store, isPending, isError, error } = useStore()
const { price_preferences: pricePreferences } = usePricePreferences({})
const variants = useWatch({ const variants = useWatch({
control: form.control, control: form.control,
name: "variants", name: "variants",
@@ -39,6 +42,7 @@ export const ProductCreateVariantsForm = ({
options, options,
currencies: store?.supported_currencies?.map((c) => c.currency_code) || [], currencies: store?.supported_currencies?.map((c) => c.currency_code) || [],
regions, regions,
pricePreferences,
}) })
const variantData = useMemo( const variantData = useMemo(
@@ -67,10 +71,12 @@ const useColumns = ({
options, options,
currencies = [], currencies = [],
regions = [], regions = [],
pricePreferences = [],
}: { }: {
options: any // CreateProductOptionSchemaType[] options: any // CreateProductOptionSchemaType[]
currencies?: string[] currencies?: string[]
regions: RegionDTO[] regions?: HttpTypes.AdminRegion[]
pricePreferences?: HttpTypes.AdminPricePreference[]
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
@@ -166,40 +172,19 @@ const useColumns = ({
type: "boolean", type: "boolean",
}), }),
...currencies.map((currency) => { ...getPriceColumns({
return columnHelper.column({ currencies,
id: `price_${currency}`, regions,
name: `Price ${currency.toUpperCase()}`, pricePreferences,
header: `Price ${currency.toUpperCase()}`, getFieldName: (context, value) => {
cell: (context) => { if (context.column.id.startsWith("currency_prices")) {
return ( return `variants.${context.row.index}.prices.${value}`
<DataGridCurrencyCell }
code={currency} return `variants.${context.row.index}.prices.${value}`
context={context} },
field={`variants.${context.row.index}.prices.${currency}`} t,
/>
)
},
})
}),
...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}`}
/>
)
},
})
}), }),
], ],
[currencies, regions, options, t] [currencies, regions, options, pricePreferences, t]
) )
} }
@@ -45,6 +45,7 @@ const CreateRegionSchema = zod.object({
name: zod.string().min(1), name: zod.string().min(1),
currency_code: zod.string().min(2, "Select a currency"), currency_code: zod.string().min(2, "Select a currency"),
automatic_taxes: zod.boolean(), automatic_taxes: zod.boolean(),
is_tax_inclusive: zod.boolean(),
countries: zod.array(zod.object({ code: zod.string(), name: zod.string() })), countries: zod.array(zod.object({ code: zod.string(), name: zod.string() })),
payment_providers: zod.array(zod.string()).min(1), payment_providers: zod.array(zod.string()).min(1),
}) })
@@ -65,6 +66,7 @@ export const CreateRegionForm = ({
name: "", name: "",
currency_code: "", currency_code: "",
automatic_taxes: true, automatic_taxes: true,
is_tax_inclusive: false,
countries: [], countries: [],
payment_providers: [], payment_providers: [],
}, },
@@ -79,31 +81,34 @@ export const CreateRegionForm = ({
const { t } = useTranslation() const { t } = useTranslation()
const { mutateAsync, isPending } = useCreateRegion() const { mutateAsync: createRegion, isPending: isPendingRegion } =
useCreateRegion()
const handleSubmit = form.handleSubmit(async (values) => { const handleSubmit = form.handleSubmit(async (values) => {
await mutateAsync( await createRegion(
{ {
name: values.name, name: values.name,
countries: values.countries.map((c) => c.code), countries: values.countries.map((c) => c.code),
currency_code: values.currency_code, currency_code: values.currency_code,
payment_providers: values.payment_providers, payment_providers: values.payment_providers,
automatic_taxes: values.automatic_taxes, 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) => { onError: (e) => {
toast.error(t("general.error"), { toast.error(t("general.error"), {
description: e.message, description: e.message,
dismissLabel: t("actions.close"), 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")} {t("actions.cancel")}
</Button> </Button>
</RouteFocusModal.Close> </RouteFocusModal.Close>
<Button size="small" type="submit" isLoading={isPending}> <Button size="small" type="submit" isLoading={isPendingRegion}>
{t("actions.save")} {t("actions.save")}
</Button> </Button>
</div> </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="bg-ui-border-base h-px w-full" />
<div className="flex flex-col gap-y-4"> <div className="flex flex-col gap-y-4">
<div> <div>
@@ -9,13 +9,22 @@ import { ListSummary } from "../../../../../components/common/list-summary/index
import { useDeleteRegion } from "../../../../../hooks/api/regions.tsx" import { useDeleteRegion } from "../../../../../hooks/api/regions.tsx"
import { currencies } from "../../../../../lib/currencies.ts" import { currencies } from "../../../../../lib/currencies.ts"
import { formatProvider } from "../../../../../lib/format-provider.ts" import { formatProvider } from "../../../../../lib/format-provider.ts"
import { SectionRow } from "../../../../../components/common/section/section-row.tsx"
type RegionGeneralSectionProps = { type RegionGeneralSectionProps = {
region: HttpTypes.AdminRegion region: HttpTypes.AdminRegion
pricePreferences: HttpTypes.AdminPricePreference[]
} }
export const RegionGeneralSection = ({ region }: RegionGeneralSectionProps) => { export const RegionGeneralSection = ({
region,
pricePreferences,
}: RegionGeneralSectionProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const pricePreferenceForRegion = pricePreferences?.find(
(preference) =>
preference.attribute === "region_id" && preference.value === region.id
)
return ( return (
<Container className="divide-y p-0"> <Container className="divide-y p-0">
@@ -23,33 +32,48 @@ export const RegionGeneralSection = ({ region }: RegionGeneralSectionProps) => {
<Heading>{region.name}</Heading> <Heading>{region.name}</Heading>
<RegionActions region={region} /> <RegionActions region={region} />
</div> </div>
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4"> <SectionRow
<Text size="small" leading="compact" weight="plus"> title={t("fields.currency")}
{t("fields.currency")} value={
</Text> <div className="flex items-center gap-x-2">
<div className="flex items-center gap-x-2"> <Badge size="2xsmall" className="uppercase">
<Badge size="2xsmall" className="uppercase"> {region.currency_code}
{region.currency_code} </Badge>
</Badge> <Text size="small" leading="compact">
<Text size="small" leading="compact"> {currencies[region.currency_code.toUpperCase()].name}
{currencies[region.currency_code.toUpperCase()].name} </Text>
</Text> </div>
</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"> <SectionRow
{t("fields.paymentProviders")} title={t("fields.automaticTaxes")}
</Text> value={region.automatic_taxes ? t("fields.true") : t("fields.false")}
<div className="inline-flex"> />
{region.payment_providers?.length > 0 ? (
<ListSummary <SectionRow
list={region.payment_providers.map((p) => formatProvider(p.id))} title={t("fields.taxInclusivePricing")}
/> value={
) : ( pricePreferenceForRegion?.is_tax_inclusive
"-" ? t("fields.true")
)} : t("fields.false")
</div> }
</div> />
<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> </Container>
) )
} }
@@ -8,6 +8,7 @@ import { regionLoader } from "./loader"
import after from "virtual:medusa/widgets/region/details/after" import after from "virtual:medusa/widgets/region/details/after"
import before from "virtual:medusa/widgets/region/details/before" import before from "virtual:medusa/widgets/region/details/before"
import { usePricePreferences } from "../../../hooks/api/price-preferences"
export const RegionDetail = () => { export const RegionDetail = () => {
const initialData = useLoaderData() as Awaited< const initialData = useLoaderData() as Awaited<
@@ -18,8 +19,8 @@ export const RegionDetail = () => {
const { const {
region, region,
isPending: isLoading, isPending: isLoading,
isError, isError: isRegionError,
error, error: regionError,
} = useRegion( } = useRegion(
id!, id!,
{ fields: "*payment_providers,*countries" }, { 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 // TODO: Move to loading.tsx and set as Suspense fallback for the route
if (isLoading || !region) { if (isLoading || isLoadingPreferences || !region) {
return <div>Loading...</div> return <div>Loading...</div>
} }
if (isError) { if (isRegionError) {
throw error throw regionError
}
if (isPreferencesError) {
throw preferencesError
} }
return ( return (
@@ -46,7 +64,10 @@ export const RegionDetail = () => {
</div> </div>
) )
})} })}
<RegionGeneralSection region={region} /> <RegionGeneralSection
region={region}
pricePreferences={pricePreferences ?? []}
/>
<RegionCountrySection region={region} /> <RegionCountrySection region={region} />
{after.widgets.map((w, i) => { {after.widgets.map((w, i) => {
return ( return (
@@ -1,5 +1,5 @@
import { HttpTypes, PaymentProviderDTO } from "@medusajs/types" 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 { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import * as zod from "zod" import * as zod from "zod"
@@ -18,40 +18,58 @@ type EditRegionFormProps = {
region: HttpTypes.AdminRegion region: HttpTypes.AdminRegion
currencies: CurrencyInfo[] currencies: CurrencyInfo[]
paymentProviders: PaymentProviderDTO[] paymentProviders: PaymentProviderDTO[]
pricePreferences: HttpTypes.AdminPricePreference[]
} }
const EditRegionSchema = zod.object({ const EditRegionSchema = zod.object({
name: zod.string().min(1), name: zod.string().min(1),
currency_code: zod.string(), currency_code: zod.string(),
payment_providers: zod.array(zod.string()), payment_providers: zod.array(zod.string()),
automatic_taxes: zod.boolean(),
is_tax_inclusive: zod.boolean(),
}) })
export const EditRegionForm = ({ export const EditRegionForm = ({
region, region,
currencies, currencies,
paymentProviders, paymentProviders,
pricePreferences,
}: EditRegionFormProps) => { }: EditRegionFormProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const { handleSuccess } = useRouteModal() const { handleSuccess } = useRouteModal()
const pricePreferenceForRegion = pricePreferences?.find(
(preference) =>
preference.attribute === "region_id" && preference.value === region.id
)
const form = useForm<zod.infer<typeof EditRegionSchema>>({ const form = useForm<zod.infer<typeof EditRegionSchema>>({
defaultValues: { defaultValues: {
name: region.name, name: region.name,
currency_code: region.currency_code.toUpperCase(), currency_code: region.currency_code.toUpperCase(),
payment_providers: region.payment_providers?.map((pp) => pp.id) || [], 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) => { const handleSubmit = form.handleSubmit(async (values) => {
await mutateAsync( await updateRegion(
{ {
name: values.name, name: values.name,
currency_code: values.currency_code.toLowerCase(), currency_code: values.currency_code.toLowerCase(),
payment_providers: values.payment_providers, 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: () => { onSuccess: () => {
toast.success(t("general.success"), { toast.success(t("general.success"), {
description: t("regions.toast.edit"), description: t("regions.toast.edit"),
@@ -59,12 +77,6 @@ export const EditRegionForm = ({
}) })
handleSuccess() handleSuccess()
}, },
onError: (e) => {
toast.error(t("general.error"), {
description: e.message,
dismissLabel: t("actions.close"),
})
},
} }
) )
}) })
@@ -117,6 +129,59 @@ export const EditRegionForm = ({
}} }}
/> />
</div> </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 className="flex flex-col gap-y-4">
<div> <div>
<Text size="small" leading="compact" weight="plus"> <Text size="small" leading="compact" weight="plus">
@@ -157,7 +222,7 @@ export const EditRegionForm = ({
{t("actions.cancel")} {t("actions.cancel")}
</Button> </Button>
</RouteDrawer.Close> </RouteDrawer.Close>
<Button size="small" type="submit" isLoading={isLoading}> <Button size="small" type="submit" isLoading={isPendingRegion}>
{t("actions.save")} {t("actions.save")}
</Button> </Button>
</div> </div>
@@ -8,6 +8,7 @@ import { useRegion } from "../../../hooks/api/regions"
import { useStore } from "../../../hooks/api/store" import { useStore } from "../../../hooks/api/store"
import { currencies } from "../../../lib/currencies" import { currencies } from "../../../lib/currencies"
import { EditRegionForm } from "./components/edit-region-form" import { EditRegionForm } from "./components/edit-region-form"
import { usePricePreferences } from "../../../hooks/api/price-preferences"
export const RegionEdit = () => { export const RegionEdit = () => {
const { t } = useTranslation() const { t } = useTranslation()
@@ -27,7 +28,20 @@ export const RegionEdit = () => {
error: storeError, error: storeError,
} = useStore() } = 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( const storeCurrencies = (store?.supported_currencies ?? []).map(
(c) => currencies[c.currency_code.toUpperCase()] (c) => currencies[c.currency_code.toUpperCase()]
@@ -44,6 +58,10 @@ export const RegionEdit = () => {
throw storeError throw storeError
} }
if (isPreferenceError) {
throw preferenceError
}
return ( return (
<RouteDrawer> <RouteDrawer>
<RouteDrawer.Header> <RouteDrawer.Header>
@@ -54,6 +72,7 @@ export const RegionEdit = () => {
region={region} region={region}
currencies={storeCurrencies} currencies={storeCurrencies}
paymentProviders={paymentProviders} paymentProviders={paymentProviders}
pricePreferences={pricePreferences}
/> />
)} )}
</RouteDrawer> </RouteDrawer>
@@ -1,11 +1,13 @@
import { WorkflowTypes } from "@medusajs/types" import { CreateRegionDTO, WorkflowTypes } from "@medusajs/types"
import { import {
createWorkflow, createWorkflow,
parallelize,
transform, transform,
WorkflowData, WorkflowData,
} from "@medusajs/workflows-sdk" } from "@medusajs/workflows-sdk"
import { createRegionsStep } from "../steps" import { createRegionsStep } from "../steps"
import { setRegionsPaymentProvidersStep } from "../steps/set-regions-payment-providers" import { setRegionsPaymentProvidersStep } from "../steps/set-regions-payment-providers"
import { createPricePreferencesWorkflow } from "../../pricing"
export const createRegionsWorkflowId = "create-regions" export const createRegionsWorkflowId = "create-regions"
export const createRegionsWorkflow = createWorkflow( export const createRegionsWorkflow = createWorkflow(
@@ -14,18 +16,22 @@ export const createRegionsWorkflow = createWorkflow(
input: WorkflowData<WorkflowTypes.RegionWorkflow.CreateRegionsWorkflowInput> input: WorkflowData<WorkflowTypes.RegionWorkflow.CreateRegionsWorkflowInput>
): WorkflowData<WorkflowTypes.RegionWorkflow.CreateRegionsWorkflowOutput> => { ): WorkflowData<WorkflowTypes.RegionWorkflow.CreateRegionsWorkflowOutput> => {
const data = transform(input, (data) => { const data = transform(input, (data) => {
const regionIndexToPaymentProviders = data.regions.map( const regionIndexToAdditionalData = data.regions.map((region, index) => {
(region, index) => { return {
return { region_index: index,
region_index: index, payment_providers: region.payment_providers,
payment_providers: region.payment_providers, is_tax_inclusive: region.is_tax_inclusive,
}
} }
) })
return { return {
regions: data.regions, regions: data.regions.map((r) => {
regionIndexToPaymentProviders, 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( const normalizedRegionProviderData = transform(
{ {
regionIndexToPaymentProviders: data.regionIndexToPaymentProviders, regionIndexToAdditionalData: data.regionIndexToAdditionalData,
regions, regions,
}, },
(data) => { (data) => {
return data.regionIndexToPaymentProviders.map( return data.regionIndexToAdditionalData.map(
({ region_index, payment_providers }) => { ({ region_index, payment_providers }) => {
return { return {
id: data.regions[region_index].id, id: data.regions[region_index].id,
@@ -48,9 +54,32 @@ export const createRegionsWorkflow = createWorkflow(
} }
) )
setRegionsPaymentProvidersStep({ const normalizedRegionPricePreferencesData = transform(
input: normalizedRegionProviderData, {
}) 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 return regions
} }
@@ -1,11 +1,14 @@
import { WorkflowTypes } from "@medusajs/types" import { WorkflowTypes } from "@medusajs/types"
import { import {
createWorkflow, createWorkflow,
parallelize,
transform, transform,
when,
WorkflowData, WorkflowData,
} from "@medusajs/workflows-sdk" } from "@medusajs/workflows-sdk"
import { updateRegionsStep } from "../steps" import { updateRegionsStep } from "../steps"
import { setRegionsPaymentProvidersStep } from "../steps/set-regions-payment-providers" import { setRegionsPaymentProvidersStep } from "../steps/set-regions-payment-providers"
import { updatePricePreferencesWorkflow } from "../../pricing"
export const updateRegionsWorkflowId = "update-regions" export const updateRegionsWorkflowId = "update-regions"
export const updateRegionsWorkflow = createWorkflow( export const updateRegionsWorkflow = createWorkflow(
@@ -13,30 +16,58 @@ export const updateRegionsWorkflow = createWorkflow(
( (
input: WorkflowData<WorkflowTypes.RegionWorkflow.UpdateRegionsWorkflowInput> input: WorkflowData<WorkflowTypes.RegionWorkflow.UpdateRegionsWorkflowInput>
): WorkflowData<WorkflowTypes.RegionWorkflow.UpdateRegionsWorkflowOutput> => { ): WorkflowData<WorkflowTypes.RegionWorkflow.UpdateRegionsWorkflowOutput> => {
const data = transform(input, (data) => { const normalizedInput = transform(input, (data) => {
const { selector, update } = data const { selector, update } = data
const { payment_providers = [], ...rest } = update const { payment_providers = [], is_tax_inclusive, ...rest } = update
return { return {
selector, selector,
update: rest, update: rest,
payment_providers, payment_providers,
is_tax_inclusive,
} }
}) })
const regions = updateRegionsStep(data) const regions = updateRegionsStep(normalizedInput)
const upsertProvidersNormalizedInput = transform( const upsertProvidersNormalizedInput = transform(
{ data, regions }, { normalizedInput, regions },
(data) => { (data) => {
return data.regions.map((region) => { return data.regions.map((region) => {
return { return {
id: region.id, 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({ setRegionsPaymentProvidersStep({
input: upsertProvidersNormalizedInput, input: upsertProvidersNormalizedInput,
}) })
+3
View File
@@ -7,6 +7,7 @@ import { InventoryItem } from "./inventory-item"
import { Invite } from "./invite" import { Invite } from "./invite"
import { Order } from "./order" import { Order } from "./order"
import { PriceList } from "./price-list" import { PriceList } from "./price-list"
import { PricePreference } from "./price-preference"
import { Product } from "./product" import { Product } from "./product"
import { ProductCategory } from "./product-category" import { ProductCategory } from "./product-category"
import { ProductCollection } from "./product-collection" import { ProductCollection } from "./product-collection"
@@ -27,6 +28,7 @@ export class Admin {
public productCollection: ProductCollection public productCollection: ProductCollection
public productCategory: ProductCategory public productCategory: ProductCategory
public priceList: PriceList public priceList: PriceList
public pricePreference: PricePreference
public product: Product public product: Product
public productType: ProductType public productType: ProductType
public upload: Upload public upload: Upload
@@ -50,6 +52,7 @@ export class Admin {
this.productCollection = new ProductCollection(client) this.productCollection = new ProductCollection(client)
this.productCategory = new ProductCategory(client) this.productCategory = new ProductCategory(client)
this.priceList = new PriceList(client) this.priceList = new PriceList(client)
this.pricePreference = new PricePreference(client)
this.product = new Product(client) this.product = new Product(client)
this.productType = new ProductType(client) this.productType = new ProductType(client)
this.upload = new Upload(client) this.upload = new Upload(client)
@@ -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,
}
)
}
}
@@ -15,6 +15,7 @@ export interface AdminCreateRegion {
currency_code: string currency_code: string
countries?: string[] countries?: string[]
automatic_taxes?: boolean automatic_taxes?: boolean
is_tax_inclusive?: boolean
payment_providers?: string[] payment_providers?: string[]
metadata?: Record<string, any> metadata?: Record<string, any>
} }
@@ -24,6 +25,7 @@ export interface AdminUpdateRegion {
currency_code?: string currency_code?: string
countries?: string[] countries?: string[]
automatic_taxes?: boolean automatic_taxes?: boolean
is_tax_inclusive?: boolean
payment_providers?: string[] payment_providers?: string[]
metadata?: Record<string, any> metadata?: Record<string, any>
} }
@@ -1,7 +1,10 @@
import { CreateRegionDTO, RegionDTO } from "../../region" import { CreateRegionDTO, RegionDTO } from "../../region"
export interface CreateRegionsWorkflowInput { export interface CreateRegionsWorkflowInput {
regions: CreateRegionDTO[] regions: (CreateRegionDTO & {
payment_providers?: string[]
is_tax_inclusive?: boolean
})[]
} }
export type CreateRegionsWorkflowOutput = RegionDTO[] export type CreateRegionsWorkflowOutput = RegionDTO[]
@@ -3,6 +3,7 @@ import { FilterableRegionProps, RegionDTO, UpdateRegionDTO } from "../../region"
export interface UpdateRegionsWorkflowInput { export interface UpdateRegionsWorkflowInput {
selector: FilterableRegionProps selector: FilterableRegionProps
update: UpdateRegionDTO & { update: UpdateRegionDTO & {
is_tax_inclusive?: boolean
payment_providers?: string[] payment_providers?: string[]
} }
} }
@@ -4,7 +4,7 @@ import { createFindParams, createSelectParams } from "../../utils/validators"
export const AdminGetPricePreferenceParams = createSelectParams() export const AdminGetPricePreferenceParams = createSelectParams()
export const AdminGetPricePreferencesParams = createFindParams({ export const AdminGetPricePreferencesParams = createFindParams({
offset: 0, offset: 0,
limit: 50, limit: 300,
}).merge( }).merge(
z.object({ z.object({
q: z.string().optional(), q: z.string().optional(),
@@ -33,6 +33,7 @@ export const AdminCreateRegion = z
currency_code: z.string(), currency_code: z.string(),
countries: z.array(z.string()).optional(), countries: z.array(z.string()).optional(),
automatic_taxes: z.boolean().optional(), automatic_taxes: z.boolean().optional(),
is_tax_inclusive: z.boolean().optional(),
payment_providers: z.array(z.string()).optional(), payment_providers: z.array(z.string()).optional(),
metadata: z.record(z.unknown()).nullish(), metadata: z.record(z.unknown()).nullish(),
}) })
@@ -45,6 +46,7 @@ export const AdminUpdateRegion = z
currency_code: z.string().optional(), currency_code: z.string().optional(),
countries: z.array(z.string()).optional(), countries: z.array(z.string()).optional(),
automatic_taxes: z.boolean().optional(), automatic_taxes: z.boolean().optional(),
is_tax_inclusive: z.boolean().optional(),
payment_providers: z.array(z.string()).optional(), payment_providers: z.array(z.string()).optional(),
metadata: z.record(z.unknown()).nullish(), metadata: z.record(z.unknown()).nullish(),
}) })