feat(dashboard, js-sdk, medusa, tax, types): custom tax providers (#12297)
* wip: setup loaders, add endpoints, module work, types, js sdk * fix: tax module provider loader * feat: select provider on region create, fix enpoint middleware registration * feat: edit form * fix: rename param * chore: changeset * fix: don't default to system provider * fix: admin fixes, dispalt tax provider * fix: some tests and types * fix: remove provider from province regions in test * fix: more tests, optional provider for sublevel regions, fix few types * fix: OE test * feat: edit tax region admin, update tax region core flow changes * feat: migrate script * fix: refactor * chore: use query graph * feat: provider section
This commit is contained in:
@@ -1626,6 +1626,11 @@ export function getRouteMap({
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "edit",
|
||||
lazy: () =>
|
||||
import("../../routes/tax-regions/tax-region-edit"),
|
||||
},
|
||||
{
|
||||
path: "provinces/:province_id",
|
||||
lazy: async () => {
|
||||
|
||||
@@ -74,6 +74,30 @@ export const useCreateTaxRegion = (
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateTaxRegion = (
|
||||
id: string,
|
||||
query?: HttpTypes.AdminTaxRegionParams,
|
||||
options?: UseMutationOptions<
|
||||
HttpTypes.AdminTaxRegionResponse,
|
||||
FetchError,
|
||||
HttpTypes.AdminUpdateTaxRegion,
|
||||
QueryKey
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload) => sdk.admin.taxRegion.update(id, payload, query),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: taxRegionsQueryKeys.detail(id),
|
||||
})
|
||||
queryClient.invalidateQueries({ queryKey: taxRegionsQueryKeys.lists() })
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteTaxRegion = (
|
||||
id: string,
|
||||
options?: UseMutationOptions<
|
||||
|
||||
@@ -2038,7 +2038,7 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["label", "hint"],
|
||||
"required": ["label", "hint", "placeholder"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"subtitle": {
|
||||
@@ -2051,7 +2051,7 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["label"],
|
||||
"required": ["label", "placeholder"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"handle": {
|
||||
@@ -2067,7 +2067,7 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["label", "tooltip"],
|
||||
"required": ["label", "tooltip", "placeholder"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"description": {
|
||||
@@ -2083,7 +2083,7 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["label", "hint"],
|
||||
"required": ["label", "hint", "placeholder"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"discountable": {
|
||||
@@ -6479,6 +6479,16 @@
|
||||
"required": ["header", "hint", "errors", "successToast"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"edit": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"successToast": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["successToast"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"province": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -6502,6 +6512,16 @@
|
||||
"required": ["header", "create"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"provider": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"header": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["header"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"state": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -6910,6 +6930,9 @@
|
||||
"taxCode": {
|
||||
"type": "string"
|
||||
},
|
||||
"taxProvider": {
|
||||
"type": "string"
|
||||
},
|
||||
"targets": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -7240,6 +7263,7 @@
|
||||
"defaultTaxRate",
|
||||
"taxRate",
|
||||
"taxCode",
|
||||
"taxProvider",
|
||||
"targets",
|
||||
"sublevels",
|
||||
"noDefaultRate"
|
||||
@@ -7252,7 +7276,9 @@
|
||||
"list",
|
||||
"delete",
|
||||
"create",
|
||||
"edit",
|
||||
"province",
|
||||
"provider",
|
||||
"state",
|
||||
"stateOrTerritory",
|
||||
"county",
|
||||
@@ -9192,9 +9218,6 @@
|
||||
"deleteRateDescription": {
|
||||
"type": "string"
|
||||
},
|
||||
"editTaxRate": {
|
||||
"type": "string"
|
||||
},
|
||||
"editRateAction": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -9252,7 +9275,6 @@
|
||||
"createTaxRate",
|
||||
"createTaxRateHint",
|
||||
"deleteRateDescription",
|
||||
"editTaxRate",
|
||||
"editRateAction",
|
||||
"editOverridesAction",
|
||||
"editOverridesTitle",
|
||||
|
||||
@@ -1723,6 +1723,9 @@
|
||||
},
|
||||
"successToast": "The tax region was successfully created."
|
||||
},
|
||||
"edit": {
|
||||
"successToast": "The tax region was successfully updated."
|
||||
},
|
||||
"province": {
|
||||
"header": "Provinces",
|
||||
"create": {
|
||||
@@ -1730,6 +1733,9 @@
|
||||
"hint": "Create a new tax region to define tax rates for a specific province."
|
||||
}
|
||||
},
|
||||
"provider": {
|
||||
"header": "Tax Provider"
|
||||
},
|
||||
"state": {
|
||||
"header": "States",
|
||||
"create": {
|
||||
@@ -1855,6 +1861,7 @@
|
||||
},
|
||||
"taxRate": "Tax rate",
|
||||
"taxCode": "Tax code",
|
||||
"taxProvider": "Tax provider",
|
||||
"targets": {
|
||||
"label": "Targets",
|
||||
"hint": "Select the targets that this tax rate will apply to.",
|
||||
@@ -2471,7 +2478,6 @@
|
||||
"createTaxRate": "Create Tax Rate",
|
||||
"createTaxRateHint": "Create a new tax rate for the region.",
|
||||
"deleteRateDescription": "You are about to delete the tax rate {{name}}. This action cannot be undone.",
|
||||
"editTaxRate": "Edit Tax Rate",
|
||||
"editRateAction": "Edit rate",
|
||||
"editOverridesAction": "Edit overrides",
|
||||
"editOverridesTitle": "Edit Tax Rate Overrides",
|
||||
|
||||
+23
-5
@@ -2,11 +2,20 @@ import { HttpTypes } from "@medusajs/types"
|
||||
import { Heading, Text, Tooltip, clx } from "@medusajs/ui"
|
||||
import ReactCountryFlag from "react-country-flag"
|
||||
|
||||
import { ExclamationCircle, MapPin, Plus, Trash } from "@medusajs/icons"
|
||||
import {
|
||||
ExclamationCircle,
|
||||
MapPin,
|
||||
Plus,
|
||||
Trash,
|
||||
PencilSquare,
|
||||
} from "@medusajs/icons"
|
||||
import { ComponentPropsWithoutRef, ReactNode } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link } from "react-router-dom"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import {
|
||||
Action,
|
||||
ActionMenu,
|
||||
} from "../../../../../components/common/action-menu"
|
||||
import { IconAvatar } from "../../../../../components/common/icon-avatar"
|
||||
import { getCountryByIso2 } from "../../../../../lib/data/countries"
|
||||
import {
|
||||
@@ -107,7 +116,9 @@ export const TaxRegionCard = ({
|
||||
{name}
|
||||
</Text>
|
||||
) : (
|
||||
<Heading>{name}</Heading>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Heading>{name}</Heading>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -164,7 +175,9 @@ const TaxRegionCardActions = ({
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const to = taxRegion.parent_id
|
||||
const hasParent = !!taxRegion.parent_id
|
||||
|
||||
const to = hasParent
|
||||
? `/settings/tax-regions/${taxRegion.parent_id}`
|
||||
: undefined
|
||||
const handleDelete = useDeleteTaxRegionAction({ taxRegion, to })
|
||||
@@ -187,12 +200,17 @@ const TaxRegionCardActions = ({
|
||||
: []),
|
||||
{
|
||||
actions: [
|
||||
!hasParent && {
|
||||
icon: <PencilSquare />,
|
||||
label: t("actions.edit"),
|
||||
to: `/settings/tax-regions/${taxRegion.id}/edit`,
|
||||
},
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("actions.delete"),
|
||||
onClick: handleDelete,
|
||||
},
|
||||
],
|
||||
].filter(Boolean) as unknown as Action[],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
+40
@@ -14,6 +14,10 @@ import {
|
||||
} from "../../../../../components/modals"
|
||||
import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
|
||||
import { useCreateTaxRegion } from "../../../../../hooks/api"
|
||||
import { useComboboxData } from "../../../../../hooks/use-combobox-data"
|
||||
import { Combobox } from "../../../../../components/inputs/combobox"
|
||||
import { formatProvider } from "../../../../../lib/format-provider"
|
||||
import { sdk } from "../../../../../lib/client"
|
||||
|
||||
type TaxRegionCreateFormProps = {
|
||||
parentId?: string
|
||||
@@ -27,12 +31,23 @@ const TaxRegionCreateSchema = z.object({
|
||||
value: z.string().optional(),
|
||||
}),
|
||||
country_code: z.string().min(1),
|
||||
provider_id: z.string(),
|
||||
})
|
||||
|
||||
export const TaxRegionCreateForm = ({ parentId }: TaxRegionCreateFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
const taxProviders = useComboboxData({
|
||||
queryKey: ["tax_providers"],
|
||||
queryFn: (params) => sdk.admin.taxProvider.list(params),
|
||||
getOptions: (data) =>
|
||||
data.tax_providers.map((provider) => ({
|
||||
label: formatProvider(provider.id),
|
||||
value: provider.id,
|
||||
})),
|
||||
})
|
||||
|
||||
const form = useForm<z.infer<typeof TaxRegionCreateSchema>>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
@@ -41,6 +56,7 @@ export const TaxRegionCreateForm = ({ parentId }: TaxRegionCreateFormProps) => {
|
||||
},
|
||||
code: "",
|
||||
country_code: "",
|
||||
provider_id: "",
|
||||
},
|
||||
resolver: zodResolver(TaxRegionCreateSchema),
|
||||
})
|
||||
@@ -64,6 +80,7 @@ export const TaxRegionCreateForm = ({ parentId }: TaxRegionCreateFormProps) => {
|
||||
country_code: values.country_code,
|
||||
parent_id: parentId,
|
||||
default_tax_rate: defaultRate,
|
||||
provider_id: values.provider_id,
|
||||
},
|
||||
{
|
||||
onSuccess: ({ tax_region }) => {
|
||||
@@ -192,6 +209,29 @@ export const TaxRegionCreateForm = ({ parentId }: TaxRegionCreateFormProps) => {
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="provider_id"
|
||||
render={({ field }) => (
|
||||
<Form.Item>
|
||||
<Form.Label>
|
||||
{t("taxRegions.fields.taxProvider")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Combobox
|
||||
{...field}
|
||||
options={taxProviders.options}
|
||||
searchValue={taxProviders.searchValue}
|
||||
onSearchValueChange={
|
||||
taxProviders.onSearchValueChange
|
||||
}
|
||||
fetchNextPage={taxProviders.fetchNextPage}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+4
-2
@@ -1,15 +1,16 @@
|
||||
import { useLoaderData, useParams } from "react-router-dom"
|
||||
import { useState } from "react"
|
||||
|
||||
import { SingleColumnPage } from "../../../components/layout/pages"
|
||||
import { useTaxRegion } from "../../../hooks/api/tax-regions"
|
||||
import { TaxRegionDetailSection } from "./components/tax-region-detail-section"
|
||||
import { TaxRegionProvinceSection } from "./components/tax-region-province-section"
|
||||
|
||||
import { useState } from "react"
|
||||
import { SingleColumnPageSkeleton } from "../../../components/common/skeleton"
|
||||
import { useExtension } from "../../../providers/extension-provider"
|
||||
import { TaxRegionOverrideSection } from "./components/tax-region-override-section"
|
||||
import { TaxRegionSublevelAlert } from "./components/tax-region-sublevel-alert"
|
||||
import { TaxRegionProviderSection } from "./tax-region-provider-section"
|
||||
import { taxRegionLoader } from "./loader"
|
||||
|
||||
export const TaxRegionDetail = () => {
|
||||
@@ -41,7 +42,7 @@ export const TaxRegionDetail = () => {
|
||||
<SingleColumnPage
|
||||
data={taxRegion}
|
||||
showJSON
|
||||
// showMetadata // TOOD -> enable when tax region update is added to the API
|
||||
// showMetadata // TOOD -> enable tax region metadata
|
||||
widgets={{
|
||||
after: getWidgets("tax.details.after"),
|
||||
before: getWidgets("tax.details.before"),
|
||||
@@ -58,6 +59,7 @@ export const TaxRegionDetail = () => {
|
||||
showSublevelRegions={showSublevelRegions}
|
||||
/>
|
||||
<TaxRegionOverrideSection taxRegion={taxRegion} />
|
||||
<TaxRegionProviderSection taxRegion={taxRegion} />
|
||||
</SingleColumnPage>
|
||||
)
|
||||
}
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./tax-region-provider-section"
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { Container, Heading } from "@medusajs/ui"
|
||||
|
||||
import { formatProvider } from "../../../../lib/format-provider"
|
||||
|
||||
export function TaxRegionProviderSection({
|
||||
taxRegion,
|
||||
}: {
|
||||
taxRegion: HttpTypes.AdminTaxRegion
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<Heading level="h2" className="px-6 py-4">
|
||||
{t("taxRegions.provider.header")}
|
||||
</Heading>
|
||||
<div className="px-6 py-4">
|
||||
{taxRegion.provider_id && (
|
||||
<span className="text-ui-fg-subtle">
|
||||
{formatProvider(taxRegion.provider_id!)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./tax-region-edit-form"
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { Button, toast } from "@medusajs/ui"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { z } from "zod"
|
||||
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import { RouteDrawer, useRouteModal } from "../../../../../components/modals"
|
||||
import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
|
||||
import { useComboboxData } from "../../../../../hooks/use-combobox-data"
|
||||
import { sdk } from "../../../../../lib/client"
|
||||
import { Combobox } from "../../../../../components/inputs/combobox"
|
||||
import { formatProvider } from "../../../../../lib/format-provider"
|
||||
import { useUpdateTaxRegion } from "../../../../../hooks/api"
|
||||
|
||||
type TaxRegionEditFormProps = {
|
||||
taxRegion: HttpTypes.AdminTaxRegion
|
||||
}
|
||||
|
||||
const TaxRegionEditSchema = z.object({
|
||||
provider_id: z.string().min(1),
|
||||
})
|
||||
|
||||
export const TaxRegionEditForm = ({ taxRegion }: TaxRegionEditFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
const taxProviders = useComboboxData({
|
||||
queryKey: ["tax_providers"],
|
||||
queryFn: (params) => sdk.admin.taxProvider.list(params),
|
||||
getOptions: (data) =>
|
||||
data.tax_providers.map((provider) => ({
|
||||
label: formatProvider(provider.id),
|
||||
value: provider.id,
|
||||
})),
|
||||
})
|
||||
|
||||
const form = useForm<z.infer<typeof TaxRegionEditSchema>>({
|
||||
defaultValues: {
|
||||
provider_id: taxRegion.provider_id,
|
||||
},
|
||||
resolver: zodResolver(TaxRegionEditSchema),
|
||||
})
|
||||
|
||||
const { mutateAsync, isPending } = useUpdateTaxRegion(taxRegion.id)
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (values) => {
|
||||
await mutateAsync(
|
||||
{
|
||||
provider_id: values.provider_id,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success(t("taxRegions.edit.successToast"))
|
||||
handleSuccess()
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<RouteDrawer.Form form={form}>
|
||||
<KeyboundForm
|
||||
className="flex flex-1 flex-col overflow-hidden"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<RouteDrawer.Body className="flex flex-1 flex-col gap-y-6 overflow-auto">
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="provider_id"
|
||||
render={({ field }) => (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("taxRegions.fields.taxProvider")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Combobox
|
||||
{...field}
|
||||
options={taxProviders.options}
|
||||
searchValue={taxProviders.searchValue}
|
||||
onSearchValueChange={taxProviders.onSearchValueChange}
|
||||
fetchNextPage={taxProviders.fetchNextPage}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</RouteDrawer.Body>
|
||||
<RouteDrawer.Footer className="shrink-0">
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<RouteDrawer.Close asChild>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteDrawer.Close>
|
||||
<Button size="small" type="submit" isLoading={isPending}>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</RouteDrawer.Footer>
|
||||
</KeyboundForm>
|
||||
</RouteDrawer.Form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { TaxRegionEdit as Component } from "./tax-region-edit"
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Heading } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useParams } from "react-router-dom"
|
||||
import { RouteDrawer } from "../../../components/modals"
|
||||
import { TaxRegionEditForm } from "./components/tax-region-edit"
|
||||
import { useTaxRegion } from "../../../hooks/api"
|
||||
|
||||
export const TaxRegionEdit = () => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
|
||||
const { tax_region, isPending, isError, error } = useTaxRegion(id!)
|
||||
|
||||
const ready = !isPending && !!tax_region
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteDrawer>
|
||||
<RouteDrawer.Header>
|
||||
<RouteDrawer.Title asChild>
|
||||
<Heading>{t("taxRegions.taxRates.edit.header")}</Heading>
|
||||
</RouteDrawer.Title>
|
||||
<RouteDrawer.Description className="sr-only">
|
||||
{t("taxRegions.taxRates.edit.hint")}
|
||||
</RouteDrawer.Description>
|
||||
</RouteDrawer.Header>
|
||||
{ready && <TaxRegionEditForm taxRegion={tax_region} />}
|
||||
</RouteDrawer>
|
||||
)
|
||||
}
|
||||
+1
@@ -33,6 +33,7 @@ export const TaxRegionProvinceDetailSection = ({
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
{defaultRates.map((rate) => {
|
||||
return <TaxRateLine key={rate.id} taxRate={rate} isSublevelTaxRate />
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user