From cbf2fcd55912b49ff96fdca847a571434c29ce40 Mon Sep 17 00:00:00 2001 From: Stevche Radevski Date: Tue, 9 Jul 2024 09:26:20 +0200 Subject: [PATCH] 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 --- .../__tests__/region/admin/region.spec.ts | 255 ++++++++---------- .../components/common/tax-badge/tax-badge.tsx | 16 +- .../data-grid-columns/price-columns.tsx | 104 +++++++ .../src/hooks/api/price-preferences.tsx | 113 ++++++++ .../dashboard/src/i18n/translations/en.json | 5 +- .../use-shipping-option-price-columns.tsx | 65 ++--- .../create-shipping-options-prices-form.tsx | 4 + .../edit-shipping-options-pricing-form.tsx | 4 + .../hooks/use-price-list-currency-data.tsx | 38 ++- .../hooks/use-price-list-grid-columns.tsx | 65 ++--- .../price-list-create-form.tsx | 14 +- .../price-list-prices-form.tsx | 3 + .../price-list-create/price-list-create.tsx | 9 +- .../price-list-prices-add-form.tsx | 14 +- .../price-list-prices-add-prices-form.tsx | 3 + .../price-list-prices-add.tsx | 4 +- .../price-list-prices-edit-form.tsx | 41 ++- .../price-list-prices-edit.tsx | 4 +- .../products/common/variant-pricing-form.tsx | 64 ++--- .../product-create-variants-form.tsx | 57 ++-- .../create-region-form/create-region-form.tsx | 54 +++- .../region-general-section.tsx | 80 ++++-- .../regions/region-detail/region-detail.tsx | 33 ++- .../edit-region-form/edit-region-form.tsx | 85 +++++- .../regions/region-edit/region-edit.tsx | 21 +- .../src/region/workflows/create-regions.ts | 59 ++-- .../src/region/workflows/update-regions.ts | 41 ++- packages/core/js-sdk/src/admin/index.ts | 3 + .../core/js-sdk/src/admin/price-preference.ts | 83 ++++++ packages/core/types/src/http/region/admin.ts | 2 + .../src/workflow/region/create-regions.ts | 5 +- .../src/workflow/region/update-regions.ts | 1 + .../api/admin/price-preferences/validators.ts | 2 +- .../src/api/admin/regions/validators.ts | 2 + 34 files changed, 920 insertions(+), 433 deletions(-) create mode 100644 packages/admin-next/dashboard/src/components/data-grid/data-grid-columns/price-columns.tsx create mode 100644 packages/admin-next/dashboard/src/hooks/api/price-preferences.tsx create mode 100644 packages/core/js-sdk/src/admin/price-preference.ts diff --git a/integration-tests/http/__tests__/region/admin/region.spec.ts b/integration-tests/http/__tests__/region/admin/region.spec.ts index db330d9666..2dc63c72c4 100644 --- a/integration-tests/http/__tests__/region/admin/region.spec.ts +++ b/integration-tests/http/__tests__/region/admin/region.spec.ts @@ -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, + }), + ]) + ) + }) + }) }, }) diff --git a/packages/admin-next/dashboard/src/components/common/tax-badge/tax-badge.tsx b/packages/admin-next/dashboard/src/components/common/tax-badge/tax-badge.tsx index 02f3c0b14c..fd6550c72e 100644 --- a/packages/admin-next/dashboard/src/components/common/tax-badge/tax-badge.tsx +++ b/packages/admin-next/dashboard/src/components/common/tax-badge/tax-badge.tsx @@ -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 ( - - + + ) } diff --git a/packages/admin-next/dashboard/src/components/data-grid/data-grid-columns/price-columns.tsx b/packages/admin-next/dashboard/src/components/data-grid/data-grid-columns/price-columns.tsx new file mode 100644 index 0000000000..6cee1a6091 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/data-grid/data-grid-columns/price-columns.tsx @@ -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() + +export const getPriceColumns = ({ + currencies, + regions, + pricePreferences, + isReadyOnly, + getFieldName, + t, +}: { + currencies?: string[] + regions?: HttpTypes.AdminRegion[] + pricePreferences?: HttpTypes.AdminPricePreference[] + isReadyOnly?: ( + context: CellContext + ) => boolean + getFieldName: ( + context: CellContext, + 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: () => ( +
+ {t("fields.priceTemplate", { + regionOrCurrency: currency.toUpperCase(), + })} + +
+ ), + cell: (context) => { + if (isReadyOnly?.(context)) { + return + } + + return ( + + ) + }, + }) + }) ?? []), + ...(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: () => ( +
+ {t("fields.priceTemplate", { + regionOrCurrency: region.name, + })} + +
+ ), + cell: (context) => { + if (isReadyOnly?.(context)) { + return + } + + const currency = currencies?.find((c) => c === region.currency_code) + if (!currency) { + return null + } + + return ( + + ) + }, + }) + }) ?? []), + ] +} diff --git a/packages/admin-next/dashboard/src/hooks/api/price-preferences.tsx b/packages/admin-next/dashboard/src/hooks/api/price-preferences.tsx new file mode 100644 index 0000000000..bdc49b41fd --- /dev/null +++ b/packages/admin-next/dashboard/src/hooks/api/price-preferences.tsx @@ -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, + }) +} diff --git a/packages/admin-next/dashboard/src/i18n/translations/en.json b/packages/admin-next/dashboard/src/i18n/translations/en.json index f2b39adf70..6cb75682ef 100644 --- a/packages/admin-next/dashboard/src/i18n/translations/en.json +++ b/packages/admin-next/dashboard/src/i18n/translations/en.json @@ -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", diff --git a/packages/admin-next/dashboard/src/routes/locations/common/hooks/use-shipping-option-price-columns.tsx b/packages/admin-next/dashboard/src/routes/locations/common/hooks/use-shipping-option-price-columns.tsx index 3dd2c94803..e0611504b8 100644 --- a/packages/admin-next/dashboard/src/routes/locations/common/hooks/use-shipping-option-price-columns.tsx +++ b/packages/admin-next/dashboard/src/routes/locations/common/hooks/use-shipping-option-price-columns.tsx @@ -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() +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 ( - - ) - }, - }) - }), - ...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 ( - - ) - }, - }) - }), - ] 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]) } diff --git a/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-options-prices-form.tsx b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-options-prices-form.tsx index 3fcaccd99f..75c61ce3fe 100644 --- a/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-options-prices-form.tsx +++ b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-options-prices-form.tsx @@ -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 @@ -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 diff --git a/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-pricing/components/create-shipping-options-form/edit-shipping-options-pricing-form.tsx b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-pricing/components/create-shipping-options-form/edit-shipping-options-pricing-form.tsx index ff0ee54f2a..e36d8fd271 100644 --- a/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-pricing/components/create-shipping-options-form/edit-shipping-options-pricing-form.tsx +++ b/packages/admin-next/dashboard/src/routes/locations/location-service-zone-shipping-option-pricing/components/create-shipping-options-form/edit-shipping-options-pricing-form.tsx @@ -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( diff --git a/packages/admin-next/dashboard/src/routes/price-lists/common/hooks/use-price-list-currency-data.tsx b/packages/admin-next/dashboard/src/routes/price-lists/common/hooks/use-price-list-currency-data.tsx index 91a83d66ef..488bb2ddab 100644 --- a/packages/admin-next/dashboard/src/routes/price-lists/common/hooks/use-price-list-currency-data.tsx +++ b/packages/admin-next/dashboard/src/routes/price-lists/common/hooks/use-price-list-currency-data.tsx @@ -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 } } diff --git a/packages/admin-next/dashboard/src/routes/price-lists/common/hooks/use-price-list-grid-columns.tsx b/packages/admin-next/dashboard/src/routes/price-lists/common/hooks/use-price-list-grid-columns.tsx index 7dc9254804..0e701d6aee 100644 --- a/packages/admin-next/dashboard/src/routes/price-lists/common/hooks/use-price-list-grid-columns.tsx +++ b/packages/admin-next/dashboard/src/routes/price-lists/common/hooks/use-price-list-grid-columns.tsx @@ -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 - } - - return ( - - ) - }, - }) - }), - ...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 - } - - return ( - - ) - }, - }) + ...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 } diff --git a/packages/admin-next/dashboard/src/routes/price-lists/price-list-create/components/price-list-create-form/price-list-create-form.tsx b/packages/admin-next/dashboard/src/routes/price-lists/price-list-create/components/price-list-create-form/price-list-create-form.tsx index 82abdb79aa..fcaeb92865 100644 --- a/packages/admin-next/dashboard/src/routes/price-lists/price-list-create/components/price-list-create-form/price-list-create-form.tsx +++ b/packages/admin-next/dashboard/src/routes/price-lists/price-list-create/components/price-list-create-form/price-list-create-form.tsx @@ -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.DETAIL) const [tabState, setTabState] = useState(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) + const values = fields.reduce( + (acc, key) => { + acc[key] = form.getValues(key) + return acc + }, + {} as Record + ) const validationResult = schema.safeParse(values) @@ -295,6 +300,7 @@ export const PriceListCreateForm = ({ form={form} regions={regions} currencies={currencies} + pricePreferences={pricePreferences} /> diff --git a/packages/admin-next/dashboard/src/routes/price-lists/price-list-create/components/price-list-create-form/price-list-prices-form.tsx b/packages/admin-next/dashboard/src/routes/price-lists/price-list-create/components/price-list-create-form/price-list-prices-form.tsx index 2639fc3c94..fb71fd2011 100644 --- a/packages/admin-next/dashboard/src/routes/price-lists/price-list-create/components/price-list-create-form/price-list-prices-form.tsx +++ b/packages/admin-next/dashboard/src/routes/price-lists/price-list-create/components/price-list-create-form/price-list-prices-form.tsx @@ -12,12 +12,14 @@ type PriceListPricesFormProps = { form: UseFormReturn 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) { diff --git a/packages/admin-next/dashboard/src/routes/price-lists/price-list-create/price-list-create.tsx b/packages/admin-next/dashboard/src/routes/price-lists/price-list-create/price-list-create.tsx index 15a7624534..3ab775488a 100644 --- a/packages/admin-next/dashboard/src/routes/price-lists/price-list-create/price-list-create.tsx +++ b/packages/admin-next/dashboard/src/routes/price-lists/price-list-create/price-list-create.tsx @@ -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 ( {isReady && ( - + )} ) diff --git a/packages/admin-next/dashboard/src/routes/price-lists/price-list-prices-add/components/price-list-prices-add-form/price-list-prices-add-form.tsx b/packages/admin-next/dashboard/src/routes/price-lists/price-list-prices-add/components/price-list-prices-add-form/price-list-prices-add-form.tsx index 1d7640c14c..75a487a2e0 100644 --- a/packages/admin-next/dashboard/src/routes/price-lists/price-list-prices-add/components/price-list-prices-add-form/price-list-prices-add-form.tsx +++ b/packages/admin-next/dashboard/src/routes/price-lists/price-list-prices-add/components/price-list-prices-add-form/price-list-prices-add-form.tsx @@ -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.PRODUCT) const [tabState, setTabState] = useState(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) + const values = fields.reduce( + (acc, key) => { + acc[key] = form.getValues(key) + return acc + }, + {} as Record + ) const validationResult = schema.safeParse(values) @@ -236,6 +241,7 @@ export const PriceListPricesAddForm = ({ form={form} regions={regions} currencies={currencies} + pricePreferences={pricePreferences} /> diff --git a/packages/admin-next/dashboard/src/routes/price-lists/price-list-prices-add/components/price-list-prices-add-form/price-list-prices-add-prices-form.tsx b/packages/admin-next/dashboard/src/routes/price-lists/price-list-prices-add/components/price-list-prices-add-form/price-list-prices-add-prices-form.tsx index bfa690c141..30b428f229 100644 --- a/packages/admin-next/dashboard/src/routes/price-lists/price-list-prices-add/components/price-list-prices-add-form/price-list-prices-add-prices-form.tsx +++ b/packages/admin-next/dashboard/src/routes/price-lists/price-list-prices-add/components/price-list-prices-add-form/price-list-prices-add-prices-form.tsx @@ -13,12 +13,14 @@ type PriceListPricesAddPricesFormProps = { form: UseFormReturn 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) { diff --git a/packages/admin-next/dashboard/src/routes/price-lists/price-list-prices-add/price-list-prices-add.tsx b/packages/admin-next/dashboard/src/routes/price-lists/price-list-prices-add/price-list-prices-add.tsx index 1e2eba76e3..f883114b03 100644 --- a/packages/admin-next/dashboard/src/routes/price-lists/price-list-prices-add/price-list-prices-add.tsx +++ b/packages/admin-next/dashboard/src/routes/price-lists/price-list-prices-add/price-list-prices-add.tsx @@ -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} /> )} diff --git a/packages/admin-next/dashboard/src/routes/price-lists/price-list-prices-edit/components/price-list-prices-edit-form/price-list-prices-edit-form.tsx b/packages/admin-next/dashboard/src/routes/price-lists/price-list-prices-edit/components/price-list-prices-edit-form/price-list-prices-edit-form.tsx index c8a0039b5e..03123f61ed 100644 --- a/packages/admin-next/dashboard/src/routes/price-lists/price-list-prices-edit/components/price-list-prices-edit-form/price-list-prices-edit-form.tsx +++ b/packages/admin-next/dashboard/src/routes/price-lists/price-list-prices-edit/components/price-list-prices-edit-form/price-list-prices-edit-form.tsx @@ -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 ( @@ -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) + const regionCurrencyMap = regions.reduce( + (map, region) => { + map[region.id] = region.currency_code + return map + }, + {} as Record + ) 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) + const initialPriceMap = initialPrices.reduce( + (map, price) => { + map[createMapKey(price)] = price + return map + }, + {} as Record + ) - const newPriceMap = newPrices.reduce((map, price) => { - map[createMapKey(price)] = price - return map - }, {} as Record) + const newPriceMap = newPrices.reduce( + (map, price) => { + map[createMapKey(price)] = price + return map + }, + {} as Record + ) const keys = new Set([ ...Object.keys(initialPriceMap), diff --git a/packages/admin-next/dashboard/src/routes/price-lists/price-list-prices-edit/price-list-prices-edit.tsx b/packages/admin-next/dashboard/src/routes/price-lists/price-list-prices-edit/price-list-prices-edit.tsx index daec61c14f..aafc400b8d 100644 --- a/packages/admin-next/dashboard/src/routes/price-lists/price-list-prices-edit/price-list-prices-edit.tsx +++ b/packages/admin-next/dashboard/src/routes/price-lists/price-list-prices-edit/price-list-prices-edit.tsx @@ -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} /> )} diff --git a/packages/admin-next/dashboard/src/routes/products/common/variant-pricing-form.tsx b/packages/admin-next/dashboard/src/routes/products/common/variant-pricing-form.tsx index 1e55bef829..19380c6044 100644 --- a/packages/admin-next/dashboard/src/routes/products/common/variant-pricing-form.tsx +++ b/packages/admin-next/dashboard/src/routes/products/common/variant-pricing-form.tsx @@ -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 @@ -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 (
- () 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 ( - - ) - }, - }) - }), - ...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 ( - - ) - }, - }) + ...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 } diff --git a/packages/admin-next/dashboard/src/routes/products/product-create/components/product-create-variants-form/product-create-variants-form.tsx b/packages/admin-next/dashboard/src/routes/products/product-create/components/product-create-variants-form/product-create-variants-form.tsx index a09338caa3..e4e7dfd968 100644 --- a/packages/admin-next/dashboard/src/routes/products/product-create/components/product-create-variants-form/product-create-variants-form.tsx +++ b/packages/admin-next/dashboard/src/routes/products/product-create/components/product-create-variants-form/product-create-variants-form.tsx @@ -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 @@ -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 ( - - ) - }, - }) - }), - - ...regions.map((region) => { - return columnHelper.column({ - id: `price_${region.id}`, - name: `Price ${region.name}`, - header: `Price ${region.name}`, - cell: (context) => { - return ( - - ) - }, - }) + ...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] ) } diff --git a/packages/admin-next/dashboard/src/routes/regions/region-create/components/create-region-form/create-region-form.tsx b/packages/admin-next/dashboard/src/routes/regions/region-create/components/create-region-form/create-region-form.tsx index 6312fd41a8..740b3dd638 100644 --- a/packages/admin-next/dashboard/src/routes/regions/region-create/components/create-region-form/create-region-form.tsx +++ b/packages/admin-next/dashboard/src/routes/regions/region-create/components/create-region-form/create-region-form.tsx @@ -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")} -
@@ -304,6 +309,35 @@ export const CreateRegionForm = ({ }} /> + { + return ( + +
+
+ + {t("fields.taxInclusivePricing")} + + + + +
+ + {t("regions.taxInclusiveHint")} + + +
+
+ ) + }} + /> +
diff --git a/packages/admin-next/dashboard/src/routes/regions/region-detail/components/region-general-section/region-general-section.tsx b/packages/admin-next/dashboard/src/routes/regions/region-detail/components/region-general-section/region-general-section.tsx index 24009a4709..c2af73d279 100644 --- a/packages/admin-next/dashboard/src/routes/regions/region-detail/components/region-general-section/region-general-section.tsx +++ b/packages/admin-next/dashboard/src/routes/regions/region-detail/components/region-general-section/region-general-section.tsx @@ -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 ( @@ -23,33 +32,48 @@ export const RegionGeneralSection = ({ region }: RegionGeneralSectionProps) => { {region.name}
-
- - {t("fields.currency")} - -
- - {region.currency_code} - - - {currencies[region.currency_code.toUpperCase()].name} - -
-
-
- - {t("fields.paymentProviders")} - -
- {region.payment_providers?.length > 0 ? ( - formatProvider(p.id))} - /> - ) : ( - "-" - )} -
-
+ + + {region.currency_code} + + + {currencies[region.currency_code.toUpperCase()].name} + +
+ } + /> + + + + + + + {region.payment_providers?.length ? ( + formatProvider(p.id))} + /> + ) : ( + "-" + )} +
+ } + /> ) } diff --git a/packages/admin-next/dashboard/src/routes/regions/region-detail/region-detail.tsx b/packages/admin-next/dashboard/src/routes/regions/region-detail/region-detail.tsx index 431b38475a..b748dbfbb8 100644 --- a/packages/admin-next/dashboard/src/routes/regions/region-detail/region-detail.tsx +++ b/packages/admin-next/dashboard/src/routes/regions/region-detail/region-detail.tsx @@ -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
Loading...
} - if (isError) { - throw error + if (isRegionError) { + throw regionError + } + + if (isPreferencesError) { + throw preferencesError } return ( @@ -46,7 +64,10 @@ export const RegionDetail = () => { ) })} - + {after.widgets.map((w, i) => { return ( diff --git a/packages/admin-next/dashboard/src/routes/regions/region-edit/components/edit-region-form/edit-region-form.tsx b/packages/admin-next/dashboard/src/routes/regions/region-edit/components/edit-region-form/edit-region-form.tsx index 7fc950afe8..3c5ea2d50a 100644 --- a/packages/admin-next/dashboard/src/routes/regions/region-edit/components/edit-region-form/edit-region-form.tsx +++ b/packages/admin-next/dashboard/src/routes/regions/region-edit/components/edit-region-form/edit-region-form.tsx @@ -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>({ 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 = ({ }} /> +
+ { + return ( + +
+
+ {t("fields.automaticTaxes")} + + + +
+ {t("regions.automaticTaxesHint")} + +
+
+ ) + }} + /> + + { + return ( + +
+
+ + {t("fields.taxInclusivePricing")} + + + + +
+ {t("regions.taxInclusiveHint")} + +
+
+ ) + }} + /> +
@@ -157,7 +222,7 @@ export const EditRegionForm = ({ {t("actions.cancel")} -
diff --git a/packages/admin-next/dashboard/src/routes/regions/region-edit/region-edit.tsx b/packages/admin-next/dashboard/src/routes/regions/region-edit/region-edit.tsx index 23547a7cec..96353416a5 100644 --- a/packages/admin-next/dashboard/src/routes/regions/region-edit/region-edit.tsx +++ b/packages/admin-next/dashboard/src/routes/regions/region-edit/region-edit.tsx @@ -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 ( @@ -54,6 +72,7 @@ export const RegionEdit = () => { region={region} currencies={storeCurrencies} paymentProviders={paymentProviders} + pricePreferences={pricePreferences} /> )} diff --git a/packages/core/core-flows/src/region/workflows/create-regions.ts b/packages/core/core-flows/src/region/workflows/create-regions.ts index 03d25b8781..48c3b14f71 100644 --- a/packages/core/core-flows/src/region/workflows/create-regions.ts +++ b/packages/core/core-flows/src/region/workflows/create-regions.ts @@ -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 ): WorkflowData => { 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 } diff --git a/packages/core/core-flows/src/region/workflows/update-regions.ts b/packages/core/core-flows/src/region/workflows/update-regions.ts index d5be031094..d0f8d58cb7 100644 --- a/packages/core/core-flows/src/region/workflows/update-regions.ts +++ b/packages/core/core-flows/src/region/workflows/update-regions.ts @@ -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 ): WorkflowData => { - 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, }) diff --git a/packages/core/js-sdk/src/admin/index.ts b/packages/core/js-sdk/src/admin/index.ts index 50b18618cb..3f95ed5e1c 100644 --- a/packages/core/js-sdk/src/admin/index.ts +++ b/packages/core/js-sdk/src/admin/index.ts @@ -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) diff --git a/packages/core/js-sdk/src/admin/price-preference.ts b/packages/core/js-sdk/src/admin/price-preference.ts new file mode 100644 index 0000000000..86c38b275f --- /dev/null +++ b/packages/core/js-sdk/src/admin/price-preference.ts @@ -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( + `/admin/price-preferences/${id}`, + { + method: "GET", + headers, + query, + } + ) + } + + async list( + query?: HttpTypes.AdminPricePreferenceListParams, + headers?: ClientHeaders + ) { + return this.client.fetch( + `/admin/price-preferences`, + { + method: "GET", + headers, + query, + } + ) + } + + async create( + body: HttpTypes.AdminCreatePricePreference, + query?: HttpTypes.AdminPricePreferenceParams, + headers?: ClientHeaders + ) { + return this.client.fetch( + `/admin/price-preferences`, + { + method: "POST", + headers, + body, + query, + } + ) + } + + async update( + id: string, + body: HttpTypes.AdminUpdatePricePreference, + query?: HttpTypes.AdminPricePreferenceParams, + headers?: ClientHeaders + ) { + return this.client.fetch( + `/admin/price-preferences/${id}`, + { + method: "POST", + headers, + body, + query, + } + ) + } + + async delete(id: string, headers?: ClientHeaders) { + return this.client.fetch( + `/admin/price-preferences/${id}`, + { + method: "DELETE", + headers, + } + ) + } +} diff --git a/packages/core/types/src/http/region/admin.ts b/packages/core/types/src/http/region/admin.ts index 2cdb71fdce..17aab95b3d 100644 --- a/packages/core/types/src/http/region/admin.ts +++ b/packages/core/types/src/http/region/admin.ts @@ -15,6 +15,7 @@ export interface AdminCreateRegion { currency_code: string countries?: string[] automatic_taxes?: boolean + is_tax_inclusive?: boolean payment_providers?: string[] metadata?: Record } @@ -24,6 +25,7 @@ export interface AdminUpdateRegion { currency_code?: string countries?: string[] automatic_taxes?: boolean + is_tax_inclusive?: boolean payment_providers?: string[] metadata?: Record } diff --git a/packages/core/types/src/workflow/region/create-regions.ts b/packages/core/types/src/workflow/region/create-regions.ts index 8a990449fd..9c008d3fb3 100644 --- a/packages/core/types/src/workflow/region/create-regions.ts +++ b/packages/core/types/src/workflow/region/create-regions.ts @@ -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[] diff --git a/packages/core/types/src/workflow/region/update-regions.ts b/packages/core/types/src/workflow/region/update-regions.ts index 1c417df0d5..1749f5d03e 100644 --- a/packages/core/types/src/workflow/region/update-regions.ts +++ b/packages/core/types/src/workflow/region/update-regions.ts @@ -3,6 +3,7 @@ import { FilterableRegionProps, RegionDTO, UpdateRegionDTO } from "../../region" export interface UpdateRegionsWorkflowInput { selector: FilterableRegionProps update: UpdateRegionDTO & { + is_tax_inclusive?: boolean payment_providers?: string[] } } diff --git a/packages/medusa/src/api/admin/price-preferences/validators.ts b/packages/medusa/src/api/admin/price-preferences/validators.ts index 166fdcdc84..a6cf677be4 100644 --- a/packages/medusa/src/api/admin/price-preferences/validators.ts +++ b/packages/medusa/src/api/admin/price-preferences/validators.ts @@ -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(), diff --git a/packages/medusa/src/api/admin/regions/validators.ts b/packages/medusa/src/api/admin/regions/validators.ts index 2dbaa73d9d..11dd233e3a 100644 --- a/packages/medusa/src/api/admin/regions/validators.ts +++ b/packages/medusa/src/api/admin/regions/validators.ts @@ -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(), })