From 9cedeb182dc19d6127b602fc06e4b8850490e2a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frane=20Poli=C4=87?= <16856471+fPolic@users.noreply.github.com> Date: Tue, 6 May 2025 19:26:33 +0200 Subject: [PATCH] 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 --- .changeset/hungry-cougars-reflect.md | 9 ++ .../__tests__/order-edits/order-edits.spec.ts | 1 + .../__tests__/product/store/product.spec.ts | 3 + .../modules/__tests__/fixtures/tax/index.ts | 4 + .../dashboard-app/routes/get-route.map.tsx | 5 + .../dashboard/src/hooks/api/tax-regions.tsx | 24 ++++ .../src/i18n/translations/$schema.json | 38 ++++-- .../dashboard/src/i18n/translations/en.json | 8 +- .../tax-region-card/tax-region-card.tsx | 28 ++++- .../tax-region-create-form.tsx | 40 +++++++ .../tax-region-detail/tax-region-detail.tsx | 6 +- .../tax-region-provider-section/index.ts | 1 + .../tax-region-provider-section.tsx | 28 +++++ .../components/tax-region-edit/index.ts | 1 + .../tax-region-edit/tax-region-edit-form.tsx | 109 ++++++++++++++++++ .../tax-regions/tax-region-edit/index.ts | 1 + .../tax-region-edit/tax-region-edit.tsx | 33 ++++++ .../tax-region-detail-section.tsx | 1 + .../src/tax/steps/update-tax-regions.ts | 3 +- .../src/tax/workflows/update-tax-regions.ts | 8 +- packages/core/js-sdk/src/admin/index.ts | 6 + .../core/js-sdk/src/admin/tax-provider.ts | 51 ++++++++ packages/core/js-sdk/src/admin/tax-rate.ts | 42 +++---- packages/core/js-sdk/src/admin/tax-region.ts | 76 ++++++++---- packages/core/types/src/http/index.ts | 1 + .../src/http/tax-provider/admin/entities.ts | 10 ++ .../src/http/tax-provider/admin/index.ts | 3 + .../src/http/tax-provider/admin/queries.ts | 15 +++ .../src/http/tax-provider/admin/responses.ts | 9 ++ .../core/types/src/http/tax-provider/index.ts | 1 + .../src/http/tax-region/admin/entities.ts | 8 +- .../src/http/tax-region/admin/payloads.ts | 22 ++++ packages/core/types/src/tax/common.ts | 5 + packages/core/types/src/tax/mutations.ts | 44 +++++++ packages/core/types/src/tax/service.ts | 20 ++++ .../api/admin/tax-providers/middlewares.ts | 19 +++ .../api/admin/tax-providers/query-config.ts | 11 ++ .../src/api/admin/tax-providers/route.ts | 28 +++++ .../src/api/admin/tax-providers/validators.ts | 22 ++++ .../src/api/admin/tax-regions/[id]/route.ts | 2 +- .../src/api/admin/tax-regions/validators.ts | 2 + packages/medusa/src/api/middlewares.ts | 2 + .../migrate-tax-region-provider.ts | 49 ++++++++ .../utils/setup-tax-structure.ts | 4 + packages/modules/tax/src/loaders/providers.ts | 63 ++++++++-- packages/modules/tax/src/services/index.ts | 1 + .../tax/src/services/tax-module-service.ts | 45 +------- .../modules/tax/src/services/tax-provider.ts | 51 ++++++++ 48 files changed, 847 insertions(+), 116 deletions(-) create mode 100644 .changeset/hungry-cougars-reflect.md create mode 100644 packages/admin/dashboard/src/routes/tax-regions/tax-region-detail/tax-region-provider-section/index.ts create mode 100644 packages/admin/dashboard/src/routes/tax-regions/tax-region-detail/tax-region-provider-section/tax-region-provider-section.tsx create mode 100644 packages/admin/dashboard/src/routes/tax-regions/tax-region-edit/components/tax-region-edit/index.ts create mode 100644 packages/admin/dashboard/src/routes/tax-regions/tax-region-edit/components/tax-region-edit/tax-region-edit-form.tsx create mode 100644 packages/admin/dashboard/src/routes/tax-regions/tax-region-edit/index.ts create mode 100644 packages/admin/dashboard/src/routes/tax-regions/tax-region-edit/tax-region-edit.tsx create mode 100644 packages/core/js-sdk/src/admin/tax-provider.ts create mode 100644 packages/core/types/src/http/tax-provider/admin/entities.ts create mode 100644 packages/core/types/src/http/tax-provider/admin/index.ts create mode 100644 packages/core/types/src/http/tax-provider/admin/queries.ts create mode 100644 packages/core/types/src/http/tax-provider/admin/responses.ts create mode 100644 packages/core/types/src/http/tax-provider/index.ts create mode 100644 packages/medusa/src/api/admin/tax-providers/middlewares.ts create mode 100644 packages/medusa/src/api/admin/tax-providers/query-config.ts create mode 100644 packages/medusa/src/api/admin/tax-providers/route.ts create mode 100644 packages/medusa/src/api/admin/tax-providers/validators.ts create mode 100644 packages/medusa/src/migration-scripts/migrate-tax-region-provider.ts create mode 100644 packages/modules/tax/src/services/tax-provider.ts diff --git a/.changeset/hungry-cougars-reflect.md b/.changeset/hungry-cougars-reflect.md new file mode 100644 index 0000000000..d2b783745e --- /dev/null +++ b/.changeset/hungry-cougars-reflect.md @@ -0,0 +1,9 @@ +--- +"@medusajs/dashboard": patch +"@medusajs/js-sdk": patch +"@medusajs/tax": patch +"@medusajs/types": patch +"@medusajs/medusa": patch +--- + +feat(dashboard, js-sdk, medusa, tax, types): custom tax providers diff --git a/integration-tests/http/__tests__/order-edits/order-edits.spec.ts b/integration-tests/http/__tests__/order-edits/order-edits.spec.ts index 30e5331ff1..2dc87c5163 100644 --- a/integration-tests/http/__tests__/order-edits/order-edits.spec.ts +++ b/integration-tests/http/__tests__/order-edits/order-edits.spec.ts @@ -58,6 +58,7 @@ medusaIntegrationTestRunner({ await api.post( "/admin/tax-regions", { + provider_id: "tp_system_system", country_code: "US", }, adminHeaders diff --git a/integration-tests/http/__tests__/product/store/product.spec.ts b/integration-tests/http/__tests__/product/store/product.spec.ts index f0928b6332..8da7af2ef2 100644 --- a/integration-tests/http/__tests__/product/store/product.spec.ts +++ b/integration-tests/http/__tests__/product/store/product.spec.ts @@ -2380,6 +2380,7 @@ medusaIntegrationTestRunner({ `/admin/tax-regions`, { country_code: "us", + provider_id: "tp_system_system", default_tax_rate: { code: "default", rate: 5, @@ -2393,6 +2394,7 @@ medusaIntegrationTestRunner({ `/admin/tax-regions`, { country_code: "it", + provider_id: "tp_system_system", default_tax_rate: { code: "default", rate: 10, @@ -2406,6 +2408,7 @@ medusaIntegrationTestRunner({ `/admin/tax-regions`, { country_code: "dk", + provider_id: "tp_system_system", default_tax_rate: { code: "default", rate: 20, diff --git a/integration-tests/modules/__tests__/fixtures/tax/index.ts b/integration-tests/modules/__tests__/fixtures/tax/index.ts index 802c5fa156..1faaef0bf3 100644 --- a/integration-tests/modules/__tests__/fixtures/tax/index.ts +++ b/integration-tests/modules/__tests__/fixtures/tax/index.ts @@ -26,10 +26,12 @@ export const setupTaxStructure = async (service: ITaxModuleService) => { const [us, dk, de, ca] = await service.createTaxRegions([ { country_code: "US", + provider_id: "tp_system_system", default_tax_rate: { name: "US Default Rate", rate: 2, code: "US_DEF" }, }, { country_code: "DK", + provider_id: "tp_system_system", default_tax_rate: { name: "Denmark Default Rate", rate: 25, @@ -38,6 +40,7 @@ export const setupTaxStructure = async (service: ITaxModuleService) => { }, { country_code: "DE", + provider_id: "tp_system_system", default_tax_rate: { code: "DE19", name: "Germany Default Rate", @@ -46,6 +49,7 @@ export const setupTaxStructure = async (service: ITaxModuleService) => { }, { country_code: "CA", + provider_id: "tp_system_system", default_tax_rate: { name: "Canada Default Rate", rate: 5, diff --git a/packages/admin/dashboard/src/dashboard-app/routes/get-route.map.tsx b/packages/admin/dashboard/src/dashboard-app/routes/get-route.map.tsx index 32e3daed08..edd9c09b86 100644 --- a/packages/admin/dashboard/src/dashboard-app/routes/get-route.map.tsx +++ b/packages/admin/dashboard/src/dashboard-app/routes/get-route.map.tsx @@ -1626,6 +1626,11 @@ export function getRouteMap({ }, ], }, + { + path: "edit", + lazy: () => + import("../../routes/tax-regions/tax-region-edit"), + }, { path: "provinces/:province_id", lazy: async () => { diff --git a/packages/admin/dashboard/src/hooks/api/tax-regions.tsx b/packages/admin/dashboard/src/hooks/api/tax-regions.tsx index 74eb7a73df..8a7ceff93c 100644 --- a/packages/admin/dashboard/src/hooks/api/tax-regions.tsx +++ b/packages/admin/dashboard/src/hooks/api/tax-regions.tsx @@ -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< diff --git a/packages/admin/dashboard/src/i18n/translations/$schema.json b/packages/admin/dashboard/src/i18n/translations/$schema.json index b2aef80e2b..d6bf8c2075 100644 --- a/packages/admin/dashboard/src/i18n/translations/$schema.json +++ b/packages/admin/dashboard/src/i18n/translations/$schema.json @@ -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", diff --git a/packages/admin/dashboard/src/i18n/translations/en.json b/packages/admin/dashboard/src/i18n/translations/en.json index 077a66045b..d5d6d06c94 100644 --- a/packages/admin/dashboard/src/i18n/translations/en.json +++ b/packages/admin/dashboard/src/i18n/translations/en.json @@ -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", diff --git a/packages/admin/dashboard/src/routes/tax-regions/common/components/tax-region-card/tax-region-card.tsx b/packages/admin/dashboard/src/routes/tax-regions/common/components/tax-region-card/tax-region-card.tsx index b4041e7edf..5546518061 100644 --- a/packages/admin/dashboard/src/routes/tax-regions/common/components/tax-region-card/tax-region-card.tsx +++ b/packages/admin/dashboard/src/routes/tax-regions/common/components/tax-region-card/tax-region-card.tsx @@ -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} ) : ( - {name} +
+ {name} +
)} @@ -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: , + label: t("actions.edit"), + to: `/settings/tax-regions/${taxRegion.id}/edit`, + }, { icon: , label: t("actions.delete"), onClick: handleDelete, }, - ], + ].filter(Boolean) as unknown as Action[], }, ]} /> diff --git a/packages/admin/dashboard/src/routes/tax-regions/tax-region-create/components/tax-region-create-form/tax-region-create-form.tsx b/packages/admin/dashboard/src/routes/tax-regions/tax-region-create/components/tax-region-create-form/tax-region-create-form.tsx index c3f41b5001..22ef01a64e 100644 --- a/packages/admin/dashboard/src/routes/tax-regions/tax-region-create/components/tax-region-create-form/tax-region-create-form.tsx +++ b/packages/admin/dashboard/src/routes/tax-regions/tax-region-create/components/tax-region-create-form/tax-region-create-form.tsx @@ -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>({ 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) => { ) }} /> + ( + + + {t("taxRegions.fields.taxProvider")} + + + + + + + )} + /> diff --git a/packages/admin/dashboard/src/routes/tax-regions/tax-region-detail/tax-region-detail.tsx b/packages/admin/dashboard/src/routes/tax-regions/tax-region-detail/tax-region-detail.tsx index 9809b3cee0..8dc8fde965 100644 --- a/packages/admin/dashboard/src/routes/tax-regions/tax-region-detail/tax-region-detail.tsx +++ b/packages/admin/dashboard/src/routes/tax-regions/tax-region-detail/tax-region-detail.tsx @@ -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 = () => { 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} /> + ) } diff --git a/packages/admin/dashboard/src/routes/tax-regions/tax-region-detail/tax-region-provider-section/index.ts b/packages/admin/dashboard/src/routes/tax-regions/tax-region-detail/tax-region-provider-section/index.ts new file mode 100644 index 0000000000..4e6b80b3ef --- /dev/null +++ b/packages/admin/dashboard/src/routes/tax-regions/tax-region-detail/tax-region-provider-section/index.ts @@ -0,0 +1 @@ +export * from "./tax-region-provider-section" diff --git a/packages/admin/dashboard/src/routes/tax-regions/tax-region-detail/tax-region-provider-section/tax-region-provider-section.tsx b/packages/admin/dashboard/src/routes/tax-regions/tax-region-detail/tax-region-provider-section/tax-region-provider-section.tsx new file mode 100644 index 0000000000..44340e1f5b --- /dev/null +++ b/packages/admin/dashboard/src/routes/tax-regions/tax-region-detail/tax-region-provider-section/tax-region-provider-section.tsx @@ -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 ( + + + {t("taxRegions.provider.header")} + +
+ {taxRegion.provider_id && ( + + {formatProvider(taxRegion.provider_id!)} + + )} +
+
+ ) +} diff --git a/packages/admin/dashboard/src/routes/tax-regions/tax-region-edit/components/tax-region-edit/index.ts b/packages/admin/dashboard/src/routes/tax-regions/tax-region-edit/components/tax-region-edit/index.ts new file mode 100644 index 0000000000..6882e60118 --- /dev/null +++ b/packages/admin/dashboard/src/routes/tax-regions/tax-region-edit/components/tax-region-edit/index.ts @@ -0,0 +1 @@ +export * from "./tax-region-edit-form" diff --git a/packages/admin/dashboard/src/routes/tax-regions/tax-region-edit/components/tax-region-edit/tax-region-edit-form.tsx b/packages/admin/dashboard/src/routes/tax-regions/tax-region-edit/components/tax-region-edit/tax-region-edit-form.tsx new file mode 100644 index 0000000000..f0929322d8 --- /dev/null +++ b/packages/admin/dashboard/src/routes/tax-regions/tax-region-edit/components/tax-region-edit/tax-region-edit-form.tsx @@ -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>({ + 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 ( + + + +
+ ( + + {t("taxRegions.fields.taxProvider")} + + + + + + )} + /> +
+
+ +
+ + + + +
+
+
+
+ ) +} diff --git a/packages/admin/dashboard/src/routes/tax-regions/tax-region-edit/index.ts b/packages/admin/dashboard/src/routes/tax-regions/tax-region-edit/index.ts new file mode 100644 index 0000000000..01a9ee8fdb --- /dev/null +++ b/packages/admin/dashboard/src/routes/tax-regions/tax-region-edit/index.ts @@ -0,0 +1 @@ +export { TaxRegionEdit as Component } from "./tax-region-edit" diff --git a/packages/admin/dashboard/src/routes/tax-regions/tax-region-edit/tax-region-edit.tsx b/packages/admin/dashboard/src/routes/tax-regions/tax-region-edit/tax-region-edit.tsx new file mode 100644 index 0000000000..03cbf32721 --- /dev/null +++ b/packages/admin/dashboard/src/routes/tax-regions/tax-region-edit/tax-region-edit.tsx @@ -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 ( + + + + {t("taxRegions.taxRates.edit.header")} + + + {t("taxRegions.taxRates.edit.hint")} + + + {ready && } + + ) +} diff --git a/packages/admin/dashboard/src/routes/tax-regions/tax-region-province-detail/components/tax-region-province-detail-section/tax-region-detail-section.tsx b/packages/admin/dashboard/src/routes/tax-regions/tax-region-province-detail/components/tax-region-province-detail-section/tax-region-detail-section.tsx index 84a2eb640c..3687132b50 100644 --- a/packages/admin/dashboard/src/routes/tax-regions/tax-region-province-detail/components/tax-region-province-detail-section/tax-region-detail-section.tsx +++ b/packages/admin/dashboard/src/routes/tax-regions/tax-region-province-detail/components/tax-region-province-detail-section/tax-region-detail-section.tsx @@ -33,6 +33,7 @@ export const TaxRegionProvinceDetailSection = ({ ) } /> + {defaultRates.map((rate) => { return })} diff --git a/packages/core/core-flows/src/tax/steps/update-tax-regions.ts b/packages/core/core-flows/src/tax/steps/update-tax-regions.ts index ba56580ca6..aa8c20cf4a 100644 --- a/packages/core/core-flows/src/tax/steps/update-tax-regions.ts +++ b/packages/core/core-flows/src/tax/steps/update-tax-regions.ts @@ -17,7 +17,7 @@ export type UpdateTaxRegionsStepInput = UpdateTaxRegionDTO[] export const updateTaxRegionsStepId = "update-tax-regions" /** * This step updates tax regions. - * + * * @example * const data = updateTaxRegionsStep([ * { @@ -45,6 +45,7 @@ export const updateTaxRegionsStep = createStep( id: d.id, province_code: d.province_code, metadata: d.metadata, + provider_id: d.provider_id, })) ) diff --git a/packages/core/core-flows/src/tax/workflows/update-tax-regions.ts b/packages/core/core-flows/src/tax/workflows/update-tax-regions.ts index 26a0c26239..a234e35184 100644 --- a/packages/core/core-flows/src/tax/workflows/update-tax-regions.ts +++ b/packages/core/core-flows/src/tax/workflows/update-tax-regions.ts @@ -20,10 +20,10 @@ export const updateTaxRegionsWorkflowId = "update-tax-regions" /** * This workflow updates one or more tax regions. It's used by the * [Update Tax Regions Admin API Route](https://docs.medusajs.com/api/admin#tax-regions_posttaxregionsid). - * + * * You can use this workflow within your own customizations or custom workflows, allowing you * to update tax regions in your custom flows. - * + * * @example * const { result } = await updateTaxRegionsWorkflow(container) * .run({ @@ -34,9 +34,9 @@ export const updateTaxRegionsWorkflowId = "update-tax-regions" * } * ] * }) - * + * * @summary - * + * * Update one or more tax regions. */ export const updateTaxRegionsWorkflow = createWorkflow( diff --git a/packages/core/js-sdk/src/admin/index.ts b/packages/core/js-sdk/src/admin/index.ts index d734bbe241..2c573ecfb8 100644 --- a/packages/core/js-sdk/src/admin/index.ts +++ b/packages/core/js-sdk/src/admin/index.ts @@ -37,6 +37,7 @@ import { ShippingOption } from "./shipping-option" import { ShippingProfile } from "./shipping-profile" import { StockLocation } from "./stock-location" import { Store } from "./store" +import { TaxProvider } from "./tax-provider" import { TaxRate } from "./tax-rate" import { TaxRegion } from "./tax-region" import { Upload } from "./upload" @@ -208,6 +209,10 @@ export class Admin { * @tags promotion */ public promotion: Promotion + /** + * @tags tax + */ + public taxProvider: TaxProvider /** * @tags promotion */ @@ -261,5 +266,6 @@ export class Admin { this.promotion = new Promotion(client) this.campaign = new Campaign(client) this.plugin = new Plugin(client) + this.taxProvider = new TaxProvider(client) } } diff --git a/packages/core/js-sdk/src/admin/tax-provider.ts b/packages/core/js-sdk/src/admin/tax-provider.ts new file mode 100644 index 0000000000..5a989f78cf --- /dev/null +++ b/packages/core/js-sdk/src/admin/tax-provider.ts @@ -0,0 +1,51 @@ +import { HttpTypes } from "@medusajs/types" +import { Client } from "../client" +import { ClientHeaders } from "../types" + +const taxProviderUrl = "/admin/tax-providers" + +export class TaxProvider { + /** + * @ignore + */ + private client: Client + /** + * @ignore + */ + constructor(client: Client) { + this.client = client + } + + /** + * This method retrieves a list of tax providers. It sends a request to the + * [List Tax Providers](https://docs.medusajs.com/api/admin#tax-providers_gettaxproviders) + * API route. + * + * @param query - Filters and pagination configurations. + * @param headers - Headers to pass in the request. + * @returns The list of tax providers. + * + * @example + * To retrieve the list of tax providers: + * + * ```ts + * sdk.admin.taxProvider.list() + * .then(({ tax_providers, count, limit, offset }) => { + * console.log(tax_providers) + * }) + * ``` + */ + async list( + query?: HttpTypes.AdminGetTaxProvidersParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + taxProviderUrl, + { + method: "GET", + headers, + query, + } + ) + } +} diff --git a/packages/core/js-sdk/src/admin/tax-rate.ts b/packages/core/js-sdk/src/admin/tax-rate.ts index 0ce2f41f9c..65538f0507 100644 --- a/packages/core/js-sdk/src/admin/tax-rate.ts +++ b/packages/core/js-sdk/src/admin/tax-rate.ts @@ -20,12 +20,12 @@ export class TaxRate { * This method creates a tax rate. It sends a request to the * [Create Tax Rate](https://docs.medusajs.com/api/admin#tax-rates_posttaxrates) * API route. - * + * * @param body - The details of the tax rate to create. * @param query - Configure the fields and relations to retrieve in the tax rate. * @param headers - Headers to pass in the request. * @returns The tax rate's details. - * + * * @example * sdk.admin.taxRate.create({ * name: "VAT", @@ -54,13 +54,13 @@ export class TaxRate { * This method updates a tax rate. It sends a request to the * [Update Tax Rate](https://docs.medusajs.com/api/admin#tax-rates_posttaxratesid) * API route. - * + * * @param id - The ID of the tax rate to update. * @param body - The details of the tax rate to update. * @param query - Configure the fields and relations to retrieve in the tax rate. * @param headers - Headers to pass in the request. * @returns The tax rate's details. - * + * * @example * sdk.admin.taxRate.update("txrat_123", { * name: "VAT", @@ -91,11 +91,11 @@ export class TaxRate { * This method deletes a tax rate. It sends a request to the * [Delete Tax Rate](https://docs.medusajs.com/api/admin#tax-rates_deletetaxratesid) * API route. - * + * * @param id - The ID of the tax rate to delete. * @param headers - Headers to pass in the request. * @returns The deletion's details. - * + * * @example * sdk.admin.taxRate.delete("txrat_123") * .then(({ deleted }) => { @@ -116,24 +116,24 @@ export class TaxRate { * This method retrieves a tax rate. It sends a request to the * [Get Tax Rate](https://docs.medusajs.com/api/admin#tax-rates_gettaxratesid) * API route. - * + * * @param id - The ID of the tax rate to retrieve. * @param query - Configure the fields and relations to retrieve in the tax rate. * @param headers - Headers to pass in the request. * @returns The tax rate's details. - * + * * @example * To retrieve a tax rate by its ID: - * + * * ```ts * sdk.admin.taxRate.retrieve("txrat_123") * .then(({ tax_rate }) => { * console.log(tax_rate) * }) * ``` - * + * * To specify the fields and relations to retrieve: - * + * * ```ts * sdk.admin.taxRate.retrieve("txrat_123", { * fields: "id,*tax_region" @@ -142,7 +142,7 @@ export class TaxRate { * console.log(tax_rate) * }) * ``` - * + * * Learn more about the `fields` property in the [API reference](https://docs.medusajs.com/api/admin#select-fields-and-relations). */ async retrieve( @@ -164,25 +164,25 @@ export class TaxRate { * This method retrieves a list of tax rates. It sends a request to the * [List Tax Rates](https://docs.medusajs.com/api/admin#tax-rates_gettaxrates) * API route. - * + * * @param query - Filters and pagination configurations. * @param headers - Headers to pass in the request. * @returns The list of tax rates. - * + * * @example * To retrieve the list of tax rates: - * + * * ```ts * sdk.admin.taxRate.list() * .then(({ tax_rates, count, limit, offset }) => { * console.log(tax_rates) * }) * ``` - * + * * To configure the pagination, pass the `limit` and `offset` query parameters. - * + * * For example, to retrieve only 10 items and skip 10 items: - * + * * ```ts * sdk.admin.taxRate.list({ * limit: 10, @@ -192,10 +192,10 @@ export class TaxRate { * console.log(tax_rates) * }) * ``` - * + * * Using the `fields` query parameter, you can specify the fields and relations to retrieve * in each tax rate: - * + * * ```ts * sdk.admin.taxRate.list({ * fields: "id,*tax_region" @@ -204,7 +204,7 @@ export class TaxRate { * console.log(tax_rates) * }) * ``` - * + * * Learn more about the `fields` property in the [API reference](https://docs.medusajs.com/api/admin#select-fields-and-relations). */ async list( diff --git a/packages/core/js-sdk/src/admin/tax-region.ts b/packages/core/js-sdk/src/admin/tax-region.ts index f232af3b3b..6b2f65f983 100644 --- a/packages/core/js-sdk/src/admin/tax-region.ts +++ b/packages/core/js-sdk/src/admin/tax-region.ts @@ -6,7 +6,7 @@ const taxRegionUrl = "/admin/tax-regions" /** * @privateRemarks - * + * * TODO: Add support for updating a tax region */ export class TaxRegion { @@ -25,12 +25,12 @@ export class TaxRegion { * This method creates a tax region. It sends a request to the * [Create Tax Region](https://docs.medusajs.com/api/admin#tax-regions_posttaxregions) * API route. - * + * * @param body - The details of the tax region to create. * @param query - Configure the fields and relations to retrieve in the tax region. * @param headers - Headers to pass in the request. * @returns The tax region's details. - * + * * @example * sdk.admin.taxRegion.create({ * country_code: "us", @@ -62,15 +62,51 @@ export class TaxRegion { ) } + /** + * This method updates a tax region. It sends a request to the + * [Update Tax Region](https://docs.medusajs.com/api/admin#tax-regions_posttaxregionsid) + * API route. + * + * @param id - The ID of the tax region to update. + * @param body - The details of the tax region to update. + * @param query - Configure the fields and relations to retrieve in the tax region. + * @param headers - Headers to pass in the request. + * @returns The tax region's details. + * + * @example + * sdk.admin.taxRegion.update("txreg_123", { + * province_code: "ca", + * }) + * .then(({ tax_region }) => { + * console.log(tax_region) + * }) + */ + async update( + id: string, + body: HttpTypes.AdminUpdateTaxRegion, + query?: HttpTypes.SelectParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `${taxRegionUrl}/${id}`, + { + method: "POST", + headers, + body, + query, + } + ) + } + /** * This method deletes a tax region. It sends a request to the * [Delete Tax Region](https://docs.medusajs.com/api/admin#tax-regions_deletetaxregionsid) * API route. - * + * * @param id - The ID of the tax region to delete. * @param headers - Headers to pass in the request. * @returns The deletion's details. - * + * * @example * sdk.admin.taxRegion.delete("txreg_123") * .then(({ deleted }) => { @@ -91,24 +127,24 @@ export class TaxRegion { * This method retrieves a tax region. It sends a request to the * [Get Tax Region](https://docs.medusajs.com/api/admin#tax-regions_gettaxregionsid) * API route. - * + * * @param id - The ID of the tax region to retrieve. * @param query - Configure the fields and relations to retrieve in the tax region. * @param headers - Headers to pass in the request. * @returns The tax region's details. - * + * * @example * To retrieve a tax region by its ID: - * + * * ```ts * sdk.admin.taxRegion.retrieve("txreg_123") * .then(({ tax_region }) => { * console.log(tax_region) * }) * ``` - * + * * To specify the fields and relations to retrieve: - * + * * ```ts * sdk.admin.taxRegion.retrieve("txreg_123", { * fields: "id,*tax_rates" @@ -117,7 +153,7 @@ export class TaxRegion { * console.log(tax_region) * }) * ``` - * + * * Learn more about the `fields` property in the [API reference](https://docs.medusajs.com/api/admin#select-fields-and-relations). */ async retrieve( @@ -139,25 +175,25 @@ export class TaxRegion { * This method retrieves a list of tax regions. It sends a request to the * [List Tax Regions](https://docs.medusajs.com/api/admin#tax-regions_gettaxregions) * API route. - * + * * @param query - Filters and pagination configurations. * @param headers - Headers to pass in the request. * @returns The list of tax regions. - * + * * @example * To retrieve the list of tax regions: - * + * * ```ts * sdk.admin.taxRegion.list() * .then(({ tax_regions, count, limit, offset }) => { * console.log(tax_regions) * }) * ``` - * + * * To configure the pagination, pass the `limit` and `offset` query parameters. - * + * * For example, to retrieve only 10 items and skip 10 items: - * + * * ```ts * sdk.admin.taxRegion.list({ * limit: 10, @@ -167,10 +203,10 @@ export class TaxRegion { * console.log(tax_regions) * }) * ``` - * + * * Using the `fields` query parameter, you can specify the fields and relations to retrieve * in each tax region: - * + * * ```ts * sdk.admin.taxRegion.list({ * fields: "id,*tax_rates" @@ -179,7 +215,7 @@ export class TaxRegion { * console.log(tax_regions) * }) * ``` - * + * * Learn more about the `fields` property in the [API reference](https://docs.medusajs.com/api/admin#select-fields-and-relations). */ async list( diff --git a/packages/core/types/src/http/index.ts b/packages/core/types/src/http/index.ts index 7a1af0bb65..72de197bd7 100644 --- a/packages/core/types/src/http/index.ts +++ b/packages/core/types/src/http/index.ts @@ -39,6 +39,7 @@ export * from "./shipping-option" export * from "./shipping-profile" export * from "./stock-locations" export * from "./store" +export * from "./tax-provider" export * from "./tax-rate" export * from "./tax-region" export * from "./user" diff --git a/packages/core/types/src/http/tax-provider/admin/entities.ts b/packages/core/types/src/http/tax-provider/admin/entities.ts new file mode 100644 index 0000000000..e303b75b18 --- /dev/null +++ b/packages/core/types/src/http/tax-provider/admin/entities.ts @@ -0,0 +1,10 @@ +export interface AdminTaxProvider { + /** + * The ID of the tax provider. + */ + id: string + /** + * Whether the tax provider is enabled. + */ + is_enabled: boolean +} diff --git a/packages/core/types/src/http/tax-provider/admin/index.ts b/packages/core/types/src/http/tax-provider/admin/index.ts new file mode 100644 index 0000000000..020c34f02c --- /dev/null +++ b/packages/core/types/src/http/tax-provider/admin/index.ts @@ -0,0 +1,3 @@ +export * from "./entities" +export * from "./queries" +export * from "./responses" diff --git a/packages/core/types/src/http/tax-provider/admin/queries.ts b/packages/core/types/src/http/tax-provider/admin/queries.ts new file mode 100644 index 0000000000..2e22a7f6ea --- /dev/null +++ b/packages/core/types/src/http/tax-provider/admin/queries.ts @@ -0,0 +1,15 @@ +import { BaseFilterable } from "../../../dal" +import { FindParams } from "../../common" + +export interface AdminGetTaxProvidersParams + extends FindParams, + BaseFilterable { + /** + * Filter by tax provider ID(s). + */ + id?: string | string[] + /** + * Whether the tax provider is enabled. + */ + is_enabled?: boolean +} diff --git a/packages/core/types/src/http/tax-provider/admin/responses.ts b/packages/core/types/src/http/tax-provider/admin/responses.ts new file mode 100644 index 0000000000..ceb35855c0 --- /dev/null +++ b/packages/core/types/src/http/tax-provider/admin/responses.ts @@ -0,0 +1,9 @@ +import { PaginatedResponse } from "../../common" +import { AdminTaxProvider } from "./entities" + +export type AdminTaxProviderListResponse = PaginatedResponse<{ + /** + * The list of tax providers. + */ + tax_providers: AdminTaxProvider[] +}> diff --git a/packages/core/types/src/http/tax-provider/index.ts b/packages/core/types/src/http/tax-provider/index.ts new file mode 100644 index 0000000000..26b8eb9dad --- /dev/null +++ b/packages/core/types/src/http/tax-provider/index.ts @@ -0,0 +1 @@ +export * from "./admin" diff --git a/packages/core/types/src/http/tax-region/admin/entities.ts b/packages/core/types/src/http/tax-region/admin/entities.ts index 75f37a2c4d..53a4fc5e1a 100644 --- a/packages/core/types/src/http/tax-region/admin/entities.ts +++ b/packages/core/types/src/http/tax-region/admin/entities.ts @@ -7,14 +7,14 @@ export interface AdminTaxRegion { id: string /** * The tax region's country code. - * + * * @example * "us" */ country_code: string | null /** * The tax region's lower-case [ISO 3166-2](https://en.wikipedia.org/wiki/ISO_3166-2) province or state code. - * + * * @example * "us-ca" */ @@ -27,6 +27,10 @@ export interface AdminTaxRegion { * The ID of the parent tax region. */ parent_id: string | null + /** + * The ID of the tax provider for the region. + */ + provider_id: string | null /** * The date the tax region was created. */ diff --git a/packages/core/types/src/http/tax-region/admin/payloads.ts b/packages/core/types/src/http/tax-region/admin/payloads.ts index 2295ad3a37..6ee466ba78 100644 --- a/packages/core/types/src/http/tax-region/admin/payloads.ts +++ b/packages/core/types/src/http/tax-region/admin/payloads.ts @@ -3,6 +3,10 @@ export interface AdminCreateTaxRegion { * The country code of the tax region. */ country_code: string + /** + * The ID of the tax provider. + */ + provider_id?: string /** * The lower-case [ISO 3166-2](https://en.wikipedia.org/wiki/ISO_3166-2) province or state code of the tax region. */ @@ -41,3 +45,21 @@ export interface AdminCreateTaxRegion { */ metadata?: Record } + +export interface AdminUpdateTaxRegion { + /** + * The lower-case [ISO 3166-2](https://en.wikipedia.org/wiki/ISO_3166-2) province or state code of the tax region. + */ + province_code?: string + /** + * The ID of the tax provider. + */ + provider_id?: string + /** + * Custom key-value pairs that can be added to the tax region. + */ + /** + * Custom key-value pairs that can be added to the tax region. + */ + metadata?: Record +} diff --git a/packages/core/types/src/tax/common.ts b/packages/core/types/src/tax/common.ts index b7616280a7..609109fd54 100644 --- a/packages/core/types/src/tax/common.ts +++ b/packages/core/types/src/tax/common.ts @@ -163,6 +163,11 @@ export interface TaxRegionDTO { */ parent_id: string | null + /** + * The ID of the tax provider for the region. + */ + provider_id: string | null + /** * Holds custom data in key-value pairs. */ diff --git a/packages/core/types/src/tax/mutations.ts b/packages/core/types/src/tax/mutations.ts index 7b5230ff99..ada920b249 100644 --- a/packages/core/types/src/tax/mutations.ts +++ b/packages/core/types/src/tax/mutations.ts @@ -1,4 +1,7 @@ import { MetadataType } from "../common" +import { BaseFilterable } from "../dal" +import { OperatorMap } from "../dal" +import { TaxProviderDTO } from "./common" /** * The tax rate to be created. @@ -49,6 +52,37 @@ export interface CreateTaxRateDTO { metadata?: MetadataType } +/** + * The tax provider to be created. + */ +export interface CreateTaxProviderDTO { + /** + * The provider's ID. + */ + id: string + + /** + * Whether the provider is enabled. + */ + is_enabled?: boolean +} + +/** + * The filters to apply on the retrieved tax providers. + */ +export interface FilterableTaxProviderProps + extends BaseFilterable { + /** + * The IDs to filter the tax provider by. + */ + id?: string | string[] | OperatorMap + + /** + * Filter by whether the tax provider is enabled. + */ + is_enabled?: boolean +} + /** * The attributes in the tax rate to be created or updated. */ @@ -166,6 +200,11 @@ export interface CreateTaxRegionDTO { */ parent_id?: string | null + /** + * The ID of the tax provider for the region. + */ + provider_id?: string | null + /** * Holds custom data in key-value pairs. */ @@ -220,6 +259,11 @@ export interface UpdateTaxRegionDTO { */ province_code?: string | null + /** + * The ID of the tax provider for the region. + */ + provider_id?: string | null + /** * Holds custom data in key-value pairs. */ diff --git a/packages/core/types/src/tax/service.ts b/packages/core/types/src/tax/service.ts index adecadaf4c..b8669fb152 100644 --- a/packages/core/types/src/tax/service.ts +++ b/packages/core/types/src/tax/service.ts @@ -11,6 +11,7 @@ import { TaxableItemDTO, TaxableShippingDTO, TaxCalculationContext, + TaxProviderDTO, TaxRateDTO, TaxRateRuleDTO, TaxRegionDTO, @@ -19,6 +20,7 @@ import { CreateTaxRateDTO, CreateTaxRateRuleDTO, CreateTaxRegionDTO, + FilterableTaxProviderProps, UpdateTaxRateDTO, UpdateTaxRegionDTO, UpsertTaxRateDTO, @@ -580,6 +582,24 @@ export interface ITaxModuleService extends IModuleService { sharedContext?: Context ): Promise + /** + * This method retrieves a paginated list of tax providers based on optional filters and configuration. + * + * @param {FilterableTaxProviderProps} filters - The filters to apply on the retrieved tax providers. + * @param {FindConfig} config - The configurations determining how the tax provider is retrieved. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The list of tax providers. + * + * @example + * const taxProviders = await taxModuleService.listTaxProviders() + */ + + listTaxProviders( + filters?: FilterableTaxProviderProps, + config?: FindConfig, + sharedContext?: Context + ): Promise + /** * This method creates tax rate rules. * diff --git a/packages/medusa/src/api/admin/tax-providers/middlewares.ts b/packages/medusa/src/api/admin/tax-providers/middlewares.ts new file mode 100644 index 0000000000..5adb2ec37f --- /dev/null +++ b/packages/medusa/src/api/admin/tax-providers/middlewares.ts @@ -0,0 +1,19 @@ +import * as QueryConfig from "./query-config" + +import { validateAndTransformQuery } from "@medusajs/framework" +import { MiddlewareRoute } from "@medusajs/framework/http" + +import { AdminGetTaxProvidersParams } from "./validators" + +export const adminTaxProviderRoutesMiddlewares: MiddlewareRoute[] = [ + { + method: "GET", + matcher: "/admin/tax-providers", + middlewares: [ + validateAndTransformQuery( + AdminGetTaxProvidersParams, + QueryConfig.listTransformQueryConfig + ), + ], + }, +] diff --git a/packages/medusa/src/api/admin/tax-providers/query-config.ts b/packages/medusa/src/api/admin/tax-providers/query-config.ts new file mode 100644 index 0000000000..7c1e78292e --- /dev/null +++ b/packages/medusa/src/api/admin/tax-providers/query-config.ts @@ -0,0 +1,11 @@ +export const defaults = ["id", "is_enabled"] + +export const retrieveTransformQueryConfig = { + defaults, + isList: false, +} + +export const listTransformQueryConfig = { + ...retrieveTransformQueryConfig, + isList: true, +} diff --git a/packages/medusa/src/api/admin/tax-providers/route.ts b/packages/medusa/src/api/admin/tax-providers/route.ts new file mode 100644 index 0000000000..8be4510423 --- /dev/null +++ b/packages/medusa/src/api/admin/tax-providers/route.ts @@ -0,0 +1,28 @@ +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" + +import { HttpTypes } from "@medusajs/framework/types" + +export const GET = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) + + const result = await query.graph({ + entity: "tax_providers", + filters: req.filterableFields, + pagination: req.queryConfig.pagination, + fields: req.queryConfig.fields, + }) + + res.status(200).json({ + tax_providers: result.data, + count: result.metadata?.count ?? 0, + offset: result.metadata?.skip ?? 0, + limit: result.metadata?.take ?? 0, + }) +} diff --git a/packages/medusa/src/api/admin/tax-providers/validators.ts b/packages/medusa/src/api/admin/tax-providers/validators.ts new file mode 100644 index 0000000000..73c4df816d --- /dev/null +++ b/packages/medusa/src/api/admin/tax-providers/validators.ts @@ -0,0 +1,22 @@ +import { z } from "zod" +import { createFindParams } from "../../utils/validators" +import { applyAndAndOrOperators } from "../../utils/common-validators" + +export const AdminGetTaxProvidersParamsFields = z.object({ + id: z.union([z.string(), z.array(z.string())]).optional(), + is_enabled: z.boolean().optional(), +}) + +export type AdminGetTaxProvidersParamsFieldsType = z.infer< + typeof AdminGetTaxProvidersParamsFields +> + +export type AdminGetTaxProvidersParamsType = z.infer< + typeof AdminGetTaxProvidersParams +> +export const AdminGetTaxProvidersParams = createFindParams({ + limit: 20, + offset: 0, +}) + .merge(AdminGetTaxProvidersParamsFields) + .merge(applyAndAndOrOperators(AdminGetTaxProvidersParamsFields)) diff --git a/packages/medusa/src/api/admin/tax-regions/[id]/route.ts b/packages/medusa/src/api/admin/tax-regions/[id]/route.ts index afbad3259e..db66025f50 100644 --- a/packages/medusa/src/api/admin/tax-regions/[id]/route.ts +++ b/packages/medusa/src/api/admin/tax-regions/[id]/route.ts @@ -14,7 +14,7 @@ import { import { AdminUpdateTaxRegionType } from "../validators" export const GET = async ( - req: AuthenticatedMedusaRequest, + req: AuthenticatedMedusaRequest, res: MedusaResponse ) => { const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) diff --git a/packages/medusa/src/api/admin/tax-regions/validators.ts b/packages/medusa/src/api/admin/tax-regions/validators.ts index 49b9314cc9..e704a37ae5 100644 --- a/packages/medusa/src/api/admin/tax-regions/validators.ts +++ b/packages/medusa/src/api/admin/tax-regions/validators.ts @@ -42,6 +42,7 @@ export const AdminGetTaxRegionsParams = createFindParams({ export type AdminCreateTaxRegionType = z.infer export const AdminCreateTaxRegion = z.object({ country_code: z.string(), + provider_id: z.string().nullish(), province_code: z.string().nullish(), parent_id: z.string().nullish(), default_tax_rate: z @@ -59,5 +60,6 @@ export const AdminCreateTaxRegion = z.object({ export type AdminUpdateTaxRegionType = z.infer export const AdminUpdateTaxRegion = z.object({ province_code: z.string().nullish(), + provider_id: z.string().optional(), metadata: z.record(z.unknown()).nullish(), }) diff --git a/packages/medusa/src/api/middlewares.ts b/packages/medusa/src/api/middlewares.ts index 5f53fc39d8..0d9a0b1884 100644 --- a/packages/medusa/src/api/middlewares.ts +++ b/packages/medusa/src/api/middlewares.ts @@ -38,6 +38,7 @@ import { adminStockLocationRoutesMiddlewares } from "./admin/stock-locations/mid import { adminStoreRoutesMiddlewares } from "./admin/stores/middlewares" import { adminTaxRateRoutesMiddlewares } from "./admin/tax-rates/middlewares" import { adminTaxRegionRoutesMiddlewares } from "./admin/tax-regions/middlewares" +import { adminTaxProviderRoutesMiddlewares } from "./admin/tax-providers/middlewares" import { adminUploadRoutesMiddlewares } from "./admin/uploads/middlewares" import { adminUserRoutesMiddlewares } from "./admin/users/middlewares" import { adminWorkflowsExecutionsMiddlewares } from "./admin/workflows-executions/middlewares" @@ -120,6 +121,7 @@ export default defineMiddlewares([ ...adminRefundReasonsRoutesMiddlewares, ...adminExchangeRoutesMiddlewares, ...adminProductVariantRoutesMiddlewares, + ...adminTaxProviderRoutesMiddlewares, ...adminOrderEditRoutesMiddlewares, ...adminPaymentCollectionsMiddlewares, ]) diff --git a/packages/medusa/src/migration-scripts/migrate-tax-region-provider.ts b/packages/medusa/src/migration-scripts/migrate-tax-region-provider.ts new file mode 100644 index 0000000000..5cab4ef9d7 --- /dev/null +++ b/packages/medusa/src/migration-scripts/migrate-tax-region-provider.ts @@ -0,0 +1,49 @@ +import { updateTaxRegionsStep, useQueryGraphStep } from "@medusajs/core-flows" +import { ExecArgs } from "@medusajs/framework/types" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" +import { transform, WorkflowResponse } from "@medusajs/framework/workflows-sdk" +import { createWorkflow } from "@medusajs/framework/workflows-sdk" + +const assignSystemProviderToTaxRegionsWorkflow = createWorkflow( + "assign-system-provider-to-tax-regions", + () => { + const { data: taxRegions } = useQueryGraphStep({ + entity: "tax_region", + fields: ["id", "provider_id", "province_code"], + }) + + const updateData = transform({ taxRegions }, ({ taxRegions }) => { + /** + * Update only parent regions that don't have a provider set. + */ + return taxRegions + .filter( + (taxRegion) => !taxRegion.province_code && !taxRegion.provider_id + ) + .map((taxRegion) => ({ + id: taxRegion.id, + provider_id: "tp_system_system", + })) + }) + + updateTaxRegionsStep(updateData) + + return new WorkflowResponse(void 0) + } +) + +export default async function assignTaxSystemProviderToTaxRegions({ + container, +}: ExecArgs) { + const logger = container.resolve(ContainerRegistrationKeys.LOGGER) + + logger.info("Assigning tax system provider to tax regions") + + try { + await assignSystemProviderToTaxRegionsWorkflow(container).run() + logger.info("System provider assigned to tax regions") + } catch (e) { + logger.error(e) + throw e + } +} diff --git a/packages/modules/tax/integration-tests/utils/setup-tax-structure.ts b/packages/modules/tax/integration-tests/utils/setup-tax-structure.ts index e2ce1ce7a1..8e3ac3d356 100644 --- a/packages/modules/tax/integration-tests/utils/setup-tax-structure.ts +++ b/packages/modules/tax/integration-tests/utils/setup-tax-structure.ts @@ -27,14 +27,17 @@ export const setupTaxStructure = async (service: ITaxModuleService) => { const [us, dk, de, ca] = await service.createTaxRegions([ { country_code: "US", + provider_id: "tp_system_system", default_tax_rate: { name: "US Default Rate", rate: 2, code: "US" }, }, { country_code: "DK", + provider_id: "tp_system_system", default_tax_rate: { name: "Denmark Default Rate", rate: 25, code: "DK" }, }, { country_code: "DE", + provider_id: "tp_system_system", default_tax_rate: { code: "DE19", name: "Germany Default Rate", @@ -43,6 +46,7 @@ export const setupTaxStructure = async (service: ITaxModuleService) => { }, { country_code: "CA", + provider_id: "tp_system_system", default_tax_rate: { name: "Canada Default Rate", rate: 5, code: "CA" }, }, ]) diff --git a/packages/modules/tax/src/loaders/providers.ts b/packages/modules/tax/src/loaders/providers.ts index 9c6268e6d8..4deca59941 100644 --- a/packages/modules/tax/src/loaders/providers.ts +++ b/packages/modules/tax/src/loaders/providers.ts @@ -4,25 +4,35 @@ import { LoaderOptions, ModuleProvider, ModulesSdkTypes, + CreateTaxProviderDTO, } from "@medusajs/framework/types" -import { asFunction, Lifetime } from "awilix" +import { asFunction, asValue, Lifetime } from "awilix" import * as providers from "../providers" +import TaxProviderService from "../services/tax-provider" +import { MedusaError } from "@medusajs/framework/utils" + +const PROVIDER_REGISTRATION_KEY = "tax_providers" as const const registrationFn = async (klass, container, pluginOptions) => { + if (!klass?.identifier) { + throw new MedusaError( + MedusaError.Types.INVALID_ARGUMENT, + `Trying to register a tax provider without a provider identifier.` + ) + } + + const key = `tp_${klass.identifier}${ + pluginOptions.id ? `_${pluginOptions.id}` : "" + }` + container.register({ - [`tp_${klass.identifier}`]: asFunction( - (cradle) => new klass(cradle, pluginOptions), - { lifetime: klass.LIFE_TIME || Lifetime.SINGLETON } - ), + [key]: asFunction((cradle) => new klass(cradle, pluginOptions.options), { + lifetime: klass.LIFE_TIME || Lifetime.SINGLETON, + }), }) - container.registerAdd( - "tax_providers", - asFunction((cradle) => new klass(cradle, pluginOptions), { - lifetime: klass.LIFE_TIME || Lifetime.SINGLETON, - }) - ) + container.registerAdd(PROVIDER_REGISTRATION_KEY, asValue(key)) } export default async ({ @@ -36,7 +46,7 @@ export default async ({ >): Promise => { // Local providers for (const provider of Object.values(providers)) { - await registrationFn(provider, container, {}) + await registrationFn(provider, container, { id: "system" }) } await moduleProviderLoader({ @@ -44,4 +54,33 @@ export default async ({ providers: options?.providers || [], registerServiceFn: registrationFn, }) + + await registerProvidersInDb({ container }) +} + +const registerProvidersInDb = async ({ + container, +}: LoaderOptions): Promise => { + const providersToLoad = container.resolve(PROVIDER_REGISTRATION_KEY) + const taxProviderService = + container.resolve("taxProviderService") + + const existingProviders = await taxProviderService.list( + { id: providersToLoad }, + {} + ) + + const upsertData: CreateTaxProviderDTO[] = [] + + for (const { id } of existingProviders) { + if (!providersToLoad.includes(id)) { + upsertData.push({ id, is_enabled: false }) + } + } + + for (const id of providersToLoad) { + upsertData.push({ id, is_enabled: true }) + } + + await taxProviderService.upsert(upsertData) } diff --git a/packages/modules/tax/src/services/index.ts b/packages/modules/tax/src/services/index.ts index 5f121c8a3b..c58b3548d8 100644 --- a/packages/modules/tax/src/services/index.ts +++ b/packages/modules/tax/src/services/index.ts @@ -1 +1,2 @@ export { default as TaxModuleService } from "./tax-module-service" +export { default as TaxProviderService } from "./tax-provider" diff --git a/packages/modules/tax/src/services/tax-module-service.ts b/packages/modules/tax/src/services/tax-module-service.ts index 48a3c018b7..3c75b356bf 100644 --- a/packages/modules/tax/src/services/tax-module-service.ts +++ b/packages/modules/tax/src/services/tax-module-service.ts @@ -20,13 +20,14 @@ import { promiseAll, } from "@medusajs/framework/utils" import { TaxProvider, TaxRate, TaxRateRule, TaxRegion } from "@models" +import { TaxProviderService } from "@services" type InjectedDependencies = { baseRepository: DAL.RepositoryService taxRateService: ModulesSdkTypes.IMedusaInternalService taxRegionService: ModulesSdkTypes.IMedusaInternalService taxRateRuleService: ModulesSdkTypes.IMedusaInternalService - taxProviderService: ModulesSdkTypes.IMedusaInternalService + taxProviderService: TaxProviderService [key: `tp_${string}`]: ITaxProvider } @@ -57,9 +58,7 @@ export default class TaxModuleService protected taxRateRuleService_: ModulesSdkTypes.IMedusaInternalService< InferEntityType > - protected taxProviderService_: ModulesSdkTypes.IMedusaInternalService< - InferEntityType - > + protected taxProviderService_: TaxProviderService constructor( { @@ -445,7 +444,7 @@ export default class TaxModuleService ) const taxLines = await this.getTaxLinesFromProvider( - parentRegion.provider_id, + parentRegion.provider_id as string, toReturn, calculationContext ) @@ -454,20 +453,11 @@ export default class TaxModuleService } private async getTaxLinesFromProvider( - rawProviderId: string | null, + providerId: string, items: ItemWithRates[], calculationContext: TaxTypes.TaxCalculationContext ) { - const providerId = rawProviderId || "system" - let provider: ITaxProvider - try { - provider = this.container_[`tp_${providerId}`] as ITaxProvider - } catch (err) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `Failed to resolve Tax Provider with id: ${providerId}. Make sure it's installed and configured in the Tax Module's options.` - ) - } + const provider = this.taxProviderService_.retrieveProvider(providerId) const [itemLines, shippingLines] = items.reduce( (acc, line) => { @@ -730,27 +720,4 @@ export default class TaxModuleService private normalizeRegionCodes(code: string) { return code.toLowerCase() } - - // @InjectTransactionManager() - // async createProvidersOnLoad(@MedusaContext() sharedContext: Context = {}) { - // const providersToLoad = this.container_["tax_providers"] as ITaxProvider[] - - // const ids = providersToLoad.map((p) => p.getIdentifier()) - - // const existing = await this.taxProviderService_.update( - // { selector: { id: { $in: ids } }, data: { is_enabled: true } }, - // sharedContext - // ) - - // const existingIds = existing.map((p) => p.id) - // const diff = arrayDifference(ids, existingIds) - // await this.taxProviderService_.create( - // diff.map((id) => ({ id, is_enabled: true })) - // ) - - // await this.taxProviderService_.update({ - // selector: { id: { $nin: ids } }, - // data: { is_enabled: false }, - // }) - // } } diff --git a/packages/modules/tax/src/services/tax-provider.ts b/packages/modules/tax/src/services/tax-provider.ts new file mode 100644 index 0000000000..51deb0624a --- /dev/null +++ b/packages/modules/tax/src/services/tax-provider.ts @@ -0,0 +1,51 @@ +import { DAL, ITaxProvider, Logger, TaxTypes } from "@medusajs/framework/types" +import { ModulesSdkUtils } from "@medusajs/framework/utils" + +import TaxProvider from "../models/tax-provider" + +type InjectedDependencies = { + logger?: Logger + taxProviderRepository: DAL.RepositoryService + [key: `tp_${string}`]: ITaxProvider +} + +export default class TaxProviderService extends ModulesSdkUtils.MedusaInternalService( + TaxProvider +) { + #logger: Logger + + constructor(container: InjectedDependencies) { + super(container) + this.#logger = container["logger"] + ? container.logger + : (console as unknown as Logger) + } + + retrieveProvider(providerId: string): ITaxProvider { + try { + return this.__container__[providerId] as ITaxProvider + } catch (err) { + if (err.name === "AwilixResolutionError") { + const errMessage = ` + Unable to retrieve the tax provider with id: ${providerId} + Please make sure that the provider is registered in the container and it is configured correctly in your project configuration file.` + throw new Error(errMessage) + } + + const errMessage = `Unable to retrieve the tax provider with id: ${providerId}, the following error occurred: ${err.message}` + this.#logger.error(errMessage) + + throw new Error(errMessage) + } + } + + async getTaxLines( + providerId: string, + itemLines: TaxTypes.ItemTaxCalculationLine[], + shippingLines: TaxTypes.ShippingTaxCalculationLine[], + context: TaxTypes.TaxCalculationContext + ): Promise<(TaxTypes.ItemTaxLineDTO | TaxTypes.ShippingTaxLineDTO)[]> { + const provider = this.retrieveProvider(providerId) + return provider.getTaxLines(itemLines, shippingLines, context) + } +}