feat(tax): singular creates and deletes of regions, rates, rules (#6464)

**What**
- Adds support for creating single rates, regions, rules.
- Adds delete methods.
This commit is contained in:
Sebastian Rindom authored and GitHub committed 2024-02-25 21:38:41 +00:00
1 parent 168f02f138
commit 3c5b020c5e
4 files changed
+269 -48

No files matched your search

@@ -9,15 +9,13 @@ moduleIntegrationTestRunner({
testSuite: ({ service }: SuiteOptions<ITaxModuleService>) => { testSuite: ({ service }: SuiteOptions<ITaxModuleService>) => {
describe("TaxModuleService", function () { describe("TaxModuleService", function () {
it("should create a tax region", async () => { it("should create a tax region", async () => {
const [region] = await service.createTaxRegions([ const region = await service.createTaxRegions({
{ country_code: "US",
country_code: "US", default_tax_rate: {
default_tax_rate: { name: "Test Rate",
name: "Test Rate", rate: 0.2,
rate: 0.2,
},
}, },
]) })
const [provinceRegion] = await service.createTaxRegions([ const [provinceRegion] = await service.createTaxRegions([
{ {
@@ -298,11 +296,106 @@ moduleIntegrationTestRunner({
}), }),
]) ])
}) })
it("should delete tax rate", async () => {
const region = await service.createTaxRegions({
country_code: "US",
})
const taxRate = await service.create({
tax_region_id: region.id,
value: 10,
code: "test",
name: "test",
})
await service.delete(taxRate.id)
const rates = await service.list({ tax_region_id: region.id })
expect(rates).toEqual([])
})
it("should delete a tax region and its rates", async () => {
const region = await service.createTaxRegions({
country_code: "US",
default_tax_rate: {
value: 2,
code: "test",
name: "default test",
},
})
await service.create({
tax_region_id: region.id,
value: 10,
code: "test",
name: "test",
})
await service.deleteTaxRegions(region.id)
const taxRegions = await service.listTaxRegions()
const rates = await service.list()
expect(taxRegions).toEqual([])
expect(rates).toEqual([])
})
it("should delete a tax rate and its rules", async () => {
const region = await service.createTaxRegions({
country_code: "US",
})
const rate = await service.create({
tax_region_id: region.id,
value: 10,
code: "test",
name: "test",
rules: [
{ reference: "product", reference_id: "product_id_1" },
{ reference: "product_type", reference_id: "product_type_id" },
],
})
await service.delete(rate.id)
const taxRegions = await service.listTaxRegions()
const rates = await service.list()
const rules = await service.listTaxRateRules()
expect(taxRegions).toEqual([expect.objectContaining({ id: region.id })])
expect(rates).toEqual([])
expect(rules).toEqual([])
})
it("should fail to create province region belonging to a parent with non-matching country", async () => {
const caRegion = await service.createTaxRegions({
country_code: "CA",
})
await expect(
service.createTaxRegions({
country_code: "US", // This should be CA
parent_id: caRegion.id,
province_code: "QC",
})
).rejects.toThrowError()
})
it("should fail to create region with non-existing parent", async () => {
await expect(
service.createTaxRegions({
parent_id: "something random",
country_code: "CA",
province_code: "QC",
})
).rejects.toThrowError()
})
}) })
}, },
}) })
const setupTaxStructure = async (service) => { const setupTaxStructure = async (service: ITaxModuleService) => {
// Setup for this specific test // Setup for this specific test
// //
// Using the following structure to setup tests. // Using the following structure to setup tests.
+142 -39
View File
@@ -11,7 +11,9 @@ import {
InjectManager, InjectManager,
InjectTransactionManager, InjectTransactionManager,
MedusaContext, MedusaContext,
MedusaError,
ModulesSdkUtils, ModulesSdkUtils,
isDefined,
promiseAll, promiseAll,
} from "@medusajs/utils" } from "@medusajs/utils"
import { TaxRate, TaxRegion, TaxRateRule } from "@models" import { TaxRate, TaxRegion, TaxRateRule } from "@models"
@@ -86,11 +88,7 @@ export default class TaxModuleService<
): Promise<TaxTypes.TaxRateDTO[] | TaxTypes.TaxRateDTO> { ): Promise<TaxTypes.TaxRateDTO[] | TaxTypes.TaxRateDTO> {
const input = Array.isArray(data) ? data : [data] const input = Array.isArray(data) ? data : [data]
const rates = await this.create_(input, sharedContext) const rates = await this.create_(input, sharedContext)
const result = await this.baseRepository_.serialize<TaxTypes.TaxRateDTO>( return Array.isArray(data) ? rates : rates[0]
rates,
{ populate: true }
)
return Array.isArray(data) ? result : result[0]
} }
@InjectTransactionManager("baseRepository_") @InjectTransactionManager("baseRepository_")
@@ -98,28 +96,80 @@ export default class TaxModuleService<
data: TaxTypes.CreateTaxRateDTO[], data: TaxTypes.CreateTaxRateDTO[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
) { ) {
return await this.taxRateService_.create(data, sharedContext) const [rules, rateData] = data.reduce(
}
@InjectManager("baseRepository_")
async createTaxRegions(
data: TaxTypes.CreateTaxRegionDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TaxTypes.TaxRegionDTO[]> {
// TODO: check that country_code === parent.country_code
const [defaultRates, regionData] = data.reduce(
(acc, region) => { (acc, region) => {
const { default_tax_rate, ...rest } = region const { rules, ...rest } = region
acc[0].push({ acc[0].push(rules)
...default_tax_rate,
is_default: true,
created_by: region.created_by,
})
acc[1].push(rest) acc[1].push(rest)
return acc return acc
}, },
[[], []] as [ [[], []] as [
Omit<TaxTypes.CreateTaxRateDTO, "tax_region_id">[], (Omit<TaxTypes.CreateTaxRateRuleDTO, "tax_rate_id">[] | undefined)[],
Partial<TaxTypes.CreateTaxRegionDTO>[]
]
)
const rates = await this.taxRateService_.create(rateData, sharedContext)
const rulesToCreate = rates
.reduce((acc, rate, i) => {
const rateRules = rules[i]
if (isDefined(rateRules)) {
acc.push(
rateRules.map((r) => {
return {
...r,
tax_rate_id: rate.id,
}
})
)
}
return acc
}, [] as TaxTypes.CreateTaxRateRuleDTO[][])
.flat()
if (rulesToCreate.length > 0) {
await this.taxRateRuleService_.create(rulesToCreate, sharedContext)
}
return await this.baseRepository_.serialize<TaxTypes.TaxRateDTO>(rates, {
populate: true,
})
}
createTaxRegions(
data: TaxTypes.CreateTaxRegionDTO,
sharedContext?: Context
): Promise<TaxRegionDTO>
createTaxRegions(
data: TaxTypes.CreateTaxRegionDTO[],
sharedContext?: Context
): Promise<TaxRegionDTO[]>
@InjectManager("baseRepository_")
async createTaxRegions(
data: TaxTypes.CreateTaxRegionDTO | TaxTypes.CreateTaxRegionDTO[],
@MedusaContext() sharedContext: Context = {}
) {
const input = Array.isArray(data) ? data : [data]
await this.verifyProvinceToCountryMatch(input, sharedContext)
const [defaultRates, regionData] = input.reduce(
(acc, region) => {
const { default_tax_rate, ...rest } = region
if (!default_tax_rate) {
acc[0].push(null)
} else {
acc[0].push({
...default_tax_rate,
is_default: true,
created_by: region.created_by,
})
}
acc[1].push(rest)
return acc
},
[[], []] as [
(Omit<TaxTypes.CreateTaxRateDTO, "tax_region_id"> | null)[],
Partial<TaxTypes.CreateTaxRegionDTO>[] Partial<TaxTypes.CreateTaxRegionDTO>[]
] ]
) )
@@ -129,35 +179,53 @@ export default class TaxModuleService<
sharedContext sharedContext
) )
const rates = regions.map((region: TaxRegionDTO, i: number) => { const rates = regions
return { .map((region, i) => {
...defaultRates[i], if (!defaultRates[i]) {
tax_region_id: region.id, return false
} }
return {
...defaultRates[i],
tax_region_id: region.id,
}
})
.filter(Boolean) as TaxTypes.CreateTaxRateDTO[]
if (rates.length !== 0) {
await this.create(rates, sharedContext)
}
const result = await this.baseRepository_.serialize<
TaxTypes.TaxRegionDTO[]
>(regions, {
populate: true,
}) })
await this.create(rates, sharedContext) return Array.isArray(data) ? result : result[0]
return await this.baseRepository_.serialize<TaxTypes.TaxRegionDTO[]>(
regions,
{
populate: true,
}
)
} }
createTaxRateRules(
data: TaxTypes.CreateTaxRateRuleDTO,
sharedContext?: Context
): Promise<TaxTypes.TaxRateRuleDTO>
createTaxRateRules(
data: TaxTypes.CreateTaxRateRuleDTO[],
sharedContext?: Context
): Promise<TaxTypes.TaxRateRuleDTO[]>
@InjectManager("baseRepository_") @InjectManager("baseRepository_")
async createTaxRateRules( async createTaxRateRules(
data: TaxTypes.CreateTaxRateRuleDTO[], data: TaxTypes.CreateTaxRateRuleDTO | TaxTypes.CreateTaxRateRuleDTO[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<TaxTypes.TaxRateRuleDTO[]> { ) {
const rules = await this.taxRateRuleService_.create(data, sharedContext) const input = Array.isArray(data) ? data : [data]
const rules = await this.taxRateRuleService_.create(input, sharedContext)
const result = await this.baseRepository_.serialize< const result = await this.baseRepository_.serialize<
TaxTypes.TaxRateRuleDTO[] TaxTypes.TaxRateRuleDTO[]
>(rules, { >(rules, {
populate: true, populate: true,
}) })
return result return Array.isArray(data) ? result : result[0]
} }
@InjectTransactionManager("baseRepository_") @InjectTransactionManager("baseRepository_")
@@ -210,6 +278,41 @@ export default class TaxModuleService<
return toReturn.flat() return toReturn.flat()
} }
private async verifyProvinceToCountryMatch(
regionsToVerify: TaxTypes.CreateTaxRegionDTO[],
sharedContext: Context = {}
) {
const parentIds = regionsToVerify.map((i) => i.parent_id).filter(isDefined)
if (parentIds.length > 0) {
const parentRegions = await this.taxRegionService_.list(
{ id: { $in: parentIds } },
{ select: ["id", "country_code"] },
sharedContext
)
for (const region of regionsToVerify) {
if (isDefined(region.parent_id)) {
const parentRegion = parentRegions.find(
(r) => r.id === region.parent_id
)
if (!isDefined(parentRegion)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Province region must belong to a parent region. You are trying to create a province region with (country: ${region.country_code}, province: ${region.province_code}) but parent does not exist`
)
}
if (parentRegion.country_code !== region.country_code) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Province region must belong to a parent region with the same country code. You are trying to create a province region with (country: ${region.country_code}, province: ${region.province_code}) but parent expects (country: ${parentRegion.country_code})`
)
}
}
}
}
}
private async getTaxRatesForItem( private async getTaxRatesForItem(
item: TaxTypes.TaxableItemDTO | TaxTypes.TaxableShippingDTO, item: TaxTypes.TaxableItemDTO | TaxTypes.TaxableShippingDTO,
rates: TTaxRate[] rates: TTaxRate[]
+1
View File
@@ -3,6 +3,7 @@ export interface CreateTaxRateDTO {
rate?: number | null rate?: number | null
code?: string | null code?: string | null
name: string name: string
rules?: Omit<CreateTaxRateRuleDTO, "tax_rate_id">[]
is_default?: boolean is_default?: boolean
created_by?: string created_by?: string
metadata?: Record<string, unknown> metadata?: Record<string, unknown>
+24
View File
@@ -1,4 +1,5 @@
import { FindConfig } from "../common" import { FindConfig } from "../common"
import { SoftDeleteReturn } from "../dal"
import { IModuleService } from "../modules-sdk" import { IModuleService } from "../modules-sdk"
import { Context } from "../shared-context" import { Context } from "../shared-context"
import { import {
@@ -48,22 +49,45 @@ export interface ITaxModuleService extends IModuleService {
delete(taxRateIds: string[], sharedContext?: Context): Promise<void> delete(taxRateIds: string[], sharedContext?: Context): Promise<void>
delete(taxRateId: string, sharedContext?: Context): Promise<void> delete(taxRateId: string, sharedContext?: Context): Promise<void>
createTaxRegions(
data: CreateTaxRegionDTO,
sharedContext?: Context
): Promise<TaxRegionDTO>
createTaxRegions( createTaxRegions(
data: CreateTaxRegionDTO[], data: CreateTaxRegionDTO[],
sharedContext?: Context sharedContext?: Context
): Promise<TaxRegionDTO[]> ): Promise<TaxRegionDTO[]>
deleteTaxRegions(
taxRegionIds: string[],
sharedContext?: Context
): Promise<void>
deleteTaxRegions(taxRegionId: string, sharedContext?: Context): Promise<void>
listTaxRegions( listTaxRegions(
filters?: FilterableTaxRegionProps, filters?: FilterableTaxRegionProps,
config?: FindConfig<TaxRegionDTO>, config?: FindConfig<TaxRegionDTO>,
sharedContext?: Context sharedContext?: Context
): Promise<TaxRegionDTO[]> ): Promise<TaxRegionDTO[]>
createTaxRateRules(
data: CreateTaxRateRuleDTO,
sharedContext?: Context
): Promise<TaxRateRuleDTO>
createTaxRateRules( createTaxRateRules(
data: CreateTaxRateRuleDTO[], data: CreateTaxRateRuleDTO[],
sharedContext?: Context sharedContext?: Context
): Promise<TaxRateRuleDTO[]> ): Promise<TaxRateRuleDTO[]>
deleteTaxRateRules(
taxRateRulePair: { tax_rate_id: string; reference_id: string },
sharedContext?: Context
): Promise<void>
deleteTaxRateRules(
taxRateRulePair: { tax_rate_id: string; reference_id: string }[],
sharedContext?: Context
): Promise<void>
listTaxRateRules( listTaxRateRules(
filters?: FilterableTaxRateRuleProps, filters?: FilterableTaxRateRuleProps,
config?: FindConfig<TaxRateRuleDTO>, config?: FindConfig<TaxRateRuleDTO>,