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:
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.
|
||||||
|
|||||||
@@ -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[]
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
Reference in new issue
Block a user