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:
Frane Polić
2025-05-06 19:26:33 +02:00
committed by GitHub
parent 405ee7f7f3
commit 9cedeb182d
48 changed files with 847 additions and 116 deletions
@@ -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" },
},
])
+51 -12
View File
@@ -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<void> => {
// 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<void> => {
const providersToLoad = container.resolve<string[]>(PROVIDER_REGISTRATION_KEY)
const taxProviderService =
container.resolve<TaxProviderService>("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)
}
@@ -1 +1,2 @@
export { default as TaxModuleService } from "./tax-module-service"
export { default as TaxProviderService } from "./tax-provider"
@@ -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<any>
taxRegionService: ModulesSdkTypes.IMedusaInternalService<any>
taxRateRuleService: ModulesSdkTypes.IMedusaInternalService<any>
taxProviderService: ModulesSdkTypes.IMedusaInternalService<any>
taxProviderService: TaxProviderService
[key: `tp_${string}`]: ITaxProvider
}
@@ -57,9 +58,7 @@ export default class TaxModuleService
protected taxRateRuleService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof TaxRateRule>
>
protected taxProviderService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof TaxProvider>
>
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 },
// })
// }
}
@@ -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<InjectedDependencies>(
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)
}
}