diff --git a/.changeset/wicked-days-buy.md b/.changeset/wicked-days-buy.md
new file mode 100644
index 0000000000..5d0ff80ff7
--- /dev/null
+++ b/.changeset/wicked-days-buy.md
@@ -0,0 +1,6 @@
+---
+"@medusajs/medusa": patch
+"@medusajs/types": patch
+---
+
+feat(medusa,types): added tax flows end to end
diff --git a/integration-tests/modules/__tests__/tax/admin/tax.spec.ts b/integration-tests/modules/__tests__/tax/admin/tax.spec.ts
index 6172f4da11..8ce08d417b 100644
--- a/integration-tests/modules/__tests__/tax/admin/tax.spec.ts
+++ b/integration-tests/modules/__tests__/tax/admin/tax.spec.ts
@@ -1,8 +1,8 @@
-import { ITaxModuleService } from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
+import { ITaxModuleService } from "@medusajs/types"
-import { createAdminUser } from "../../../../helpers/create-admin-user"
import { medusaIntegrationTestRunner } from "medusa-test-utils"
+import { createAdminUser } from "../../../../helpers/create-admin-user"
jest.setTimeout(50000)
@@ -58,6 +58,8 @@ medusaIntegrationTestRunner({
updated_at: expect.any(String),
deleted_at: null,
created_by: null,
+ rules: [],
+ tax_region: expect.any(Object),
},
})
})
@@ -91,6 +93,9 @@ medusaIntegrationTestRunner({
created_by: "admin_user",
provider_id: null,
metadata: null,
+ children: [],
+ parent: null,
+ tax_rates: expect.any(Array),
},
})
@@ -121,6 +126,13 @@ medusaIntegrationTestRunner({
deleted_at: null,
created_by: "admin_user",
is_combinable: false,
+ tax_region: expect.any(Object),
+ rules: [
+ expect.objectContaining({
+ reference: "product",
+ reference_id: "prod_1234",
+ }),
+ ],
},
})
@@ -147,6 +159,11 @@ medusaIntegrationTestRunner({
created_by: "admin_user",
metadata: null,
provider_id: null,
+ children: [],
+ tax_rates: [],
+ parent: expect.objectContaining({
+ id: usRegionId,
+ }),
},
})
@@ -219,6 +236,9 @@ medusaIntegrationTestRunner({
created_by: "admin_user",
provider_id: null,
metadata: null,
+ children: [],
+ parent: null,
+ tax_rates: expect.any(Array),
},
})
@@ -249,6 +269,13 @@ medusaIntegrationTestRunner({
deleted_at: null,
created_by: "admin_user",
is_combinable: false,
+ tax_region: expect.any(Object),
+ rules: [
+ expect.objectContaining({
+ reference: "product",
+ reference_id: "prod_1234",
+ }),
+ ],
},
})
@@ -279,6 +306,13 @@ medusaIntegrationTestRunner({
updated_at: expect.any(String),
created_by: "admin_user",
is_combinable: true,
+ tax_region: expect.any(Object),
+ rules: [
+ expect.objectContaining({
+ reference: "product",
+ reference_id: "prod_1234",
+ }),
+ ],
},
})
})
@@ -331,6 +365,43 @@ medusaIntegrationTestRunner({
expect(rates[0].deleted_at).not.toBeNull()
})
+ it("can retrieve a tax region", async () => {
+ const region = await service.createTaxRegions({
+ country_code: "us",
+ })
+
+ const rate = await service.create({
+ tax_region_id: region.id,
+ code: "test",
+ rate: 2.5,
+ name: "Test Rate",
+ })
+
+ const response = await api.get(
+ `/admin/tax-regions/${region.id}`,
+ adminHeaders
+ )
+
+ expect(response.status).toEqual(200)
+ expect(response.data).toEqual({
+ tax_region: {
+ id: region.id,
+ country_code: "us",
+ province_code: null,
+ parent_id: null,
+ provider_id: null,
+ created_by: null,
+ created_at: expect.any(String),
+ updated_at: expect.any(String),
+ tax_rates: expect.any(Array),
+ deleted_at: null,
+ metadata: null,
+ children: [],
+ parent: null,
+ },
+ })
+ })
+
it("can create a tax region and delete it", async () => {
const regionRes = await api.post(
`/admin/tax-regions`,
diff --git a/packages/admin-next/dashboard/public/locales/$schema.json b/packages/admin-next/dashboard/public/locales/$schema.json
index 98af3952a9..7214957f47 100644
--- a/packages/admin-next/dashboard/public/locales/$schema.json
+++ b/packages/admin-next/dashboard/public/locales/$schema.json
@@ -140,6 +140,27 @@
},
"required": ["domain"]
},
+ "taxRegions": {
+ "type": "object",
+ "properties": {
+ "domain": {
+ "type": "string"
+ }
+ },
+ "required": ["domain"]
+ },
+ "taxRates": {
+ "type": "object",
+ "properties": {
+ "domain": {
+ "type": "string"
+ },
+ "fields": {
+ "type": "object"
+ }
+ },
+ "required": ["domain"]
+ },
"campaigns": {
"type": "object",
"properties": {
diff --git a/packages/admin-next/dashboard/public/locales/en-US/translation.json b/packages/admin-next/dashboard/public/locales/en-US/translation.json
index ead37f3bec..9f7cf35383 100644
--- a/packages/admin-next/dashboard/public/locales/en-US/translation.json
+++ b/packages/admin-next/dashboard/public/locales/en-US/translation.json
@@ -633,6 +633,49 @@
"disabled": "Disabled"
}
},
+ "taxRates": {
+ "domain": "Tax Rates",
+ "fields": {
+ "isCombinable": "Is combinable?",
+ "appliesTo": "Tax Rate applies to",
+ "customer_groups": "Customer Group",
+ "product_collections": "Product Collection",
+ "product_tags": "Product Tag",
+ "product_types": "Product Type",
+ "products": "Product"
+ },
+ "edit": {
+ "title": "Edit Tax Rate",
+ "description": "Edits tax rate for a tax region"
+ },
+ "create": {
+ "title": "Create Tax Rate Override",
+ "description": "Creates tax rate overrides for a tax region"
+ }
+ },
+ "taxRegions": {
+ "domain": "Tax Regions",
+ "description": "Manage your region's tax structure",
+ "create": {
+ "title": "Create Tax Region",
+ "description": "Creates a tax region with default tax rate"
+ },
+ "create-child": {
+ "title": "Create Default Rate for Province",
+ "description": "Creates a tax region for a province with default tax rate"
+ },
+ "removeWarning": "You are about to remove {{tax_region_name}}. This action cannot be undone.",
+ "fields": {
+ "rate": {
+ "name": "Rate",
+ "hint": "Tax rate to apply for a region or province"
+ },
+ "is_combinable": {
+ "name": "Is combinable",
+ "hint": "If this tax rate can be combined with the default rate from province or parent"
+ }
+ }
+ },
"promotions": {
"domain": "Promotions",
"fields": {
@@ -907,6 +950,7 @@
},
"taxes": {
"domain": "Tax Regions",
+ "domainDescription": "Manage your tax region",
"countries": {
"taxCountriesHint": "Tax settings apply to the listed countries."
},
@@ -1123,6 +1167,7 @@
"amount": "Amount",
"refundAmount": "Refund amount",
"name": "Name",
+ "default": "Default",
"lastName": "Last Name",
"firstName": "First Name",
"title": "Title",
@@ -1247,6 +1292,7 @@
"quantity": "Quantity",
"variant": "Variant",
"id": "ID",
+ "parent": "Parent",
"minSubtotal": "Min. Subtotal",
"maxSubtotal": "Max. Subtotal",
"shippingProfile": "Shipping Profile",
diff --git a/packages/admin-next/dashboard/src/components/common/date/index.ts b/packages/admin-next/dashboard/src/components/common/date/index.ts
new file mode 100644
index 0000000000..98cb13cafc
--- /dev/null
+++ b/packages/admin-next/dashboard/src/components/common/date/index.ts
@@ -0,0 +1,11 @@
+import format from "date-fns/format"
+
+export function formatDate(date: string | Date) {
+ const value = new Date(date)
+ value.setMinutes(value.getMinutes() - value.getTimezoneOffset())
+
+ const hour12 = Intl.DateTimeFormat().resolvedOptions().hour12
+ const timestampFormat = hour12 ? "dd MMM yyyy hh:MM a" : "dd MMM yyyy HH:MM"
+
+ return format(value, timestampFormat)
+}
diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-query/data-table-query.tsx b/packages/admin-next/dashboard/src/components/table/data-table/data-table-query/data-table-query.tsx
index 60cfe1ba0e..6cf99ee531 100644
--- a/packages/admin-next/dashboard/src/components/table/data-table/data-table-query/data-table-query.tsx
+++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-query/data-table-query.tsx
@@ -17,16 +17,18 @@ export const DataTableQuery = ({
prefix,
}: DataTableQueryProps) => {
return (
-
-
- {filters && filters.length > 0 && (
-
- )}
+ (search || orderBy || filters || prefix) && (
+
+
+ {filters && filters.length > 0 && (
+
+ )}
+
+
+ {search && }
+ {orderBy && }
+
-
- {search && }
- {orderBy && }
-
-
+ )
)
}
diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/taxes/type-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/taxes/type-cell/index.ts
new file mode 100644
index 0000000000..4db84006ba
--- /dev/null
+++ b/packages/admin-next/dashboard/src/components/table/table-cells/taxes/type-cell/index.ts
@@ -0,0 +1 @@
+export * from "./type-cell"
diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/taxes/type-cell/type-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/taxes/type-cell/type-cell.tsx
new file mode 100644
index 0000000000..15b41543e0
--- /dev/null
+++ b/packages/admin-next/dashboard/src/components/table/table-cells/taxes/type-cell/type-cell.tsx
@@ -0,0 +1,33 @@
+import { Badge } from "@medusajs/ui"
+
+type CellProps = {
+ is_combinable: boolean
+}
+
+type HeaderProps = {
+ text: string
+}
+
+export const TypeCell = ({ is_combinable }: CellProps) => {
+ return (
+
+
+ {is_combinable ? (
+
+ Combinable
+
+ ) : (
+ ""
+ )}
+
+
+ )
+}
+
+export const TypeHeader = ({ text }: HeaderProps) => {
+ return (
+
+ {text}
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/hooks/api/tax-rates.tsx b/packages/admin-next/dashboard/src/hooks/api/tax-rates.tsx
new file mode 100644
index 0000000000..01983f2151
--- /dev/null
+++ b/packages/admin-next/dashboard/src/hooks/api/tax-rates.tsx
@@ -0,0 +1,119 @@
+import {
+ AdminPostTaxRatesReq,
+ AdminPostTaxRatesTaxRateReq,
+} from "@medusajs/medusa"
+import { AdminTaxRateListResponse, AdminTaxRateResponse } from "@medusajs/types"
+import {
+ QueryKey,
+ useMutation,
+ UseMutationOptions,
+ useQuery,
+ UseQueryOptions,
+} from "@tanstack/react-query"
+import { client } from "../../lib/client"
+import { queryClient } from "../../lib/medusa"
+import { queryKeysFactory } from "../../lib/query-key-factory"
+import { TaxRateDeleteRes } from "../../types/api-responses"
+
+const TAX_RATES_QUERY_KEY = "tax_rates" as const
+export const taxRatesQueryKeys = queryKeysFactory(TAX_RATES_QUERY_KEY)
+
+export const useTaxRate = (
+ id: string,
+ query?: Record
,
+ options?: Omit<
+ UseQueryOptions<
+ AdminTaxRateResponse,
+ Error,
+ AdminTaxRateResponse,
+ QueryKey
+ >,
+ "queryFn" | "queryKey"
+ >
+) => {
+ const { data, ...rest } = useQuery({
+ queryKey: taxRatesQueryKeys.detail(id),
+ queryFn: async () => client.taxes.retrieveTaxRate(id, query),
+ ...options,
+ })
+
+ return { ...data, ...rest }
+}
+
+export const useTaxRates = (
+ query?: Record,
+ options?: Omit<
+ UseQueryOptions<
+ AdminTaxRateListResponse,
+ Error,
+ AdminTaxRateListResponse,
+ QueryKey
+ >,
+ "queryFn" | "queryKey"
+ >
+) => {
+ const { data, ...rest } = useQuery({
+ queryFn: () => client.taxes.listTaxRates(query),
+ queryKey: taxRatesQueryKeys.list(query),
+ ...options,
+ })
+
+ return { ...data, ...rest }
+}
+
+export const useUpdateTaxRate = (
+ id: string,
+ options?: UseMutationOptions<
+ AdminTaxRateResponse,
+ Error,
+ AdminPostTaxRatesTaxRateReq
+ >
+) => {
+ return useMutation({
+ mutationFn: (payload) => client.taxes.updateTaxRate(id, payload),
+ onSuccess: (data, variables, context) => {
+ queryClient.invalidateQueries({ queryKey: taxRatesQueryKeys.lists() })
+ queryClient.invalidateQueries({
+ queryKey: taxRatesQueryKeys.detail(id),
+ })
+
+ options?.onSuccess?.(data, variables, context)
+ },
+ ...options,
+ })
+}
+
+export const useCreateTaxRate = (
+ options?: UseMutationOptions<
+ AdminTaxRateResponse,
+ Error,
+ AdminPostTaxRatesReq
+ >
+) => {
+ return useMutation({
+ mutationFn: (payload) => client.taxes.createTaxRate(payload),
+ onSuccess: (data, variables, context) => {
+ queryClient.invalidateQueries({ queryKey: taxRatesQueryKeys.lists() })
+ options?.onSuccess?.(data, variables, context)
+ },
+ ...options,
+ })
+}
+
+export const useDeleteTaxRate = (
+ id: string,
+ options?: UseMutationOptions
+) => {
+ return useMutation({
+ mutationFn: () => client.taxes.deleteTaxRate(id),
+ onSuccess: (data, variables, context) => {
+ queryClient.invalidateQueries({ queryKey: taxRatesQueryKeys.lists() })
+ queryClient.invalidateQueries({
+ queryKey: taxRatesQueryKeys.detail(id),
+ })
+
+ options?.onSuccess?.(data, variables, context)
+ },
+ ...options,
+ })
+}
diff --git a/packages/admin-next/dashboard/src/hooks/api/tax-regions.tsx b/packages/admin-next/dashboard/src/hooks/api/tax-regions.tsx
index b98230d3d7..6c516242fa 100644
--- a/packages/admin-next/dashboard/src/hooks/api/tax-regions.tsx
+++ b/packages/admin-next/dashboard/src/hooks/api/tax-regions.tsx
@@ -1,13 +1,22 @@
-import { QueryKey, UseQueryOptions, useQuery } from "@tanstack/react-query"
-import { client } from "../../lib/client"
-import { queryKeysFactory } from "../../lib/query-key-factory"
+import { AdminCreateTaxRegion } from "@medusajs/medusa"
import {
- AdminTaxRegionResponse,
AdminTaxRegionListResponse,
+ AdminTaxRegionResponse,
} from "@medusajs/types"
+import {
+ QueryKey,
+ UseMutationOptions,
+ UseQueryOptions,
+ useMutation,
+ useQuery,
+} from "@tanstack/react-query"
+import { client } from "../../lib/client"
+import { queryClient } from "../../lib/medusa"
+import { queryKeysFactory } from "../../lib/query-key-factory"
+import { TaxRegionDeleteRes } from "../../types/api-responses"
const TAX_REGIONS_QUERY_KEY = "tax_regions" as const
-const taxRegionsQueryKeys = queryKeysFactory(TAX_REGIONS_QUERY_KEY)
+export const taxRegionsQueryKeys = queryKeysFactory(TAX_REGIONS_QUERY_KEY)
export const useTaxRegion = (
id: string,
@@ -51,3 +60,38 @@ export const useTaxRegions = (
return { ...data, ...rest }
}
+
+export const useCreateTaxRegion = (
+ options?: UseMutationOptions<
+ AdminTaxRegionResponse,
+ Error,
+ AdminCreateTaxRegion
+ >
+) => {
+ return useMutation({
+ mutationFn: (payload) => client.taxes.createTaxRegion(payload),
+ onSuccess: (data, variables, context) => {
+ queryClient.invalidateQueries({ queryKey: taxRegionsQueryKeys.all })
+ options?.onSuccess?.(data, variables, context)
+ },
+ ...options,
+ })
+}
+
+export const useDeleteTaxRegion = (
+ id: string,
+ options?: UseMutationOptions
+) => {
+ return useMutation({
+ mutationFn: () => client.taxes.deleteTaxRegion(id),
+ onSuccess: (data, variables, context) => {
+ queryClient.invalidateQueries({ queryKey: taxRegionsQueryKeys.lists() })
+ queryClient.invalidateQueries({
+ queryKey: taxRegionsQueryKeys.detail(id),
+ })
+
+ options?.onSuccess?.(data, variables, context)
+ },
+ ...options,
+ })
+}
diff --git a/packages/admin-next/dashboard/src/hooks/table/columns/use-tax-rates-table-columns.tsx b/packages/admin-next/dashboard/src/hooks/table/columns/use-tax-rates-table-columns.tsx
new file mode 100644
index 0000000000..2e96600a36
--- /dev/null
+++ b/packages/admin-next/dashboard/src/hooks/table/columns/use-tax-rates-table-columns.tsx
@@ -0,0 +1,55 @@
+import { createColumnHelper } from "@tanstack/react-table"
+import { useMemo } from "react"
+import { useTranslation } from "react-i18next"
+
+import { TaxRateResponse } from "@medusajs/types"
+import {
+ TextCell,
+ TextHeader,
+} from "../../../components/table/table-cells/common/text-cell"
+
+import {
+ TypeCell,
+ TypeHeader,
+} from "../../../components/table/table-cells/taxes/type-cell"
+
+const columnHelper = createColumnHelper()
+
+export const useTaxRateTableColumns = () => {
+ const { t } = useTranslation()
+
+ return useMemo(
+ () => [
+ columnHelper.display({
+ id: "name",
+ header: () => ,
+ cell: ({ row }) => ,
+ }),
+ columnHelper.display({
+ id: "province",
+ header: () => ,
+ cell: ({ row }) => (
+
+ ),
+ }),
+ columnHelper.display({
+ id: "rate",
+ header: () => ,
+ cell: ({ row }) => ,
+ }),
+ columnHelper.display({
+ id: "is_combinable",
+ header: () => ,
+ cell: ({ row }) => (
+
+ ),
+ }),
+ columnHelper.display({
+ id: "code",
+ header: () => ,
+ cell: ({ row }) => ,
+ }),
+ ],
+ []
+ )
+}
diff --git a/packages/admin-next/dashboard/src/hooks/table/filters/use-tax-rate-table-filters.tsx b/packages/admin-next/dashboard/src/hooks/table/filters/use-tax-rate-table-filters.tsx
new file mode 100644
index 0000000000..2a21625940
--- /dev/null
+++ b/packages/admin-next/dashboard/src/hooks/table/filters/use-tax-rate-table-filters.tsx
@@ -0,0 +1,19 @@
+import { useTranslation } from "react-i18next"
+import { Filter } from "../../../components/table/data-table"
+
+export const useTaxRateTableFilters = () => {
+ const { t } = useTranslation()
+
+ const dateFilters: Filter[] = [
+ { label: t("fields.createdAt"), key: "created_at" },
+ { label: t("fields.updatedAt"), key: "updated_at" },
+ ].map((f) => ({
+ key: f.key,
+ label: f.label,
+ type: "date",
+ }))
+
+ const filters = [...dateFilters]
+
+ return filters
+}
diff --git a/packages/admin-next/dashboard/src/hooks/table/query/use-tax-rate-table-query.tsx b/packages/admin-next/dashboard/src/hooks/table/query/use-tax-rate-table-query.tsx
new file mode 100644
index 0000000000..04fa145750
--- /dev/null
+++ b/packages/admin-next/dashboard/src/hooks/table/query/use-tax-rate-table-query.tsx
@@ -0,0 +1,33 @@
+import { AdminGetTaxRatesParams } from "@medusajs/medusa"
+import { useQueryParams } from "../../use-query-params"
+
+type UseTaxRateTableQueryProps = {
+ prefix?: string
+ pageSize?: number
+}
+
+export const useTaxRateTableQuery = ({
+ prefix,
+ pageSize = 20,
+}: UseTaxRateTableQueryProps) => {
+ const queryObject = useQueryParams(
+ ["offset", "q", "order", "created_at", "updated_at"],
+ prefix
+ )
+
+ const { offset, q, order, created_at, updated_at } = queryObject
+
+ const searchParams: AdminGetTaxRatesParams = {
+ limit: pageSize,
+ offset: offset ? Number(offset) : 0,
+ order,
+ created_at: created_at ? JSON.parse(created_at) : undefined,
+ updated_at: updated_at ? JSON.parse(updated_at) : undefined,
+ q,
+ }
+
+ return {
+ searchParams,
+ raw: queryObject,
+ }
+}
diff --git a/packages/admin-next/dashboard/src/lib/client/taxes.ts b/packages/admin-next/dashboard/src/lib/client/taxes.ts
index 4edb7f0aac..6eb9d4d7c5 100644
--- a/packages/admin-next/dashboard/src/lib/client/taxes.ts
+++ b/packages/admin-next/dashboard/src/lib/client/taxes.ts
@@ -1,8 +1,14 @@
import {
+ AdminCreateTaxRegion,
+ AdminPostTaxRatesTaxRateReq,
+} from "@medusajs/medusa"
+import {
+ AdminTaxRateResponse,
AdminTaxRegionListResponse,
AdminTaxRegionResponse,
} from "@medusajs/types"
-import { getRequest } from "./common"
+import { TaxRateDeleteRes, TaxRegionDeleteRes } from "../../types/api-responses"
+import { deleteRequest, getRequest, postRequest } from "./common"
async function retrieveTaxRegion(id: string, query?: Record) {
return getRequest(`/admin/tax-regions/${id}`, query)
@@ -12,7 +18,42 @@ async function listTaxRegions(query?: Record) {
return getRequest(`/admin/tax-regions`, query)
}
+async function createTaxRegion(payload: AdminCreateTaxRegion) {
+ return postRequest(`/admin/tax-regions`, payload)
+}
+
+async function deleteTaxRegion(id: string) {
+ return deleteRequest(`/admin/tax-regions/${id}`)
+}
+
+async function retrieveTaxRate(id: string, query?: Record) {
+ return getRequest(`/admin/tax-rates/${id}`, query)
+}
+
+async function listTaxRates(query?: Record) {
+ return getRequest(`/admin/tax-rates`, query)
+}
+
+async function updateTaxRate(id: string, payload: AdminPostTaxRatesTaxRateReq) {
+ return postRequest(`/admin/tax-rates/${id}`, payload)
+}
+
+async function createTaxRate(payload: AdminCreateTaxRate) {
+ return postRequest(`/admin/tax-rates`, payload)
+}
+
+async function deleteTaxRate(id: string) {
+ return deleteRequest(`/admin/tax-rates/${id}`)
+}
+
export const taxes = {
retrieveTaxRegion,
listTaxRegions,
+ retrieveTaxRate,
+ listTaxRates,
+ updateTaxRate,
+ createTaxRegion,
+ deleteTaxRegion,
+ createTaxRate,
+ deleteTaxRate,
}
diff --git a/packages/admin-next/dashboard/src/lib/countries.ts b/packages/admin-next/dashboard/src/lib/countries.ts
index 62ce8cea73..bf1fa276d9 100644
--- a/packages/admin-next/dashboard/src/lib/countries.ts
+++ b/packages/admin-next/dashboard/src/lib/countries.ts
@@ -1,6 +1,16 @@
/** This file is auto-generated. Do not modify it manually. */
import type { RegionCountryDTO } from "@medusajs/types"
+export function getCountryByIso2(
+ iso2: string | null
+): Omit | undefined {
+ if (!iso2) {
+ return
+ }
+
+ return countries.find((c) => c.iso_2 === iso2)
+}
+
export const countries: Omit[] = [
{
iso_2: "af",
diff --git a/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx b/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx
index 9505e090bc..0c4b2bf749 100644
--- a/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx
+++ b/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx
@@ -9,6 +9,8 @@ import {
AdminApiKeyResponse,
AdminCustomerGroupResponse,
AdminProductCategoryResponse,
+ AdminTaxRateResponse,
+ AdminTaxRegionResponse,
SalesChannelDTO,
UserDTO,
} from "@medusajs/types"
@@ -757,7 +759,56 @@ export const v2Routes: RouteObject[] = [
{
path: "",
lazy: () => import("../../v2-routes/taxes/tax-region-list"),
- children: [],
+ children: [
+ {
+ path: "create",
+ lazy: () =>
+ import("../../v2-routes/taxes/tax-region-create"),
+ children: [],
+ },
+ ],
+ },
+ {
+ path: ":id",
+ lazy: () => import("../../v2-routes/taxes/tax-region-detail"),
+ handle: {
+ crumb: (data: AdminTaxRegionResponse) => {
+ return data.tax_region.country_code
+ },
+ },
+ children: [
+ {
+ path: "create-default",
+ lazy: () =>
+ import("../../v2-routes/taxes/tax-province-create"),
+ children: [],
+ },
+ {
+ path: "create-override",
+ lazy: () => import("../../v2-routes/taxes/tax-rate-create"),
+ children: [],
+ },
+ {
+ path: "tax-rates",
+ children: [
+ {
+ path: ":taxRateId",
+ children: [
+ {
+ path: "edit",
+ lazy: () =>
+ import("../../v2-routes/taxes/tax-rate-edit"),
+ handle: {
+ crumb: (data: AdminTaxRateResponse) => {
+ return data.tax_rate.code
+ },
+ },
+ },
+ ],
+ },
+ ],
+ },
+ ],
},
],
},
diff --git a/packages/admin-next/dashboard/src/types/api-responses.ts b/packages/admin-next/dashboard/src/types/api-responses.ts
index 6bdfaa7ddb..ffbf536684 100644
--- a/packages/admin-next/dashboard/src/types/api-responses.ts
+++ b/packages/admin-next/dashboard/src/types/api-responses.ts
@@ -142,6 +142,10 @@ export type ProductCollectionListRes = {
} & ListRes
export type ProductCollectionDeleteRes = DeleteRes
+// Taxes
+export type TaxRegionDeleteRes = DeleteRes
+export type TaxRateDeleteRes = DeleteRes
+
// Inventory Items
export type InventoryItemRes = {
inventory_item: InventoryNext.InventoryItemDTO & {
diff --git a/packages/admin-next/dashboard/src/v2-routes/regions/region-list/components/region-list-table/region-list-table.tsx b/packages/admin-next/dashboard/src/v2-routes/regions/region-list/components/region-list-table/region-list-table.tsx
index b07fef8289..d037b1fc2b 100644
--- a/packages/admin-next/dashboard/src/v2-routes/regions/region-list/components/region-list-table/region-list-table.tsx
+++ b/packages/admin-next/dashboard/src/v2-routes/regions/region-list/components/region-list-table/region-list-table.tsx
@@ -1,18 +1,18 @@
import { PencilSquare, Trash } from "@medusajs/icons"
+import { RegionDTO } from "@medusajs/types"
import { Button, Container, Heading, usePrompt } from "@medusajs/ui"
import { createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom"
-import { RegionDTO } from "@medusajs/types"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { DataTable } from "../../../../../components/table/data-table"
+import { useDeleteRegion, useRegions } from "../../../../../hooks/api/regions"
import { useRegionTableColumns } from "../../../../../hooks/table/columns/use-region-table-columns"
import { useRegionTableFilters } from "../../../../../hooks/table/filters/use-region-table-filters"
import { useRegionTableQuery } from "../../../../../hooks/table/query/use-region-table-query"
import { useDataTable } from "../../../../../hooks/use-data-table"
-import { useDeleteRegion, useRegions } from "../../../../../hooks/api/regions"
const PAGE_SIZE = 20
@@ -56,6 +56,7 @@ export const RegionListTable = () => {
+
void
+}
+
+const initRowState = (selected: ConditionsOption[] = []): RowSelectionState => {
+ return selected.reduce((acc, { value }) => {
+ acc[value] = true
+ return acc
+ }, {} as RowSelectionState)
+}
+
+const ConditionsFooter = ({ onSave }: { onSave: () => void }) => {
+ const { t } = useTranslation()
+
+ return (
+
+
+
+
+
+
+
+ )
+}
+
+const ProductConditionsTable = ({ selected = [], onSave }: ConditionsProps) => {
+ const [rowSelection, setRowSelection] = useState(
+ initRowState(selected)
+ )
+
+ const [intermediate, setIntermediate] = useState(selected)
+
+ const { searchParams, raw } = useProductTableQuery({
+ pageSize: PAGE_SIZE,
+ prefix: PRODUCT_PREFIX,
+ })
+ const { products, count, isLoading, isError, error } = useProducts(
+ {
+ ...searchParams,
+ },
+ {
+ keepPreviousData: true,
+ }
+ )
+
+ const updater: OnChangeFn = (fn) => {
+ const newState: RowSelectionState =
+ typeof fn === "function" ? fn(rowSelection) : fn
+
+ const added = Object.keys(newState).filter(
+ (k) => newState[k] !== rowSelection[k]
+ )
+
+ if (added.length) {
+ const addedProducts = (products?.filter((p) => added.includes(p.id!)) ??
+ []) as Product[]
+
+ if (addedProducts.length > 0) {
+ const newConditions = addedProducts.map((p) => ({
+ label: p.title,
+ value: p.id!,
+ }))
+
+ setIntermediate((prev) => {
+ const filteredPrev = prev.filter((p) => p.value in newState)
+ return Array.from(new Set([...filteredPrev, ...newConditions]))
+ })
+ }
+
+ setRowSelection(newState)
+ }
+
+ const removed = Object.keys(rowSelection).filter(
+ (k) => newState[k] !== rowSelection[k]
+ )
+
+ if (removed.length) {
+ setIntermediate((prev) => {
+ return prev.filter((p) => !removed.includes(p.value))
+ })
+
+ setRowSelection(newState)
+ }
+ }
+
+ const handleSave = () => {
+ onSave(intermediate)
+ }
+
+ const columns = useProductConditionsTableColumns()
+ const filters = useProductTableFilters()
+
+ const { table } = useDataTable({
+ data: products ?? [],
+ columns: columns,
+ count,
+ enablePagination: true,
+ getRowId: (row) => row.id,
+ pageSize: PAGE_SIZE,
+ enableRowSelection: true,
+ rowSelection: {
+ state: rowSelection,
+ updater,
+ },
+ prefix: PRODUCT_PREFIX,
+ })
+
+ if (isError) {
+ throw error
+ }
+
+ return (
+
+
+
+
+ )
+}
+
+const CustomerGroupConditionsTable = ({
+ selected = [],
+ onSave,
+}: ConditionsProps) => {
+ const [rowSelection, setRowSelection] = useState(
+ initRowState(selected)
+ )
+
+ const [intermediate, setIntermediate] = useState(selected)
+
+ const { searchParams, raw } = useCustomerGroupTableQuery({
+ pageSize: PAGE_SIZE,
+ prefix: PRODUCT_PREFIX,
+ })
+ const { customer_groups, count, isLoading, isError, error } =
+ useCustomerGroups(
+ {
+ ...searchParams,
+ },
+ {
+ keepPreviousData: true,
+ }
+ )
+
+ const updater: OnChangeFn = (fn) => {
+ const newState: RowSelectionState =
+ typeof fn === "function" ? fn(rowSelection) : fn
+
+ const added = Object.keys(newState).filter(
+ (k) => newState[k] !== rowSelection[k]
+ )
+
+ if (added.length) {
+ const addedGroups =
+ customer_groups?.filter((p) => added.includes(p.id!)) ?? []
+
+ if (addedGroups.length > 0) {
+ const newConditions = addedGroups.map((p) => ({
+ label: p.name,
+ value: p.id!,
+ }))
+
+ setIntermediate((prev) => {
+ const filteredPrev = prev.filter((p) => p.value in newState)
+ return Array.from(new Set([...filteredPrev, ...newConditions]))
+ })
+ }
+
+ setRowSelection(newState)
+ }
+
+ const removed = Object.keys(rowSelection).filter(
+ (k) => newState[k] !== rowSelection[k]
+ )
+
+ if (removed.length) {
+ setIntermediate((prev) => {
+ return prev.filter((p) => !removed.includes(p.value))
+ })
+
+ setRowSelection(newState)
+ }
+ }
+
+ const handleSave = () => {
+ onSave(intermediate)
+ }
+
+ const columns = useCustomerGroupConditionsTableColumns()
+ const filters = useCustomerGroupConditionsTableFilters()
+
+ const { table } = useDataTable({
+ data: (customer_groups ?? []) as CustomerGroup[],
+ columns: columns,
+ count,
+ enablePagination: true,
+ getRowId: (row) => row.id,
+ pageSize: PAGE_SIZE,
+ enableRowSelection: true,
+ rowSelection: {
+ state: rowSelection,
+ updater,
+ },
+ prefix: CUSTOMER_GROUP_PREFIX,
+ })
+
+ if (isError) {
+ throw error
+ }
+
+ return (
+
+
+
+
+ )
+}
+
+const ProductTypeConditionsTable = ({
+ onSave,
+ selected = [],
+}: ConditionsProps) => {
+ const [rowSelection, setRowSelection] = useState(
+ initRowState(selected)
+ )
+ const [intermediate, setIntermediate] = useState(selected)
+
+ const { searchParams, raw } = useProductTypeConditionsTableQuery({
+ pageSize: PAGE_SIZE,
+ prefix: PRODUCT_TYPE_PREFIX,
+ })
+ const { product_types, count, isLoading, isError, error } = useProductTypes(
+ {
+ ...searchParams,
+ },
+ {
+ keepPreviousData: true,
+ }
+ )
+
+ const updater: OnChangeFn = (fn) => {
+ const newState: RowSelectionState =
+ typeof fn === "function" ? fn(rowSelection) : fn
+
+ const added = Object.keys(newState).filter(
+ (k) => newState[k] !== rowSelection[k]
+ )
+
+ if (added.length) {
+ const addedTypes = (product_types?.filter((p) => added.includes(p.id!)) ??
+ []) as ProductType[]
+
+ if (addedTypes.length > 0) {
+ const newConditions = addedTypes.map((p) => ({
+ label: p.value,
+ value: p.id!,
+ }))
+
+ setIntermediate((prev) => {
+ const filteredPrev = prev.filter((p) => p.value in newState)
+ return Array.from(new Set([...filteredPrev, ...newConditions]))
+ })
+ }
+
+ setRowSelection(newState)
+ }
+ const removed = Object.keys(rowSelection).filter(
+ (k) => newState[k] !== rowSelection[k]
+ )
+
+ if (removed.length) {
+ setIntermediate((prev) => {
+ return prev.filter((p) => !removed.includes(p.value))
+ })
+
+ setRowSelection(newState)
+ }
+ }
+
+ const handleSave = () => {
+ onSave(intermediate)
+ }
+
+ const columns = useProductTypeConditionsTableColumns()
+ const filters = useProductTypeConditionsTableFilters()
+
+ const { table } = useDataTable({
+ data: product_types ?? [],
+ columns: columns,
+ count,
+ enablePagination: true,
+ getRowId: (row) => row.id,
+ pageSize: PAGE_SIZE,
+ enableRowSelection: true,
+ rowSelection: {
+ state: rowSelection,
+ updater,
+ },
+ prefix: PRODUCT_TYPE_PREFIX,
+ })
+
+ if (isError) {
+ throw error
+ }
+
+ return (
+
+
+
+
+ )
+}
+
+const ProductCollectionConditionsTable = ({
+ onSave,
+ selected = [],
+}: ConditionsProps) => {
+ const [rowSelection, setRowSelection] = useState(
+ initRowState(selected)
+ )
+ const [intermediate, setIntermediate] = useState(selected)
+
+ const { searchParams, raw } = useProductCollectionConditionsTableQuery({
+ pageSize: PAGE_SIZE,
+ prefix: PRODUCT_COLLECTION_PREFIX,
+ })
+ const { collections, count, isPending, isError, error } = useCollections(
+ {
+ ...searchParams,
+ },
+ {
+ keepPreviousData: true,
+ }
+ )
+
+ const updater: OnChangeFn = (fn) => {
+ const newState: RowSelectionState =
+ typeof fn === "function" ? fn(rowSelection) : fn
+
+ const added = Object.keys(newState).filter(
+ (k) => newState[k] !== rowSelection[k]
+ )
+
+ if (added.length) {
+ const addedCollections = (collections?.filter((p) =>
+ added.includes(p.id!)
+ ) ?? []) as ProductCollectionDTO[]
+
+ if (addedCollections.length > 0) {
+ const newConditions = addedCollections.map((p) => ({
+ label: p.title,
+ value: p.id!,
+ }))
+
+ setIntermediate((prev) => {
+ const filteredPrev = prev.filter((p) => p.value in newState)
+ return Array.from(new Set([...filteredPrev, ...newConditions]))
+ })
+ }
+
+ setRowSelection(newState)
+ }
+
+ const removed = Object.keys(rowSelection).filter(
+ (k) => newState[k] !== rowSelection[k]
+ )
+
+ if (removed.length) {
+ setIntermediate((prev) => {
+ return prev.filter((p) => !removed.includes(p.value))
+ })
+
+ setRowSelection(newState)
+ }
+ }
+
+ const handleSave = () => {
+ onSave(intermediate)
+ }
+
+ const columns = useProductCollectionConditionsTableColumns()
+ const filters = useProductCollectionConditionsTableFilters()
+
+ const { table } = useDataTable({
+ data: collections ?? [],
+ columns: columns,
+ count,
+ enablePagination: true,
+ getRowId: (row) => row.id,
+ pageSize: PAGE_SIZE,
+ enableRowSelection: true,
+ rowSelection: {
+ state: rowSelection,
+ updater,
+ },
+ prefix: PRODUCT_COLLECTION_PREFIX,
+ })
+
+ if (isError) {
+ throw error
+ }
+
+ return (
+
+
+
+
+ )
+}
+
+const ProductTagConditionsTable = ({
+ onSave,
+ selected = [],
+}: ConditionsProps) => {
+ const [rowSelection, setRowSelection] = useState(
+ initRowState(selected)
+ )
+ const [intermediate, setIntermediate] = useState(selected)
+
+ const { searchParams, raw } = useProductTagConditionsTableQuery({
+ pageSize: PAGE_SIZE,
+ prefix: PRODUCT_TAG_PREFIX,
+ })
+
+ // TODO: replace this with useProductTags when its available
+ const { product_tags, count, isLoading, isError, error } =
+ useAdminProductTags(
+ {
+ ...searchParams,
+ },
+ {
+ keepPreviousData: true,
+ }
+ )
+
+ const updater: OnChangeFn = (fn) => {
+ const newState: RowSelectionState =
+ typeof fn === "function" ? fn(rowSelection) : fn
+
+ const added = Object.keys(newState).filter(
+ (k) => newState[k] !== rowSelection[k]
+ )
+
+ if (added.length) {
+ const addedTags = (product_tags?.filter((p) => added.includes(p.id!)) ??
+ []) as ProductTag[]
+
+ if (addedTags.length > 0) {
+ const newConditions = addedTags.map((p) => ({
+ label: p.value,
+ value: p.id!,
+ }))
+
+ setIntermediate((prev) => {
+ const filteredPrev = prev.filter((p) => p.value in newState)
+ return Array.from(new Set([...filteredPrev, ...newConditions]))
+ })
+ }
+
+ setRowSelection(newState)
+ }
+
+ const removed = Object.keys(rowSelection).filter(
+ (k) => newState[k] !== rowSelection[k]
+ )
+
+ if (removed.length) {
+ setIntermediate((prev) => {
+ return prev.filter((p) => !removed.includes(p.value))
+ })
+
+ setRowSelection(newState)
+ }
+ }
+
+ const handleSave = () => {
+ onSave(intermediate)
+ }
+
+ const columns = useProductTagConditionsTableColumns()
+ const filters = useProductTagConditionsTableFilters()
+
+ const { table } = useDataTable({
+ data: product_tags ?? [],
+ columns: columns,
+ count,
+ enablePagination: true,
+ getRowId: (row) => row.id,
+ pageSize: PAGE_SIZE,
+ enableRowSelection: true,
+ rowSelection: {
+ state: rowSelection,
+ updater,
+ },
+ prefix: PRODUCT_TAG_PREFIX,
+ })
+
+ if (isError) {
+ throw error
+ }
+
+ return (
+
+
+
+
+ )
+}
+
+type ConditionsTableProps = {
+ product: ConditionsProps
+ productType: ConditionsProps
+ productTag: ConditionsProps
+ productCollection: ConditionsProps
+ customerGroup: ConditionsProps
+ selected: ConditionEntities | null
+}
+
+export const ConditionsDrawer = ({
+ product,
+ productType,
+ customerGroup,
+ productCollection,
+ productTag,
+ selected,
+}: ConditionsTableProps) => {
+ switch (selected) {
+ case ConditionEntities.PRODUCT:
+ return
+ case ConditionEntities.PRODUCT_TYPE:
+ return
+ case ConditionEntities.PRODUCT_COLLECTION:
+ return
+ case ConditionEntities.PRODUCT_TAG:
+ return
+ case ConditionEntities.CUSTOMER_GROUP:
+ return
+ default:
+ return null
+ }
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/common/components/conditions-drawer/index.ts b/packages/admin-next/dashboard/src/v2-routes/taxes/common/components/conditions-drawer/index.ts
new file mode 100644
index 0000000000..b65a27e54d
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/common/components/conditions-drawer/index.ts
@@ -0,0 +1 @@
+export * from "./conditions-drawer"
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/common/components/tax-region-create-form/index.ts b/packages/admin-next/dashboard/src/v2-routes/taxes/common/components/tax-region-create-form/index.ts
new file mode 100644
index 0000000000..e8bb24daae
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/common/components/tax-region-create-form/index.ts
@@ -0,0 +1 @@
+export * from "./tax-region-create-form"
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/common/components/tax-region-create-form/tax-region-create-form.tsx b/packages/admin-next/dashboard/src/v2-routes/taxes/common/components/tax-region-create-form/tax-region-create-form.tsx
new file mode 100644
index 0000000000..6de3816e89
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/common/components/tax-region-create-form/tax-region-create-form.tsx
@@ -0,0 +1,250 @@
+import { zodResolver } from "@hookform/resolvers/zod"
+import { Button, Heading, Input, Select, Switch, Text } from "@medusajs/ui"
+import { useForm } from "react-hook-form"
+import { useTranslation } from "react-i18next"
+import * as zod from "zod"
+
+import { TaxRegionResponse } from "@medusajs/types"
+import { Form } from "../../../../../components/common/form"
+import { PercentageInput } from "../../../../../components/common/percentage-input"
+import {
+ RouteFocusModal,
+ useRouteModal,
+} from "../../../../../components/route-modal"
+import { useCreateTaxRegion } from "../../../../../hooks/api/tax-regions"
+import { countries } from "../../../../../lib/countries"
+
+export const TaxRegionCreateForm = ({
+ taxRegion,
+ formSchema,
+}: {
+ taxRegion?: TaxRegionResponse
+ formSchema: zod.ZodObject<{
+ province_code: any
+ country_code: any
+ parent_id: any
+ name: any
+ code: any
+ rate: any
+ is_combinable: any
+ }>
+}) => {
+ const { t } = useTranslation()
+ const { handleSuccess } = useRouteModal()
+
+ const form = useForm>({
+ defaultValues: {
+ country_code: taxRegion?.country_code || undefined,
+ parent_id: taxRegion?.id || undefined,
+ },
+ resolver: zodResolver(formSchema),
+ })
+
+ const { mutateAsync, isPending } = useCreateTaxRegion()
+
+ const handleSubmit = form.handleSubmit(async (data) => {
+ await mutateAsync(
+ {
+ parent_id: taxRegion?.id,
+ province_code: data.province_code,
+ country_code: data.country_code,
+ default_tax_rate: {
+ name: data.name,
+ code: data.code,
+ rate: data.rate,
+ },
+ },
+ {
+ onSuccess: () => {
+ taxRegion?.id
+ ? handleSuccess(`/settings/taxes/${taxRegion.id}`)
+ : handleSuccess(`/settings/taxes`)
+ },
+ }
+ )
+ })
+
+ return (
+
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/common/constants.ts b/packages/admin-next/dashboard/src/v2-routes/taxes/common/constants.ts
new file mode 100644
index 0000000000..2541fdb756
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/common/constants.ts
@@ -0,0 +1,11 @@
+export enum ConditionEntities {
+ PRODUCT = "products",
+ PRODUCT_TYPE = "product_types",
+ PRODUCT_COLLECTION = "product_collections",
+ PRODUCT_TAG = "product_tags",
+ CUSTOMER_GROUP = "customer_groups",
+}
+
+export enum Operators {
+ IN = "in",
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/common/hooks/columns/use-customer-group-conditions-table-columns.tsx b/packages/admin-next/dashboard/src/v2-routes/taxes/common/hooks/columns/use-customer-group-conditions-table-columns.tsx
new file mode 100644
index 0000000000..5ed653dad6
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/common/hooks/columns/use-customer-group-conditions-table-columns.tsx
@@ -0,0 +1,46 @@
+import { CustomerGroup } from "@medusajs/medusa"
+import { Checkbox } from "@medusajs/ui"
+import { createColumnHelper } from "@tanstack/react-table"
+import { useMemo } from "react"
+import { useCustomerGroupTableColumns } from "../../../../../hooks/table/columns/use-customer-group-table-columns"
+
+const columnHelper = createColumnHelper()
+
+export const useCustomerGroupConditionsTableColumns = () => {
+ const base = useCustomerGroupTableColumns()
+
+ return useMemo(
+ () => [
+ columnHelper.display({
+ id: "select",
+ header: ({ table }) => {
+ return (
+
+ table.toggleAllPageRowsSelected(!!value)
+ }
+ />
+ )
+ },
+ cell: ({ row }) => {
+ return (
+ row.toggleSelected(!!value)}
+ onClick={(e) => {
+ e.stopPropagation()
+ }}
+ />
+ )
+ },
+ }),
+ ...base,
+ ],
+ [base]
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/common/hooks/columns/use-product-collection-conditions-table-columns.tsx b/packages/admin-next/dashboard/src/v2-routes/taxes/common/hooks/columns/use-product-collection-conditions-table-columns.tsx
new file mode 100644
index 0000000000..b61ee5e7f6
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/common/hooks/columns/use-product-collection-conditions-table-columns.tsx
@@ -0,0 +1,60 @@
+import { ProductCollectionDTO } from "@medusajs/types"
+import { Checkbox } from "@medusajs/ui"
+import { createColumnHelper } from "@tanstack/react-table"
+import { useMemo } from "react"
+import { useTranslation } from "react-i18next"
+
+const columnHelper = createColumnHelper()
+
+export const useProductCollectionConditionsTableColumns = () => {
+ const { t } = useTranslation()
+
+ return useMemo(
+ () => [
+ columnHelper.display({
+ id: "select",
+ header: ({ table }) => {
+ return (
+
+ table.toggleAllPageRowsSelected(!!value)
+ }
+ />
+ )
+ },
+ cell: ({ row }) => {
+ return (
+ row.toggleSelected(!!value)}
+ onClick={(e) => {
+ e.stopPropagation()
+ }}
+ />
+ )
+ },
+ }),
+ columnHelper.accessor("title", {
+ header: t("fields.title"),
+ }),
+ columnHelper.accessor("handle", {
+ header: t("fields.handle"),
+ cell: ({ getValue }) => `/${getValue()}`,
+ }),
+ columnHelper.accessor("products", {
+ header: t("fields.products"),
+ cell: ({ getValue }) => {
+ const count = getValue()?.length
+
+ return {count || "-"}
+ },
+ }),
+ ],
+ [t]
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/common/hooks/columns/use-product-conditions-table-columns.tsx b/packages/admin-next/dashboard/src/v2-routes/taxes/common/hooks/columns/use-product-conditions-table-columns.tsx
new file mode 100644
index 0000000000..9fc6850729
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/common/hooks/columns/use-product-conditions-table-columns.tsx
@@ -0,0 +1,46 @@
+import { Product } from "@medusajs/medusa"
+import { Checkbox } from "@medusajs/ui"
+import { createColumnHelper } from "@tanstack/react-table"
+import { useMemo } from "react"
+import { useProductTableColumns } from "../../../../../hooks/table/columns/use-product-table-columns"
+
+const columnHelper = createColumnHelper()
+
+export const useProductConditionsTableColumns = () => {
+ const base = useProductTableColumns()
+
+ return useMemo(
+ () => [
+ columnHelper.display({
+ id: "select",
+ header: ({ table }) => {
+ return (
+
+ table.toggleAllPageRowsSelected(!!value)
+ }
+ />
+ )
+ },
+ cell: ({ row }) => {
+ return (
+ row.toggleSelected(!!value)}
+ onClick={(e) => {
+ e.stopPropagation()
+ }}
+ />
+ )
+ },
+ }),
+ ...base,
+ ],
+ [base]
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/common/hooks/columns/use-product-tag-conditions-table-columns.tsx b/packages/admin-next/dashboard/src/v2-routes/taxes/common/hooks/columns/use-product-tag-conditions-table-columns.tsx
new file mode 100644
index 0000000000..540c8d1354
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/common/hooks/columns/use-product-tag-conditions-table-columns.tsx
@@ -0,0 +1,48 @@
+import { useTranslation } from "react-i18next"
+import { useMemo } from "react"
+import { createColumnHelper } from "@tanstack/react-table"
+import { ProductTag } from "@medusajs/medusa"
+import { Checkbox } from "@medusajs/ui"
+
+const columnHelper = createColumnHelper()
+
+export const useProductTagConditionsTableColumns = () => {
+ const { t } = useTranslation()
+
+ return useMemo(
+ () => [
+ columnHelper.display({
+ id: "select",
+ header: ({ table }) => {
+ return (
+
+ table.toggleAllPageRowsSelected(!!value)
+ }
+ />
+ )
+ },
+ cell: ({ row }) => {
+ return (
+ row.toggleSelected(!!value)}
+ onClick={(e) => {
+ e.stopPropagation()
+ }}
+ />
+ )
+ },
+ }),
+ columnHelper.accessor("value", {
+ header: t("fields.value"),
+ }),
+ ],
+ [t]
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/common/hooks/columns/use-product-type-conditions-table-columns.tsx b/packages/admin-next/dashboard/src/v2-routes/taxes/common/hooks/columns/use-product-type-conditions-table-columns.tsx
new file mode 100644
index 0000000000..9066f5b02f
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/common/hooks/columns/use-product-type-conditions-table-columns.tsx
@@ -0,0 +1,49 @@
+import { ProductType } from "@medusajs/medusa"
+import { Checkbox } from "@medusajs/ui"
+import { createColumnHelper } from "@tanstack/react-table"
+import { useMemo } from "react"
+import { useTranslation } from "react-i18next"
+
+const columnHelper = createColumnHelper()
+
+export const useProductTypeConditionsTableColumns = () => {
+ const { t } = useTranslation()
+
+ return useMemo(
+ () => [
+ columnHelper.display({
+ id: "select",
+ header: ({ table }) => {
+ return (
+
+ table.toggleAllPageRowsSelected(!!value)
+ }
+ />
+ )
+ },
+ cell: ({ row }) => {
+ return (
+ row.toggleSelected(!!value)}
+ onClick={(e) => {
+ e.stopPropagation()
+ }}
+ />
+ )
+ },
+ }),
+ columnHelper.accessor("value", {
+ header: t("fields.value"),
+ cell: ({ getValue }) => getValue(),
+ }),
+ ],
+ [t]
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/common/hooks/filters/use-customer-group-conditions-table-filters.tsx b/packages/admin-next/dashboard/src/v2-routes/taxes/common/hooks/filters/use-customer-group-conditions-table-filters.tsx
new file mode 100644
index 0000000000..50859c8e24
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/common/hooks/filters/use-customer-group-conditions-table-filters.tsx
@@ -0,0 +1,17 @@
+import { useTranslation } from "react-i18next"
+import { Filter } from "../../../../../components/table/data-table"
+
+export const useCustomerGroupConditionsTableFilters = () => {
+ const { t } = useTranslation()
+
+ const filters: Filter[] = [
+ { label: t("fields.createdAt"), key: "created_at" },
+ { label: t("fields.updatedAt"), key: "updated_at" },
+ ].map((f) => ({
+ key: f.key,
+ label: f.label,
+ type: "date",
+ }))
+
+ return filters
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/common/hooks/filters/use-product-collection-conditions-table-filters.tsx b/packages/admin-next/dashboard/src/v2-routes/taxes/common/hooks/filters/use-product-collection-conditions-table-filters.tsx
new file mode 100644
index 0000000000..45de4cc975
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/common/hooks/filters/use-product-collection-conditions-table-filters.tsx
@@ -0,0 +1,17 @@
+import { useTranslation } from "react-i18next"
+import { Filter } from "../../../../../components/table/data-table"
+
+export const useProductCollectionConditionsTableFilters = () => {
+ const { t } = useTranslation()
+
+ const filters: Filter[] = [
+ { label: t("fields.createdAt"), key: "created_at" },
+ { label: t("fields.updatedAt"), key: "updated_at" },
+ ].map((f) => ({
+ key: f.key,
+ label: f.label,
+ type: "date",
+ }))
+
+ return filters
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/common/hooks/filters/use-product-tag-conditions-table-filters.tsx b/packages/admin-next/dashboard/src/v2-routes/taxes/common/hooks/filters/use-product-tag-conditions-table-filters.tsx
new file mode 100644
index 0000000000..b79dad59a0
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/common/hooks/filters/use-product-tag-conditions-table-filters.tsx
@@ -0,0 +1,17 @@
+import { useTranslation } from "react-i18next"
+import { Filter } from "../../../../../components/table/data-table"
+
+export const useProductTagConditionsTableFilters = () => {
+ const { t } = useTranslation()
+
+ const filters: Filter[] = [
+ { label: t("fields.createdAt"), key: "created_at" },
+ { label: t("fields.updatedAt"), key: "updated_at" },
+ ].map((f) => ({
+ key: f.key,
+ label: f.label,
+ type: "date",
+ }))
+
+ return filters
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/common/hooks/filters/use-product-type-conditions-table-filters.tsx b/packages/admin-next/dashboard/src/v2-routes/taxes/common/hooks/filters/use-product-type-conditions-table-filters.tsx
new file mode 100644
index 0000000000..7c9a74018c
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/common/hooks/filters/use-product-type-conditions-table-filters.tsx
@@ -0,0 +1,17 @@
+import { useTranslation } from "react-i18next"
+import { Filter } from "../../../../../components/table/data-table"
+
+export const useProductTypeConditionsTableFilters = () => {
+ const { t } = useTranslation()
+
+ const filters: Filter[] = [
+ { label: t("fields.createdAt"), key: "created_at" },
+ { label: t("fields.updatedAt"), key: "updated_at" },
+ ].map((f) => ({
+ key: f.key,
+ label: f.label,
+ type: "date",
+ }))
+
+ return filters
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/common/hooks/query/use-product-collection-conditions-table-query.tsx b/packages/admin-next/dashboard/src/v2-routes/taxes/common/hooks/query/use-product-collection-conditions-table-query.tsx
new file mode 100644
index 0000000000..159ab0a8bd
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/common/hooks/query/use-product-collection-conditions-table-query.tsx
@@ -0,0 +1,42 @@
+import { AdminGetCollectionsParams } from "@medusajs/medusa"
+
+import { useQueryParams } from "../../../../../hooks/use-query-params"
+
+export const useProductCollectionConditionsTableQuery = ({
+ pageSize = 50,
+ prefix,
+}: {
+ pageSize?: number
+ prefix: string
+}) => {
+ const raw = useQueryParams(
+ [
+ "offset",
+ "q",
+ "order",
+ "title",
+ "handle",
+ "discount_condition_id",
+ "created_at",
+ "updated_at",
+ ],
+ prefix
+ )
+
+ const searchParams: AdminGetCollectionsParams = {
+ limit: pageSize,
+ offset: raw.offset ? Number(raw.offset) : 0,
+ q: raw.q,
+ title: raw.title,
+ handle: raw.handle,
+ discount_condition_id: raw.discount_condition_id,
+ created_at: raw.created_at ? JSON.parse(raw.created_at) : undefined,
+ updated_at: raw.updated_at ? JSON.parse(raw.updated_at) : undefined,
+ order: raw.order,
+ }
+
+ return {
+ searchParams,
+ raw,
+ }
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/common/hooks/query/use-product-tag-conditions-table-query.tsx b/packages/admin-next/dashboard/src/v2-routes/taxes/common/hooks/query/use-product-tag-conditions-table-query.tsx
new file mode 100644
index 0000000000..717cab17f6
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/common/hooks/query/use-product-tag-conditions-table-query.tsx
@@ -0,0 +1,39 @@
+import { AdminGetProductTagsParams } from "@medusajs/medusa"
+
+import { useQueryParams } from "../../../../../hooks/use-query-params"
+
+export const useProductTagConditionsTableQuery = ({
+ pageSize = 50,
+ prefix,
+}: {
+ pageSize?: number
+ prefix: string
+}) => {
+ const raw = useQueryParams(
+ [
+ "offset",
+ "q",
+ "order",
+ "value",
+ "discount_condition_id",
+ "created_at",
+ "updated_at",
+ ],
+ prefix
+ )
+
+ const searchParams: AdminGetProductTagsParams = {
+ limit: pageSize,
+ offset: raw.offset ? Number(raw.offset) : 0,
+ q: raw.q,
+ discount_condition_id: raw.discount_condition_id,
+ created_at: raw.created_at ? JSON.parse(raw.created_at) : undefined,
+ updated_at: raw.updated_at ? JSON.parse(raw.updated_at) : undefined,
+ order: raw.order,
+ }
+
+ return {
+ searchParams,
+ raw,
+ }
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/common/hooks/query/use-product-type-conditions-table-query.tsx b/packages/admin-next/dashboard/src/v2-routes/taxes/common/hooks/query/use-product-type-conditions-table-query.tsx
new file mode 100644
index 0000000000..737d00d143
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/common/hooks/query/use-product-type-conditions-table-query.tsx
@@ -0,0 +1,29 @@
+import { AdminGetProductTypesParams } from "@medusajs/medusa"
+import { useQueryParams } from "../../../../../hooks/use-query-params"
+
+export const useProductTypeConditionsTableQuery = ({
+ pageSize = 50,
+ prefix,
+}: {
+ pageSize?: number
+ prefix: string
+}) => {
+ const raw = useQueryParams(
+ ["offset", "q", "order", "created_at", "updated_at"],
+ prefix
+ )
+
+ const searchParams: AdminGetProductTypesParams = {
+ limit: pageSize,
+ offset: raw.offset ? Number(raw.offset) : 0,
+ q: raw.q,
+ created_at: raw.created_at ? JSON.parse(raw.created_at) : undefined,
+ updated_at: raw.updated_at ? JSON.parse(raw.updated_at) : undefined,
+ order: raw.order,
+ }
+
+ return {
+ searchParams,
+ raw,
+ }
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/common/types.ts b/packages/admin-next/dashboard/src/v2-routes/taxes/common/types.ts
new file mode 100644
index 0000000000..59b303c420
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/common/types.ts
@@ -0,0 +1,12 @@
+import { ConditionEntities } from "./constants"
+
+export type ConditionsOption = {
+ value: string
+ label: string
+}
+
+export type ConditionsState = {
+ [K in ConditionEntities]: boolean
+}
+
+export type ConditionEntitiesValues = `${ConditionEntities}`
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/tax-province-create/index.ts b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-province-create/index.ts
new file mode 100644
index 0000000000..ace2775212
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-province-create/index.ts
@@ -0,0 +1,3 @@
+export * from "./tax-province-create"
+
+export { TaxProvinceCreate as Component } from "./tax-province-create"
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/tax-province-create/tax-province-create.tsx b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-province-create/tax-province-create.tsx
new file mode 100644
index 0000000000..031809dda8
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-province-create/tax-province-create.tsx
@@ -0,0 +1,36 @@
+import { useParams } from "react-router-dom"
+import * as zod from "zod"
+import { RouteFocusModal } from "../../../components/route-modal"
+import { useTaxRegion } from "../../../hooks/api/tax-regions"
+import { TaxRegionCreateForm } from "../common/components/tax-region-create-form"
+
+const CreateTaxProvinceForm = zod.object({
+ province_code: zod.string(),
+ country_code: zod.string(),
+ parent_id: zod.string(),
+ name: zod.string(),
+ code: zod.string().optional(),
+ rate: zod.number(),
+ is_combinable: zod.boolean().default(false),
+})
+
+export const TaxProvinceCreate = () => {
+ const { id } = useParams()
+
+ const { tax_region: taxRegion } = useTaxRegion(
+ id!,
+ {},
+ {
+ enabled: !!id,
+ }
+ )
+
+ return (
+
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-create/components/condition/condition.tsx b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-create/components/condition/condition.tsx
new file mode 100644
index 0000000000..d3a836601e
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-create/components/condition/condition.tsx
@@ -0,0 +1,53 @@
+import { Button, Text } from "@medusajs/ui"
+import { useTranslation } from "react-i18next"
+import { ListSummary } from "../../../../../components/common/list-summary"
+import { ConditionEntitiesValues } from "../../../common/types"
+
+const N = 2
+
+type ConditionProps = {
+ labels: string[]
+ type: ConditionEntitiesValues
+ onClick: () => void
+}
+
+export function Condition({ labels, type, onClick }: ConditionProps) {
+ const { t } = useTranslation()
+ const isInButtonDisabled = !!labels.length
+
+ return (
+
+
+
+ {t("taxRates.fields.appliesTo")} {t(`taxRates.fields.${type}`)}
+
+
+
+
+
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-create/components/condition/index.ts b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-create/components/condition/index.ts
new file mode 100644
index 0000000000..1e9884b3b3
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-create/components/condition/index.ts
@@ -0,0 +1 @@
+export * from "./condition"
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-create/components/index.ts b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-create/components/index.ts
new file mode 100644
index 0000000000..3cf124f9d6
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-create/components/index.ts
@@ -0,0 +1,2 @@
+export * from "./condition"
+export * from "./tax-rate-create-form"
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-create/components/tax-rate-create-form/index.ts b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-create/components/tax-rate-create-form/index.ts
new file mode 100644
index 0000000000..69b098c24a
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-create/components/tax-rate-create-form/index.ts
@@ -0,0 +1 @@
+export * from "./tax-rate-create-form"
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-create/components/tax-rate-create-form/tax-rate-create-form.tsx b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-create/components/tax-rate-create-form/tax-rate-create-form.tsx
new file mode 100644
index 0000000000..5704bdb1b9
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-create/components/tax-rate-create-form/tax-rate-create-form.tsx
@@ -0,0 +1,480 @@
+import { zodResolver } from "@hookform/resolvers/zod"
+import {
+ Button,
+ clx,
+ DropdownMenu,
+ Heading,
+ Input,
+ Select,
+ Switch,
+ Text,
+} from "@medusajs/ui"
+import { useForm, useWatch } from "react-hook-form"
+import { useTranslation } from "react-i18next"
+import * as zod from "zod"
+
+import { TaxRegionResponse } from "@medusajs/types"
+import { useState } from "react"
+import { useSearchParams } from "react-router-dom"
+import { Form } from "../../../../../components/common/form"
+import { PercentageInput } from "../../../../../components/common/percentage-input"
+import { SplitView } from "../../../../../components/layout/split-view"
+import {
+ RouteFocusModal,
+ useRouteModal,
+} from "../../../../../components/route-modal"
+import { useCreateTaxRate } from "../../../../../hooks/api/tax-rates"
+import { useTaxRegions } from "../../../../../hooks/api/tax-regions"
+import { ConditionsDrawer } from "../../../common/components/conditions-drawer"
+import { ConditionEntities } from "../../../common/constants"
+import {
+ ConditionEntitiesValues,
+ ConditionsOption,
+} from "../../../common/types"
+import { Condition } from "../condition"
+
+const SelectedConditionTypesSchema = zod.object({
+ [ConditionEntities.PRODUCT]: zod.boolean(),
+ [ConditionEntities.PRODUCT_COLLECTION]: zod.boolean(),
+ [ConditionEntities.PRODUCT_TAG]: zod.boolean(),
+ [ConditionEntities.PRODUCT_TYPE]: zod.boolean(),
+ [ConditionEntities.CUSTOMER_GROUP]: zod.boolean(),
+})
+
+const ConditionSchema = zod.array(
+ zod.object({
+ label: zod.string(),
+ value: zod.string(),
+ })
+)
+
+const CreateTaxRateSchema = zod.object({
+ tax_region_id: zod.string(),
+ name: zod.string(),
+ code: zod.string(),
+ rate: zod.number(),
+ is_combinable: zod.boolean().default(false),
+ selected_condition_types: SelectedConditionTypesSchema,
+ products: ConditionSchema,
+ product_types: ConditionSchema,
+ product_collections: ConditionSchema,
+ product_tags: ConditionSchema,
+ customer_groups: ConditionSchema,
+})
+
+export const TaxRateCreateForm = ({
+ taxRegion,
+}: {
+ taxRegion: TaxRegionResponse
+}) => {
+ const { t } = useTranslation()
+ const { handleSuccess } = useRouteModal()
+
+ const form = useForm>({
+ defaultValues: {
+ selected_condition_types: {
+ [ConditionEntities.PRODUCT]: true,
+ [ConditionEntities.PRODUCT_TYPE]: false,
+ [ConditionEntities.PRODUCT_COLLECTION]: false,
+ [ConditionEntities.PRODUCT_TAG]: false,
+ [ConditionEntities.CUSTOMER_GROUP]: false,
+ },
+ products: [],
+ product_types: [],
+ product_collections: [],
+ product_tags: [],
+ customer_groups: [],
+ },
+ resolver: zodResolver(CreateTaxRateSchema),
+ })
+
+ const { tax_regions: taxRegions } = useTaxRegions({
+ parent_id: taxRegion.id,
+ province_code: { $ne: "null" },
+ })
+
+ const { mutateAsync, isPending } = useCreateTaxRate()
+
+ const handleSubmit = form.handleSubmit(async (data) => {
+ await mutateAsync(
+ {
+ name: data.name,
+ code: data.code,
+ rate: data.rate,
+ is_combinable: data.is_combinable,
+ tax_region_id: data.tax_region_id,
+ rules:
+ data.products?.map((product) => ({
+ reference: "product",
+ reference_id: product.value,
+ })) || [],
+ },
+ {
+ onSuccess: () => handleSuccess(`/settings/taxes/${taxRegion.id}`),
+ }
+ )
+ })
+
+ const [open, setOpen] = useState(false)
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false)
+ const [conditionType, setConditionType] = useState(
+ null
+ )
+ const selectedConditionTypes = useWatch({
+ name: "selected_condition_types",
+ control: form.control,
+ })
+
+ const selectedProducts = useWatch({
+ control: form.control,
+ name: "products",
+ })
+
+ const selectedProductCollections = useWatch({
+ control: form.control,
+ name: "product_collections",
+ })
+
+ const selectedProductTypes = useWatch({
+ control: form.control,
+ name: "product_types",
+ })
+
+ const selectedProductTags = useWatch({
+ control: form.control,
+ name: "product_tags",
+ })
+
+ const selectedCustomerGroups = useWatch({
+ control: form.control,
+ name: "customer_groups",
+ })
+
+ const handleSaveConditions = (type: ConditionEntitiesValues) => {
+ return (options: ConditionsOption[]) => {
+ form.setValue(type, options, {
+ shouldDirty: true,
+ shouldTouch: true,
+ })
+
+ setOpen(false)
+ }
+ }
+
+ const selectedTypes = Object.keys(selectedConditionTypes || {})
+ .filter(
+ (k) => selectedConditionTypes[k as keyof typeof selectedConditionTypes]
+ )
+ .sort() as ConditionEntities[]
+
+ const toggleSelectedConditionTypes = (type: ConditionEntities) => {
+ const state = { ...form.getValues().selected_condition_types }
+
+ if (state[type]) {
+ delete state[type]
+ } else {
+ state[type] = true
+ }
+
+ form.setValue("selected_condition_types", state, {
+ shouldDirty: true,
+ shouldTouch: true,
+ })
+ }
+
+ const clearAllSelectedConditions = () => {
+ form.setValue(
+ "selected_condition_types",
+ {
+ [ConditionEntities.PRODUCT]: false,
+ [ConditionEntities.PRODUCT_TYPE]: false,
+ [ConditionEntities.PRODUCT_COLLECTION]: false,
+ [ConditionEntities.PRODUCT_TAG]: false,
+ [ConditionEntities.CUSTOMER_GROUP]: false,
+ },
+ {
+ shouldDirty: true,
+ shouldTouch: true,
+ }
+ )
+ }
+
+ const [, setSearchParams] = useSearchParams()
+ const handleOpenChange = (open: boolean) => {
+ if (!open) {
+ setConditionType(null)
+ setSearchParams(
+ {},
+ {
+ replace: true,
+ }
+ )
+ }
+
+ setOpen(open)
+ }
+
+ return (
+
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-create/index.ts b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-create/index.ts
new file mode 100644
index 0000000000..72ee1c8329
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-create/index.ts
@@ -0,0 +1,3 @@
+export * from "./tax-rate-create"
+
+export { TaxRateCreate as Component } from "./tax-rate-create"
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-create/tax-rate-create.tsx b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-create/tax-rate-create.tsx
new file mode 100644
index 0000000000..37d03e5206
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-create/tax-rate-create.tsx
@@ -0,0 +1,21 @@
+import { useParams } from "react-router-dom"
+import { RouteFocusModal } from "../../../components/route-modal"
+import { useTaxRegion } from "../../../hooks/api/tax-regions"
+import { TaxRateCreateForm } from "./components"
+
+export const TaxRateCreate = () => {
+ const params = useParams()
+ const { tax_region: taxRegion, isError, error } = useTaxRegion(params.id!)
+
+ if (isError) {
+ throw error
+ }
+
+ return (
+ taxRegion && (
+
+
+
+ )
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-edit/components/index.ts b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-edit/components/index.ts
new file mode 100644
index 0000000000..8f2ab9b263
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-edit/components/index.ts
@@ -0,0 +1 @@
+export * from "./tax-rate-edit-form"
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-edit/components/tax-rate-edit-form/index.ts b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-edit/components/tax-rate-edit-form/index.ts
new file mode 100644
index 0000000000..8f2ab9b263
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-edit/components/tax-rate-edit-form/index.ts
@@ -0,0 +1 @@
+export * from "./tax-rate-edit-form"
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-edit/components/tax-rate-edit-form/tax-rate-edit-form.tsx b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-edit/components/tax-rate-edit-form/tax-rate-edit-form.tsx
new file mode 100644
index 0000000000..b01e4e3d17
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-edit/components/tax-rate-edit-form/tax-rate-edit-form.tsx
@@ -0,0 +1,488 @@
+import { zodResolver } from "@hookform/resolvers/zod"
+import {
+ Button,
+ clx,
+ DropdownMenu,
+ Heading,
+ Input,
+ Switch,
+ Text,
+} from "@medusajs/ui"
+import { useForm, useWatch } from "react-hook-form"
+import { useTranslation } from "react-i18next"
+import * as zod from "zod"
+
+import { TaxRateResponse, TaxRegionResponse } from "@medusajs/types"
+import { useState } from "react"
+import { useSearchParams } from "react-router-dom"
+import { Form } from "../../../../../components/common/form"
+import { PercentageInput } from "../../../../../components/common/percentage-input"
+import { SplitView } from "../../../../../components/layout/split-view"
+import {
+ RouteFocusModal,
+ useRouteModal,
+} from "../../../../../components/route-modal"
+import { useUpdateTaxRate } from "../../../../../hooks/api/tax-rates"
+import { ConditionsDrawer } from "../../../common/components/conditions-drawer"
+import { ConditionEntities, Operators } from "../../../common/constants"
+import { ConditionsOption } from "../../../common/types"
+import { Condition } from "../../../tax-rate-create/components"
+
+const SelectedConditionTypesSchema = zod.object({
+ [ConditionEntities.PRODUCT]: zod.boolean(),
+ [ConditionEntities.PRODUCT_TYPE]: zod.boolean(),
+ [ConditionEntities.PRODUCT_COLLECTION]: zod.boolean(),
+ [ConditionEntities.PRODUCT_TAG]: zod.boolean(),
+ [ConditionEntities.CUSTOMER_GROUP]: zod.boolean(),
+})
+
+const ResourceSchema = zod.array(
+ zod.object({
+ label: zod.string(),
+ value: zod.string(),
+ })
+)
+
+const UpdateTaxRateSchema = zod.object({
+ name: zod.string().optional(),
+ code: zod.string().optional(),
+ rate: zod.number().optional(),
+ is_combinable: zod.boolean().optional(),
+ selected_condition_types: SelectedConditionTypesSchema,
+ products: ResourceSchema,
+ product_types: ResourceSchema,
+ product_collections: ResourceSchema,
+ product_tags: ResourceSchema,
+ customer_groups: ResourceSchema,
+})
+
+export const TaxRateEditForm = ({
+ taxRegion,
+ taxRate,
+}: {
+ taxRegion: TaxRegionResponse
+ taxRate: TaxRateResponse
+}) => {
+ const { t } = useTranslation()
+ const { handleSuccess } = useRouteModal()
+ const productRules = taxRate.rules?.filter((r) => r.reference == "product")
+ const productTypeRules = taxRate.rules?.filter(
+ (r) => r.reference == "product_type"
+ )
+ const productCollectionRules = taxRate.rules?.filter(
+ (r) => r.reference == "product_collection"
+ )
+ const productTagRules = taxRate.rules?.filter(
+ (r) => r.reference == "product_tag"
+ )
+ const customerGroupRules = taxRate.rules?.filter(
+ (r) => r.reference == "customer_group"
+ )
+
+ const form = useForm>({
+ defaultValues: {
+ name: taxRate.name,
+ code: taxRate.code || undefined,
+ rate: taxRate.rate || undefined,
+ is_combinable: taxRate.is_combinable,
+ selected_condition_types: {
+ [ConditionEntities.PRODUCT]: !!productRules?.length,
+ [ConditionEntities.PRODUCT_TYPE]: !!productTypeRules?.length,
+ [ConditionEntities.PRODUCT_COLLECTION]:
+ !!productCollectionRules?.length,
+ [ConditionEntities.PRODUCT_TAG]: !!productTagRules?.length,
+ [ConditionEntities.CUSTOMER_GROUP]: !!customerGroupRules?.length,
+ },
+ products: productRules.map((r) => ({
+ label: r.reference,
+ value: r.reference_id,
+ })),
+ product_types: productTypeRules.map((r) => ({
+ label: r.reference,
+ value: r.reference_id,
+ })),
+ product_collections: productCollectionRules.map((r) => ({
+ label: r.reference,
+ value: r.reference_id,
+ })),
+ product_tags: productTagRules.map((r) => ({
+ label: r.reference,
+ value: r.reference_id,
+ })),
+ customer_groups: customerGroupRules.map((r) => ({
+ label: r.reference,
+ value: r.reference_id,
+ })),
+ },
+ resolver: zodResolver(UpdateTaxRateSchema),
+ })
+
+ const { mutateAsync, isPending } = useUpdateTaxRate(taxRate.id)
+
+ const buildRules = (key: string, data: { value: string }[]) =>
+ data?.map((product) => ({
+ reference: key,
+ reference_id: product.value,
+ })) || []
+
+ const handleSubmit = form.handleSubmit(async (data) => {
+ const rules = [
+ ...buildRules("product", data.products),
+ ...buildRules("product_type", data.product_types),
+ ...buildRules("product_collection", data.product_collections),
+ ...buildRules("product_tag", data.product_tags),
+ ...buildRules("customer_group", data.customer_groups),
+ ]
+
+ await mutateAsync(
+ {
+ name: data.name,
+ code: data.code || undefined,
+ rate: data.rate,
+ is_combinable: data.is_combinable,
+ rules,
+ },
+ {
+ onSuccess: () => handleSuccess(`/settings/taxes/${taxRegion.id}`),
+ }
+ )
+ })
+
+ const [open, setOpen] = useState(false)
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false)
+ const [conditionType, setConditionType] = useState(
+ null
+ )
+ const selectedConditionTypes = useWatch({
+ name: "selected_condition_types",
+ control: form.control,
+ })
+
+ const selectedProducts = useWatch({
+ control: form.control,
+ name: "products",
+ })
+
+ const selectedProductCollections = useWatch({
+ control: form.control,
+ name: "product_collections",
+ })
+
+ const selectedProductTypes = useWatch({
+ control: form.control,
+ name: "product_types",
+ })
+
+ const selectedProductTags = useWatch({
+ control: form.control,
+ name: "product_tags",
+ })
+
+ const selectedCustomerGroups = useWatch({
+ control: form.control,
+ name: "customer_groups",
+ })
+
+ const handleSaveConditions = (type: ConditionEntities) => {
+ return (options: ConditionsOption[]) => {
+ form.setValue(type, options, {
+ shouldDirty: true,
+ shouldTouch: true,
+ })
+
+ setOpen(false)
+ }
+ }
+
+ const selectedTypes = Object.keys(selectedConditionTypes || {})
+ .filter(
+ (k) => selectedConditionTypes[k as keyof typeof selectedConditionTypes]
+ )
+ .sort() as ConditionEntities[]
+
+ const handleOpenDrawer = (type: ConditionEntities, operator: Operators) => {
+ setConditionType(type)
+ setOpen(true)
+ }
+
+ const toggleSelectedConditionTypes = (type: ConditionEntities) => {
+ const state = { ...form.getValues().selected_condition_types }
+ if (state[type]) {
+ delete state[type]
+ } else {
+ state[type] = true
+ }
+
+ form.setValue("selected_condition_types", state, {
+ shouldDirty: true,
+ shouldTouch: true,
+ })
+ }
+
+ const clearAllSelectedConditions = () => {
+ form.setValue(
+ "selected_condition_types",
+ {
+ [ConditionEntities.PRODUCT]: false,
+ [ConditionEntities.PRODUCT_TYPE]: false,
+ [ConditionEntities.PRODUCT_COLLECTION]: false,
+ [ConditionEntities.PRODUCT_TAG]: false,
+ [ConditionEntities.CUSTOMER_GROUP]: false,
+ },
+ {
+ shouldDirty: true,
+ shouldTouch: true,
+ }
+ )
+ }
+
+ const [, setSearchParams] = useSearchParams()
+ const handleOpenChange = (open: boolean) => {
+ if (!open) {
+ setConditionType(null)
+ setSearchParams(
+ {},
+ {
+ replace: true,
+ }
+ )
+ }
+
+ setOpen(open)
+ }
+
+ return (
+
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-edit/index.ts b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-edit/index.ts
new file mode 100644
index 0000000000..ef8328fd8b
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-edit/index.ts
@@ -0,0 +1,4 @@
+export * from "./tax-rate-edit"
+
+export { taxRateLoader as loader } from "./loader"
+export { TaxRateEdit as Component } from "./tax-rate-edit"
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-edit/loader.ts b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-edit/loader.ts
new file mode 100644
index 0000000000..3dfc7feb62
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-edit/loader.ts
@@ -0,0 +1,21 @@
+import { LoaderFunctionArgs } from "react-router-dom"
+
+import { AdminTaxRateResponse } from "@medusajs/types"
+import { taxRatesQueryKeys } from "../../../hooks/api/tax-rates"
+import { client } from "../../../lib/client"
+import { queryClient } from "../../../lib/medusa"
+
+const taxRateDetailQuery = (id: string) => ({
+ queryKey: taxRatesQueryKeys.detail(id),
+ queryFn: async () => client.taxes.retrieveTaxRate(id),
+})
+
+export const taxRateLoader = async ({ params }: LoaderFunctionArgs) => {
+ const id = params.taxRateId
+ const query = taxRateDetailQuery(id!)
+
+ return (
+ queryClient.getQueryData(query.queryKey) ??
+ (await queryClient.fetchQuery(query))
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-edit/tax-rate-edit.tsx b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-edit/tax-rate-edit.tsx
new file mode 100644
index 0000000000..a6196b38af
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-rate-edit/tax-rate-edit.tsx
@@ -0,0 +1,34 @@
+import { useParams } from "react-router-dom"
+import { RouteFocusModal } from "../../../components/route-modal"
+import { useTaxRate } from "../../../hooks/api/tax-rates"
+import { useTaxRegion } from "../../../hooks/api/tax-regions"
+import { TaxRateEditForm } from "./components"
+
+export const TaxRateEdit = () => {
+ const params = useParams()
+
+ const { tax_region: taxRegion } = useTaxRegion(params.id!)
+ const {
+ tax_rate: taxRate,
+ isLoading,
+ isError,
+ error,
+ } = useTaxRate(params.taxRateId!)
+
+ if (isLoading) {
+ return Loading...
+ }
+
+ if (isError) {
+ throw error
+ }
+
+ return (
+ taxRegion &&
+ taxRate && (
+
+
+
+ )
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/tax-region-create/index.ts b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-region-create/index.ts
new file mode 100644
index 0000000000..26d1bff91c
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-region-create/index.ts
@@ -0,0 +1,3 @@
+export * from "./tax-region-create"
+
+export { TaxRegionCreate as Component } from "./tax-region-create"
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/tax-region-create/tax-region-create.tsx b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-region-create/tax-region-create.tsx
new file mode 100644
index 0000000000..ae6a4115f6
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-region-create/tax-region-create.tsx
@@ -0,0 +1,21 @@
+import * as zod from "zod"
+import { RouteFocusModal } from "../../../components/route-modal"
+import { TaxRegionCreateForm } from "../common/components/tax-region-create-form"
+
+const CreateTaxRegionForm = zod.object({
+ province_code: zod.string().optional(),
+ country_code: zod.string(),
+ parent_id: zod.string().optional(),
+ name: zod.string(),
+ code: zod.string().optional(),
+ rate: zod.number(),
+ is_combinable: zod.boolean().default(false),
+})
+
+export const TaxRegionCreate = () => {
+ return (
+
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/tax-region-detail/components/index.ts b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-region-detail/components/index.ts
new file mode 100644
index 0000000000..7c45c4182c
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-region-detail/components/index.ts
@@ -0,0 +1,2 @@
+export * from "./tax-rate-list"
+export * from "./tax-region-general-detail"
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/tax-region-detail/components/tax-rate-list/index.ts b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-region-detail/components/tax-rate-list/index.ts
new file mode 100644
index 0000000000..9e413d7d01
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-region-detail/components/tax-rate-list/index.ts
@@ -0,0 +1 @@
+export * from "./tax-rate-list"
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/tax-region-detail/components/tax-rate-list/tax-rate-list.tsx b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-region-detail/components/tax-rate-list/tax-rate-list.tsx
new file mode 100644
index 0000000000..ea96bea4a4
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-region-detail/components/tax-rate-list/tax-rate-list.tsx
@@ -0,0 +1,214 @@
+import { PencilSquare, Trash } from "@medusajs/icons"
+import { TaxRateResponse, TaxRegionResponse } from "@medusajs/types"
+import { Button, Container, Heading, usePrompt } from "@medusajs/ui"
+import { keepPreviousData } from "@tanstack/react-query"
+import { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
+import { useMemo, useState } from "react"
+import { useTranslation } from "react-i18next"
+import { Link } from "react-router-dom"
+import { ActionMenu } from "../../../../../components/common/action-menu"
+import { DataTable } from "../../../../../components/table/data-table"
+import {
+ useDeleteTaxRate,
+ useTaxRates,
+} from "../../../../../hooks/api/tax-rates"
+import { useDeleteTaxRegion } from "../../../../../hooks/api/tax-regions"
+import { useTaxRateTableColumns } from "../../../../../hooks/table/columns/use-tax-rates-table-columns"
+import { useTaxRateTableFilters } from "../../../../../hooks/table/filters/use-tax-rate-table-filters"
+import { useTaxRateTableQuery } from "../../../../../hooks/table/query/use-tax-rate-table-query"
+import { useDataTable } from "../../../../../hooks/use-data-table"
+
+const PAGE_SIZE = 10
+
+type TaxRateListProps = {
+ taxRegion: TaxRegionResponse
+ isDefault: boolean
+}
+
+export const TaxRateList = ({
+ taxRegion,
+ isDefault = false,
+}: TaxRateListProps) => {
+ const [rowSelection, setRowSelection] = useState({})
+
+ const { searchParams, raw } = useTaxRateTableQuery({ pageSize: PAGE_SIZE })
+ const childrenIds = taxRegion.children?.map((c) => c.id) || []
+ const {
+ tax_rates: taxRates,
+ count,
+ isLoading,
+ isError,
+ error,
+ } = useTaxRates(
+ {
+ ...searchParams,
+ tax_region_id: [taxRegion.id, ...childrenIds],
+ is_default: isDefault,
+ },
+ {
+ placeholderData: keepPreviousData,
+ }
+ )
+
+ const columns = useColumns()
+ const filters = useTaxRateTableFilters()
+
+ const { table } = useDataTable({
+ data: taxRates ?? [],
+ columns,
+ count,
+ enablePagination: true,
+ enableRowSelection: true,
+ pageSize: PAGE_SIZE,
+ getRowId: (row) => row.id,
+ rowSelection: {
+ state: rowSelection,
+ updater: setRowSelection,
+ },
+ meta: {
+ taxRegionId: taxRegion.id,
+ },
+ })
+
+ const { mutateAsync } = useDeleteTaxRegion(taxRegion.id)
+
+ const prompt = usePrompt()
+ const { t } = useTranslation()
+
+ const handleRemove = async () => {
+ const result = await prompt({
+ title: t("general.areYouSure"),
+ description: t("taxRegions.removeWarning", {
+ tax_region_name: taxRegion.name,
+ }),
+ confirmText: t("actions.delete"),
+ cancelText: t("actions.cancel"),
+ })
+
+ if (!result) {
+ return
+ }
+
+ await mutateAsync(undefined, {
+ onSuccess: () => {
+ setRowSelection({})
+ },
+ })
+ }
+
+ if (isError) {
+ throw error
+ }
+
+ return (
+
+
+
+ {isDefault ? `Default ${t("taxRates.domain")}` : `Tax Rate Overrides`}
+
+
+
+
+
+
+
+
+ `/settings/taxes/${taxRegion.id}/tax-rates/${row.id}/edit`
+ }
+ isLoading={isLoading}
+ orderBy={["is_default", "rate", "created_at", "updated_at"]}
+ queryObject={raw}
+ />
+
+ )
+}
+
+const columnHelper = createColumnHelper()
+
+const useColumns = () => {
+ const base = useTaxRateTableColumns()
+
+ return useMemo(
+ () => [
+ ...base,
+ columnHelper.display({
+ id: "actions",
+ cell: ({ row, table }) => {
+ const { taxRegionId } = table.options.meta as {
+ taxRegionId: string
+ }
+
+ return (
+
+ )
+ },
+ }),
+ ],
+ [base]
+ )
+}
+
+const TaxRateListActions = ({
+ taxRateId,
+ taxRegionId,
+}: {
+ taxRateId: string
+ taxRegionId: string
+}) => {
+ const { t } = useTranslation()
+
+ const { mutateAsync } = useDeleteTaxRate(taxRateId)
+
+ const onRemove = async () => await mutateAsync()
+
+ return (
+ ,
+ label: t("actions.edit"),
+ to: `/settings/taxes/${taxRegionId}/tax-rates/${taxRateId}/edit`,
+ },
+ ],
+ },
+ {
+ actions: [
+ {
+ icon: ,
+ label: t("actions.remove"),
+ onClick: onRemove,
+ },
+ ],
+ },
+ ]}
+ />
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/tax-region-detail/components/tax-region-general-detail/index.ts b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-region-detail/components/tax-region-general-detail/index.ts
new file mode 100644
index 0000000000..c9f34fc23f
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-region-detail/components/tax-region-general-detail/index.ts
@@ -0,0 +1 @@
+export * from "./tax-region-general-detail"
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/tax-region-detail/components/tax-region-general-detail/tax-region-general-detail.tsx b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-region-detail/components/tax-region-general-detail/tax-region-general-detail.tsx
new file mode 100644
index 0000000000..3300174685
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-region-detail/components/tax-region-general-detail/tax-region-general-detail.tsx
@@ -0,0 +1,41 @@
+import { TaxRegionResponse } from "@medusajs/types"
+import { Container, Heading, Text } from "@medusajs/ui"
+import { useTranslation } from "react-i18next"
+import { formatDate } from "../../../../../components/common/date"
+import { getCountryByIso2 } from "../../../../../lib/countries"
+
+type TaxRegionGeneralDetailProps = {
+ taxRegion: TaxRegionResponse
+}
+
+export const TaxRegionGeneralDetail = ({
+ taxRegion,
+}: TaxRegionGeneralDetailProps) => {
+ const { t } = useTranslation()
+ const countryCode = taxRegion.parent?.country_code || taxRegion.country_code
+ const displayName = getCountryByIso2(countryCode)?.display_name || countryCode
+
+ return (
+
+
+
+ {displayName}
+
+
+ {t("taxRegions.description")}
+
+
+
+
+
+
+ {t("fields.created")}
+
+
+
+ {formatDate(taxRegion.created_at)}
+
+
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/tax-region-detail/index.ts b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-region-detail/index.ts
new file mode 100644
index 0000000000..38f10d7514
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-region-detail/index.ts
@@ -0,0 +1,4 @@
+export * from "./tax-region-detail"
+
+export { taxRegionLoader as loader } from "./loader"
+export { TaxRegionDetail as Component } from "./tax-region-detail"
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/tax-region-detail/loader.ts b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-region-detail/loader.ts
new file mode 100644
index 0000000000..ba3e896841
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-region-detail/loader.ts
@@ -0,0 +1,21 @@
+import { LoaderFunctionArgs } from "react-router-dom"
+
+import { AdminTaxRegionResponse } from "@medusajs/types"
+import { taxRegionsQueryKeys } from "../../../hooks/api/tax-regions"
+import { client } from "../../../lib/client"
+import { queryClient } from "../../../lib/medusa"
+
+const taxRegionDetailQuery = (id: string) => ({
+ queryKey: taxRegionsQueryKeys.detail(id),
+ queryFn: async () => client.taxes.retrieveTaxRegion(id),
+})
+
+export const taxRegionLoader = async ({ params }: LoaderFunctionArgs) => {
+ const id = params.id
+ const query = taxRegionDetailQuery(id!)
+
+ return (
+ queryClient.getQueryData(query.queryKey) ??
+ (await queryClient.fetchQuery(query))
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/tax-region-detail/tax-region-detail.tsx b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-region-detail/tax-region-detail.tsx
new file mode 100644
index 0000000000..eaf249b3ab
--- /dev/null
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-region-detail/tax-region-detail.tsx
@@ -0,0 +1,30 @@
+import { Outlet, useParams } from "react-router-dom"
+import { JsonViewSection } from "../../../components/common/json-view-section"
+import { useTaxRegion } from "../../../hooks/api/tax-regions"
+import { TaxRateList } from "./components/tax-rate-list"
+import { TaxRegionGeneralDetail } from "./components/tax-region-general-detail"
+
+export const TaxRegionDetail = () => {
+ const { id } = useParams()
+ const { tax_region: taxRegion, isLoading, isError, error } = useTaxRegion(id!)
+
+ if (isLoading) {
+ return Loading...
+ }
+
+ if (isError) {
+ throw error
+ }
+
+ return (
+ taxRegion && (
+
+
+
+
+
+
+
+ )
+ )
+}
diff --git a/packages/admin-next/dashboard/src/v2-routes/taxes/tax-region-list/components/region-list-table/tax-region-list-table.tsx b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-region-list/components/region-list-table/tax-region-list-table.tsx
index 95b66eedd8..dd424ea18a 100644
--- a/packages/admin-next/dashboard/src/v2-routes/taxes/tax-region-list/components/region-list-table/tax-region-list-table.tsx
+++ b/packages/admin-next/dashboard/src/v2-routes/taxes/tax-region-list/components/region-list-table/tax-region-list-table.tsx
@@ -1,31 +1,34 @@
-import { PencilSquare, Trash } from "@medusajs/icons"
-import { Button, Container, Heading, usePrompt } from "@medusajs/ui"
+import { Trash } from "@medusajs/icons"
+import { AdminTaxRegionResponse } from "@medusajs/types"
+import { Button, Container, Heading } from "@medusajs/ui"
import { createColumnHelper } from "@tanstack/react-table"
+import { t } from "i18next"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
-import { Link } from "react-router-dom"
-import { AdminTaxRegionResponse } from "@medusajs/types"
+import { Link, useNavigate } from "react-router-dom"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { DataTable } from "../../../../../components/table/data-table"
-import { useRegionTableFilters } from "../../../../../hooks/table/filters/use-region-table-filters"
-import { useDataTable } from "../../../../../hooks/use-data-table"
-import { useDeleteRegion } from "../../../../../hooks/api/regions"
-import { useTaxRegions } from "../../../../../hooks/api/tax-regions"
+import {
+ useDeleteTaxRegion,
+ useTaxRegions,
+} from "../../../../../hooks/api/tax-regions"
import { useTaxRegionTableQuery } from "../../../../../hooks/table/query/use-tax-region-table-query copy"
-import { t } from "i18next"
-import { DateCell } from "../../../../../components/table/table-cells/common/date-cell"
+import { useDataTable } from "../../../../../hooks/use-data-table"
+import { getCountryByIso2 } from "../../../../../lib/countries"
const PAGE_SIZE = 20
export const TaxRegionListTable = () => {
const { t } = useTranslation()
- const { searchParams, raw } = useTaxRegionTableQuery({ pageSize: PAGE_SIZE })
+ const { searchParams, raw } = useTaxRegionTableQuery({
+ pageSize: PAGE_SIZE,
+ })
const { tax_regions, count, isLoading, isError, error } = useTaxRegions({
...searchParams,
+ parent_id: "null",
})
- const filters = useRegionTableFilters()
const columns = useColumns()
const { table } = useDataTable({
@@ -45,11 +48,9 @@ export const TaxRegionListTable = () => {
{t("taxes.domain")}
-
-
-
+
{
count={count}
pageSize={PAGE_SIZE}
isLoading={isLoading}
- filters={filters}
- orderBy={["name", "created_at", "updated_at"]}
navigateTo={(row) => `${row.original.id}`}
pagination
- search
queryObject={raw}
/>
@@ -74,6 +72,16 @@ const TaxRegionActions = ({
taxRegion: AdminTaxRegionResponse["tax_region"]
}) => {
const { t } = useTranslation()
+ const navigate = useNavigate()
+ const { mutateAsync } = useDeleteTaxRegion(taxRegion.id)
+
+ const handleDelete = async () => {
+ await mutateAsync(undefined, {
+ onSuccess: () => {
+ navigate("/settings/taxes", { replace: true })
+ },
+ })
+ }
return (
,
+ icon: ,
+ label: t("actions.delete"),
+ onClick: handleDelete,
},
],
},
@@ -97,20 +105,18 @@ const columnHelper = createColumnHelper()
const useColumns = () => {
return useMemo(
() => [
- columnHelper.accessor("name", {
- header: t("fields.code"),
- cell: ({ getValue }) => (
-
- {getValue()}
-
- ),
- }),
- columnHelper.accessor("created_at", {
- header: t("fields.created"),
+ columnHelper.accessor("country_code", {
+ header: t("fields.country"),
cell: ({ getValue }) => {
- const date = getValue()
+ const countryCode = getValue()
+ const displayName =
+ getCountryByIso2(countryCode)?.display_name || countryCode
- return
+ return (
+
+ {displayName}
+
+ )
},
}),
columnHelper.display({
diff --git a/packages/medusa/src/api-v2/admin/tax-rates/[id]/route.ts b/packages/medusa/src/api-v2/admin/tax-rates/[id]/route.ts
index c8be812b48..89df349cc5 100644
--- a/packages/medusa/src/api-v2/admin/tax-rates/[id]/route.ts
+++ b/packages/medusa/src/api-v2/admin/tax-rates/[id]/route.ts
@@ -1,21 +1,20 @@
-import {
- AuthenticatedMedusaRequest,
- MedusaResponse,
-} from "../../../../types/routing"
-
-import {
- remoteQueryObjectFromString,
- ContainerRegistrationKeys,
-} from "@medusajs/utils"
import {
deleteTaxRatesWorkflow,
updateTaxRatesWorkflow,
} from "@medusajs/core-flows"
+import {
+ ContainerRegistrationKeys,
+ remoteQueryObjectFromString,
+} from "@medusajs/utils"
+import {
+ AuthenticatedMedusaRequest,
+ MedusaResponse,
+} from "../../../../types/routing"
+import { refetchTaxRate } from "../helpers"
import {
AdminGetTaxRateParamsType,
AdminUpdateTaxRateType,
} from "../validators"
-import { refetchTaxRate } from "../helpers"
export const POST = async (
req: AuthenticatedMedusaRequest,
diff --git a/packages/medusa/src/api-v2/admin/tax-rates/middlewares.ts b/packages/medusa/src/api-v2/admin/tax-rates/middlewares.ts
index 1a8d4274b9..63e810ef8d 100644
--- a/packages/medusa/src/api-v2/admin/tax-rates/middlewares.ts
+++ b/packages/medusa/src/api-v2/admin/tax-rates/middlewares.ts
@@ -1,5 +1,6 @@
import * as QueryConfig from "./query-config"
+import { validateAndTransformQuery } from "../../utils/validate-query"
import {
AdminCreateTaxRate,
AdminCreateTaxRateRule,
@@ -11,7 +12,6 @@ import {
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
import { authenticate } from "../../../utils/authenticate-middleware"
import { validateAndTransformBody } from "../../utils/validate-body"
-import { validateAndTransformQuery } from "../../utils/validate-query"
export const adminTaxRateRoutesMiddlewares: MiddlewareRoute[] = [
{
@@ -41,16 +41,6 @@ export const adminTaxRateRoutesMiddlewares: MiddlewareRoute[] = [
),
],
},
- {
- method: "GET",
- matcher: "/admin/tax-rates",
- middlewares: [
- validateAndTransformQuery(
- AdminGetTaxRatesParams,
- QueryConfig.listTransformQueryConfig
- ),
- ],
- },
{
method: "GET",
matcher: "/admin/tax-rates/:id",
@@ -61,6 +51,16 @@ export const adminTaxRateRoutesMiddlewares: MiddlewareRoute[] = [
),
],
},
+ {
+ method: "GET",
+ matcher: "/admin/tax-rates",
+ middlewares: [
+ validateAndTransformQuery(
+ AdminGetTaxRatesParams,
+ QueryConfig.listTransformQueryConfig
+ ),
+ ],
+ },
{
method: "POST",
matcher: "/admin/tax-rates/:id/rules",
diff --git a/packages/medusa/src/api-v2/admin/tax-rates/query-config.ts b/packages/medusa/src/api-v2/admin/tax-rates/query-config.ts
index b405ddc44c..c2bda15988 100644
--- a/packages/medusa/src/api-v2/admin/tax-rates/query-config.ts
+++ b/packages/medusa/src/api-v2/admin/tax-rates/query-config.ts
@@ -1,6 +1,4 @@
-export const defaultAdminTaxRateRelations = []
-export const allowedAdminTaxRateRelations = []
-export const defaultAdminTaxRateFields = [
+export const defaults = [
"id",
"name",
"code",
@@ -13,14 +11,16 @@ export const defaultAdminTaxRateFields = [
"updated_at",
"deleted_at",
"metadata",
+ "*tax_region",
+ "*rules",
]
export const retrieveTransformQueryConfig = {
- defaults: defaultAdminTaxRateFields,
+ defaults,
isList: false,
}
export const listTransformQueryConfig = {
- defaults: defaultAdminTaxRateFields,
+ defaults,
isList: true,
}
diff --git a/packages/medusa/src/api-v2/admin/tax-rates/route.ts b/packages/medusa/src/api-v2/admin/tax-rates/route.ts
index 75579d9d0d..4553826d72 100644
--- a/packages/medusa/src/api-v2/admin/tax-rates/route.ts
+++ b/packages/medusa/src/api-v2/admin/tax-rates/route.ts
@@ -1,17 +1,17 @@
import { createTaxRatesWorkflow } from "@medusajs/core-flows"
import {
- remoteQueryObjectFromString,
ContainerRegistrationKeys,
+ remoteQueryObjectFromString,
} from "@medusajs/utils"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../types/routing"
+import { refetchTaxRate } from "./helpers"
import {
AdminCreateTaxRateType,
AdminGetTaxRatesParamsType,
} from "./validators"
-import { refetchTaxRate } from "./helpers"
export const POST = async (
req: AuthenticatedMedusaRequest,
diff --git a/packages/medusa/src/api-v2/admin/tax-rates/validators.ts b/packages/medusa/src/api-v2/admin/tax-rates/validators.ts
index f84a403516..5420fcfd2a 100644
--- a/packages/medusa/src/api-v2/admin/tax-rates/validators.ts
+++ b/packages/medusa/src/api-v2/admin/tax-rates/validators.ts
@@ -1,5 +1,9 @@
-import { createFindParams, createSelectParams } from "../../utils/validators"
import { z } from "zod"
+import {
+ createFindParams,
+ createOperatorMap,
+ createSelectParams,
+} from "../../utils/validators"
export type AdminGetTaxRateParamsType = z.infer
export const AdminGetTaxRateParams = createSelectParams()
@@ -8,7 +12,20 @@ export type AdminGetTaxRatesParamsType = z.infer
export const AdminGetTaxRatesParams = createFindParams({
limit: 20,
offset: 0,
-})
+}).merge(
+ z.object({
+ q: z.string().optional(),
+ tax_region_id: z
+ .union([z.string(), z.array(z.string()), createOperatorMap()])
+ .optional(),
+ is_default: z.union([z.literal("true"), z.literal("false")]).optional(),
+ created_at: createOperatorMap().optional(),
+ updated_at: createOperatorMap().optional(),
+ deleted_at: createOperatorMap().optional(),
+ $and: z.lazy(() => AdminGetTaxRatesParams.array()).optional(),
+ $or: z.lazy(() => AdminGetTaxRatesParams.array()).optional(),
+ })
+)
export type AdminCreateTaxRateRuleType = z.infer
export const AdminCreateTaxRateRule = z.object({
diff --git a/packages/medusa/src/api-v2/admin/tax-regions/[id]/route.ts b/packages/medusa/src/api-v2/admin/tax-regions/[id]/route.ts
index d23d7f9eec..4e4697953e 100644
--- a/packages/medusa/src/api-v2/admin/tax-regions/[id]/route.ts
+++ b/packages/medusa/src/api-v2/admin/tax-regions/[id]/route.ts
@@ -1,9 +1,31 @@
import { deleteTaxRegionsWorkflow } from "@medusajs/core-flows"
+import {
+ ContainerRegistrationKeys,
+ remoteQueryObjectFromString,
+} from "@medusajs/utils"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../../types/routing"
+export const GET = async (
+ req: AuthenticatedMedusaRequest,
+ res: MedusaResponse
+) => {
+ const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
+
+ const filters = { id: req.params.id }
+ const [taxRegion] = await remoteQuery(
+ remoteQueryObjectFromString({
+ entryPoint: "tax_region",
+ variables: { filters },
+ fields: req.remoteQueryConfig.fields,
+ })
+ )
+
+ res.status(200).json({ tax_region: taxRegion })
+}
+
export const DELETE = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
diff --git a/packages/medusa/src/api-v2/admin/tax-regions/middlewares.ts b/packages/medusa/src/api-v2/admin/tax-regions/middlewares.ts
index fc149ee14f..288dcd9245 100644
--- a/packages/medusa/src/api-v2/admin/tax-regions/middlewares.ts
+++ b/packages/medusa/src/api-v2/admin/tax-regions/middlewares.ts
@@ -1,6 +1,10 @@
import * as QueryConfig from "./query-config"
-import { AdminCreateTaxRegion, AdminGetTaxRegionsParams } from "./validators"
+import {
+ AdminCreateTaxRegion,
+ AdminGetTaxRegionParams,
+ AdminGetTaxRegionsParams,
+} from "./validators"
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
import { authenticate } from "../../../utils/authenticate-middleware"
@@ -34,4 +38,14 @@ export const adminTaxRegionRoutesMiddlewares: MiddlewareRoute[] = [
),
],
},
+ {
+ method: "GET",
+ matcher: "/admin/tax-regions/:id",
+ middlewares: [
+ validateAndTransformQuery(
+ AdminGetTaxRegionParams,
+ QueryConfig.retrieveTransformQueryConfig
+ ),
+ ],
+ },
]
diff --git a/packages/medusa/src/api-v2/admin/tax-regions/query-config.ts b/packages/medusa/src/api-v2/admin/tax-regions/query-config.ts
index 73ef61ff25..1173d47075 100644
--- a/packages/medusa/src/api-v2/admin/tax-regions/query-config.ts
+++ b/packages/medusa/src/api-v2/admin/tax-regions/query-config.ts
@@ -9,6 +9,12 @@ export const defaults = [
"updated_at",
"deleted_at",
"metadata",
+ "*children",
+ "*children.tax_rates",
+ "*children.tax_rates.rules",
+ "*parent",
+ "*tax_rates",
+ "*tax_rates.rules",
]
export const retrieveTransformQueryConfig = {
diff --git a/packages/medusa/src/api-v2/admin/tax-regions/validators.ts b/packages/medusa/src/api-v2/admin/tax-regions/validators.ts
index 834ff1dec6..11bf4f3b87 100644
--- a/packages/medusa/src/api-v2/admin/tax-regions/validators.ts
+++ b/packages/medusa/src/api-v2/admin/tax-regions/validators.ts
@@ -19,9 +19,16 @@ export const AdminGetTaxRegionsParams = createFindParams({
}).merge(
z.object({
id: z.union([z.string(), z.array(z.string())]).optional(),
- country_code: z.union([z.string(), z.array(z.string())]).optional(),
- province_code: z.union([z.string(), z.array(z.string())]).optional(),
- parent_id: z.union([z.string(), z.array(z.string())]).optional(),
+ q: z.string().optional(),
+ country_code: z
+ .union([z.string(), z.array(z.string()), createOperatorMap()])
+ .optional(),
+ province_code: z
+ .union([z.string(), z.array(z.string()), createOperatorMap()])
+ .optional(),
+ parent_id: z
+ .union([z.string(), z.array(z.string()), createOperatorMap()])
+ .optional(),
created_by: z.union([z.string(), z.array(z.string())]).optional(),
created_at: createOperatorMap().optional(),
updated_at: createOperatorMap().optional(),
@@ -41,6 +48,9 @@ export const AdminCreateTaxRegion = z.object({
rate: z.number().optional(),
code: z.string().optional(),
name: z.string(),
+ is_combinable: z
+ .union([z.literal("true"), z.literal("false")])
+ .optional(),
metadata: z.record(z.unknown()).optional(),
})
.optional(),
diff --git a/packages/tax/src/joiner-config.ts b/packages/tax/src/joiner-config.ts
index d89a3bda3e..5f47703061 100644
--- a/packages/tax/src/joiner-config.ts
+++ b/packages/tax/src/joiner-config.ts
@@ -1,7 +1,7 @@
import { Modules } from "@medusajs/modules-sdk"
import { ModuleJoinerConfig } from "@medusajs/types"
import { MapToConfig } from "@medusajs/utils"
-import { TaxRate, TaxRegion, TaxRateRule, TaxProvider } from "@models"
+import { TaxProvider, TaxRate, TaxRateRule, TaxRegion } from "@models"
export const LinkableKeys: Record = {
tax_rate_id: TaxRate.name,
diff --git a/packages/tax/src/models/tax-rate.ts b/packages/tax/src/models/tax-rate.ts
index 6add3d5693..8b2e6f8212 100644
--- a/packages/tax/src/models/tax-rate.ts
+++ b/packages/tax/src/models/tax-rate.ts
@@ -1,24 +1,25 @@
import { DAL } from "@medusajs/types"
import {
- DALUtils,
createPsqlIndexStatementHelper,
+ DALUtils,
generateEntityId,
+ Searchable,
} from "@medusajs/utils"
import {
- Filter,
BeforeCreate,
Cascade,
Collection,
Entity,
+ Filter,
ManyToOne,
- OnInit,
OneToMany,
+ OnInit,
OptionalProps,
PrimaryKey,
Property,
} from "@mikro-orm/core"
-import TaxRegion from "./tax-region"
import TaxRateRule from "./tax-rate-rule"
+import TaxRegion from "./tax-region"
type OptionalTaxRateProps = DAL.SoftDeletableEntityDateColumns
@@ -53,9 +54,11 @@ export default class TaxRate {
@Property({ columnType: "real", nullable: true })
rate: number | null = null
+ @Searchable()
@Property({ columnType: "text", nullable: true })
code: string | null = null
+ @Searchable()
@Property({ columnType: "text" })
name: string
@@ -66,7 +69,7 @@ export default class TaxRate {
is_combinable = false
@ManyToOne(() => TaxRegion, {
- type: "text",
+ columnType: "text",
fieldName: "tax_region_id",
mapToPk: true,
onDelete: "cascade",
@@ -79,7 +82,6 @@ export default class TaxRate {
@OneToMany(() => TaxRateRule, (rule) => rule.tax_rate, {
cascade: ["soft-remove" as Cascade],
- persist: false,
})
rules = new Collection(this)
diff --git a/packages/tax/src/models/tax-region.ts b/packages/tax/src/models/tax-region.ts
index 4d58a6566d..f053a40edc 100644
--- a/packages/tax/src/models/tax-region.ts
+++ b/packages/tax/src/models/tax-region.ts
@@ -1,25 +1,26 @@
import { DAL } from "@medusajs/types"
import {
DALUtils,
+ Searchable,
createPsqlIndexStatementHelper,
generateEntityId,
} from "@medusajs/utils"
import {
- Filter,
BeforeCreate,
+ Cascade,
+ Check,
Collection,
Entity,
+ Filter,
+ ManyToOne,
OnInit,
+ OneToMany,
OptionalProps,
PrimaryKey,
Property,
- OneToMany,
- ManyToOne,
- Check,
- Cascade,
} from "@mikro-orm/core"
-import TaxRate from "./tax-rate"
import TaxProvider from "./tax-provider"
+import TaxRate from "./tax-rate"
type OptionalTaxRegionProps = DAL.SoftDeletableEntityDateColumns
@@ -63,9 +64,14 @@ export default class TaxRegion {
})
provider_id: string | null = null
+ @ManyToOne(() => TaxProvider, { persist: false })
+ provider: TaxProvider
+
+ @Searchable()
@Property({ columnType: "text" })
country_code: string
+ @Searchable()
@Property({ columnType: "text", nullable: true })
province_code: string | null = null
diff --git a/packages/types/src/http/tax/admin/index.ts b/packages/types/src/http/tax/admin/index.ts
index d69cd9a33b..a0d80a2f66 100644
--- a/packages/types/src/http/tax/admin/index.ts
+++ b/packages/types/src/http/tax/admin/index.ts
@@ -1 +1,2 @@
+export * from "./tax-rates"
export * from "./tax-regions"
diff --git a/packages/types/src/http/tax/admin/tax-rates.ts b/packages/types/src/http/tax/admin/tax-rates.ts
new file mode 100644
index 0000000000..56eb031bcd
--- /dev/null
+++ b/packages/types/src/http/tax/admin/tax-rates.ts
@@ -0,0 +1,39 @@
+import { PaginatedResponse } from "../../../common"
+import { TaxRegionResponse } from "./tax-regions"
+
+/**
+ * @experimental
+ */
+export interface TaxRateResponse {
+ id: string
+ rate: number | null
+ code: string | null
+ name: string
+ metadata: Record | null
+ tax_region_id: string
+ is_combinable: boolean
+ is_default: boolean
+ created_at: string | Date
+ updated_at: string | Date
+ deleted_at: Date | null
+ created_by: string | null
+ tax_region: TaxRegionResponse
+ rules: {
+ reference: string
+ reference_id: string
+ }[]
+}
+
+/**
+ * @experimental
+ */
+export interface AdminTaxRateResponse {
+ tax_rate: TaxRateResponse
+}
+
+/**
+ * @experimental
+ */
+export interface AdminTaxRateListResponse extends PaginatedResponse {
+ tax_rates: TaxRateResponse[]
+}
diff --git a/packages/types/src/http/tax/admin/tax-regions.ts b/packages/types/src/http/tax/admin/tax-regions.ts
index 44d1d02979..dcd4a85aaa 100644
--- a/packages/types/src/http/tax/admin/tax-regions.ts
+++ b/packages/types/src/http/tax/admin/tax-regions.ts
@@ -1,21 +1,29 @@
import { PaginatedResponse } from "../../../common"
+import { TaxRateResponse } from "./tax-rates"
/**
* @experimental
*/
-interface TaxRegionResponse {
+export interface TaxRegionResponse {
id: string
rate: number | null
code: string | null
+ country_code: string | null
+ province_code: string | null
name: string
metadata: Record | null
tax_region_id: string
is_combinable: boolean
is_default: boolean
+ parent_id: string | null
created_at: string | Date
updated_at: string | Date
deleted_at: Date | null
created_by: string | null
+
+ tax_rates: TaxRateResponse[]
+
+ parent: TaxRegionResponse
}
/**