feat(tax): soft deletes (#6486)

**What**
- Adds soft deletes
This commit is contained in:
Sebastian Rindom
2024-02-26 08:34:46 +00:00
committed by GitHub
parent 3b18f399c1
commit d983329481
6 changed files with 387 additions and 31 deletions
@@ -316,6 +316,33 @@ moduleIntegrationTestRunner({
expect(rates).toEqual([]) expect(rates).toEqual([])
}) })
it("should soft 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.softDelete([taxRate.id])
const rates = await service.list(
{ tax_region_id: region.id },
{ withDeleted: true }
)
expect(rates).toEqual([
expect.objectContaining({
id: taxRate.id,
deleted_at: expect.any(Date),
}),
])
})
it("should delete a tax region and its rates", async () => { it("should delete a tax region and its rates", async () => {
const region = await service.createTaxRegions({ const region = await service.createTaxRegions({
country_code: "US", country_code: "US",
@@ -342,6 +369,47 @@ moduleIntegrationTestRunner({
expect(rates).toEqual([]) expect(rates).toEqual([])
}) })
it("should soft 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.softDeleteTaxRegions([region.id])
const taxRegions = await service.listTaxRegions(
{},
{ withDeleted: true }
)
const rates = await service.list({}, { withDeleted: true })
expect(taxRegions).toEqual([
expect.objectContaining({
id: region.id,
deleted_at: expect.any(Date),
}),
])
expect(rates).toEqual([
expect.objectContaining({
deleted_at: expect.any(Date),
}),
expect.objectContaining({
deleted_at: expect.any(Date),
}),
])
})
it("should delete a tax rate and its rules", async () => { it("should delete a tax rate and its rules", async () => {
const region = await service.createTaxRegions({ const region = await service.createTaxRegions({
country_code: "US", country_code: "US",
@@ -369,6 +437,130 @@ moduleIntegrationTestRunner({
expect(rules).toEqual([]) expect(rules).toEqual([])
}) })
it("should soft 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.softDelete(rate.id)
const taxRegions = await service.listTaxRegions(
{},
{ withDeleted: true }
)
const rates = await service.list({}, { withDeleted: true })
const rules = await service.listTaxRateRules({}, { withDeleted: true })
expect(taxRegions).toEqual([
expect.objectContaining({ id: region.id, deleted_at: null }),
])
expect(rates).toEqual([
expect.objectContaining({
id: rate.id,
deleted_at: expect.any(Date),
}),
])
expect(rules).toEqual([
expect.objectContaining({
tax_rate_id: rate.id,
deleted_at: expect.any(Date),
}),
expect.objectContaining({
tax_rate_id: rate.id,
deleted_at: expect.any(Date),
}),
])
})
it("should soft delete a tax rule", 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",
})
const [ruleOne, ruleTwo] = await service.createTaxRateRules([
{
tax_rate_id: rate.id,
reference: "product",
reference_id: "product_id_1",
},
{
tax_rate_id: rate.id,
reference: "product_type",
reference_id: "product_type_id",
},
])
await service.softDeleteTaxRateRules([ruleOne.id])
const rules = await service.listTaxRateRules({}, { withDeleted: true })
expect(rules).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: ruleOne.id,
deleted_at: expect.any(Date),
}),
expect.objectContaining({
id: ruleTwo.id,
deleted_at: null,
}),
])
)
const rateWithRules = await service.retrieve(rate.id, {
relations: ["rules"],
})
expect(rateWithRules.rules.length).toBe(1)
// should be possible to add the rule back again
await service.createTaxRateRules({
tax_rate_id: rate.id,
reference: ruleOne.reference,
reference_id: ruleOne.reference_id,
})
const rateWithRulesAfterReAdd = await service.retrieve(rate.id, {
relations: ["rules"],
})
expect(rateWithRulesAfterReAdd.rules.length).toBe(2)
})
it("should fail on duplicate rules", async () => {
const region = await service.createTaxRegions({
country_code: "US",
})
await expect(
service.create({
tax_region_id: region.id,
value: 10,
code: "test",
name: "test",
rules: [
{ reference: "product", reference_id: "product_id_1" },
{ reference: "product", reference_id: "product_id_1" },
],
})
).rejects.toThrowError()
})
it("should fail to create province region belonging to a parent with non-matching country", async () => { it("should fail to create province region belonging to a parent with non-matching country", async () => {
const caRegion = await service.createTaxRegions({ const caRegion = await service.createTaxRegions({
country_code: "CA", country_code: "CA",
@@ -391,6 +583,52 @@ moduleIntegrationTestRunner({
}) })
).rejects.toThrowError() ).rejects.toThrowError()
}) })
it("should delete all child regions when parent region is deleted", async () => {
const region = await service.createTaxRegions({
country_code: "CA",
})
const provinceRegion = await service.createTaxRegions({
parent_id: region.id,
country_code: "CA",
province_code: "QC",
})
await service.deleteTaxRegions(region.id)
const taxRegions = await service.listTaxRegions({
id: provinceRegion.id,
})
expect(taxRegions).toEqual([])
})
it("it should soft delete all child regions when parent region is deleted", async () => {
const region = await service.createTaxRegions({
country_code: "CA",
})
const provinceRegion = await service.createTaxRegions({
parent_id: region.id,
country_code: "CA",
province_code: "QC",
})
await service.softDeleteTaxRegions([region.id])
const taxRegions = await service.listTaxRegions(
{
id: provinceRegion.id,
},
{ withDeleted: true }
)
expect(taxRegions).toEqual([
expect.objectContaining({
id: provinceRegion.id,
deleted_at: expect.any(Date),
}),
])
})
}) })
}, },
}) })
+1 -1
View File
@@ -9,7 +9,7 @@ module.exports = {
"^.+\\.[jt]s?$": [ "^.+\\.[jt]s?$": [
"ts-jest", "ts-jest",
{ {
tsConfig: "tsconfig.spec.json", tsconfig: "tsconfig.spec.json",
isolatedModules: true, isolatedModules: true,
}, },
], ],
+70 -10
View File
@@ -1,35 +1,77 @@
import { DAL } from "@medusajs/types"
import {
DALUtils,
createPsqlIndexStatementHelper,
generateEntityId,
} from "@medusajs/utils"
import { import {
Cascade, Cascade,
Entity, Entity,
ManyToOne, ManyToOne,
PrimaryKey, PrimaryKey,
PrimaryKeyProp,
Property, Property,
Filter,
OptionalProps,
BeforeCreate,
OnInit,
} from "@mikro-orm/core" } from "@mikro-orm/core"
import TaxRate from "./tax-rate" import TaxRate from "./tax-rate"
const TABLE_NAME = "tax_rate_rule" const TABLE_NAME = "tax_rate_rule"
type OptionalRuleProps = DAL.SoftDeletableEntityDateColumns
const taxRateIdIndexName = "IDX_tax_rate_rule_tax_rate_id" const taxRateIdIndexName = "IDX_tax_rate_rule_tax_rate_id"
const taxRateIdIndexStatement = createPsqlIndexStatementHelper({
name: taxRateIdIndexName,
tableName: TABLE_NAME,
columns: "tax_rate_id",
where: "deleted_at IS NULL",
})
const referenceIdIndexName = "IDX_tax_rate_rule_reference_id"
const referenceIdIndexStatement = createPsqlIndexStatementHelper({
name: referenceIdIndexName,
tableName: TABLE_NAME,
columns: "reference_id",
where: "deleted_at IS NULL",
})
const uniqueRateReferenceIndexName = "IDX_tax_rate_rule_unique_rate_reference"
const uniqueRateReferenceIndexStatement = createPsqlIndexStatementHelper({
name: uniqueRateReferenceIndexName,
tableName: TABLE_NAME,
columns: ["tax_rate_id", "reference_id"],
unique: true,
where: "deleted_at IS NULL",
})
@Entity({ tableName: TABLE_NAME }) @Entity({ tableName: TABLE_NAME })
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
@uniqueRateReferenceIndexStatement.MikroORMIndex()
export default class TaxRateRule { export default class TaxRateRule {
@PrimaryKey({ columnType: "text" }) [OptionalProps]?: OptionalRuleProps
tax_rate_id!: string
@PrimaryKey({ columnType: "text" }) @PrimaryKey({ columnType: "text" })
reference_id!: string; id!: string
[PrimaryKeyProp]?: ["tax_rate_id", "reference_id"] @ManyToOne(() => TaxRate, {
type: "text",
fieldName: "tax_rate_id",
mapToPk: true,
cascade: [Cascade.REMOVE],
})
@taxRateIdIndexStatement.MikroORMIndex()
tax_rate_id: string
@Property({ columnType: "text" })
@referenceIdIndexStatement.MikroORMIndex()
reference_id: string
@Property({ columnType: "text" }) @Property({ columnType: "text" })
reference: string reference: string
@ManyToOne(() => TaxRate, { @ManyToOne(() => TaxRate, { persist: false })
fieldName: "tax_rate_id",
index: taxRateIdIndexName,
cascade: [Cascade.REMOVE, Cascade.PERSIST],
})
tax_rate: TaxRate tax_rate: TaxRate
@Property({ columnType: "jsonb", nullable: true }) @Property({ columnType: "jsonb", nullable: true })
@@ -52,4 +94,22 @@ export default class TaxRateRule {
@Property({ columnType: "text", nullable: true }) @Property({ columnType: "text", nullable: true })
created_by: string | null = null created_by: string | null = null
@createPsqlIndexStatementHelper({
tableName: TABLE_NAME,
columns: "deleted_at",
where: "deleted_at IS NOT NULL",
}).MikroORMIndex()
@Property({ columnType: "timestamptz", nullable: true })
deleted_at: Date | null = null
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, "txr")
}
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "txr")
}
} }
+33 -12
View File
@@ -1,9 +1,11 @@
import { DAL } from "@medusajs/types" import { DAL } from "@medusajs/types"
import { import {
DALUtils,
createPsqlIndexStatementHelper, createPsqlIndexStatementHelper,
generateEntityId, generateEntityId,
} from "@medusajs/utils" } from "@medusajs/utils"
import { import {
Filter,
BeforeCreate, BeforeCreate,
Cascade, Cascade,
Collection, Collection,
@@ -18,25 +20,32 @@ import {
import TaxRegion from "./tax-region" import TaxRegion from "./tax-region"
import TaxRateRule from "./tax-rate-rule" import TaxRateRule from "./tax-rate-rule"
type OptionalTaxRateProps = DAL.EntityDateColumns type OptionalTaxRateProps = DAL.SoftDeletableEntityDateColumns
const TABLE_NAME = "tax_rate" const TABLE_NAME = "tax_rate"
const taxRegionIdIndexName = "IDX_tax_rate_tax_region_id"
const singleDefaultRegionIndexName = "IDX_single_default_region" const singleDefaultRegionIndexName = "IDX_single_default_region"
const singleDefaultRegionIndexStatement = createPsqlIndexStatementHelper({ const singleDefaultRegionIndexStatement = createPsqlIndexStatementHelper({
name: singleDefaultRegionIndexName, name: singleDefaultRegionIndexName,
tableName: TABLE_NAME, tableName: TABLE_NAME,
columns: "tax_region_id", columns: "tax_region_id",
unique: true, unique: true,
where: "is_default = true", where: "is_default = true AND deleted_at IS NULL",
})
const taxRegionIdIndexName = "IDX_tax_rate_tax_region_id"
const taxRegionIdIndexStatement = createPsqlIndexStatementHelper({
name: taxRegionIdIndexName,
tableName: TABLE_NAME,
columns: "tax_region_id",
where: "deleted_at IS NULL",
}) })
@singleDefaultRegionIndexStatement.MikroORMIndex() @singleDefaultRegionIndexStatement.MikroORMIndex()
@Entity({ tableName: TABLE_NAME }) @Entity({ tableName: TABLE_NAME })
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
export default class TaxRate { export default class TaxRate {
[OptionalProps]: OptionalTaxRateProps [OptionalProps]?: OptionalTaxRateProps
@PrimaryKey({ columnType: "text" }) @PrimaryKey({ columnType: "text" })
id!: string id!: string
@@ -56,17 +65,21 @@ export default class TaxRate {
@Property({ columnType: "bool", default: false }) @Property({ columnType: "bool", default: false })
is_combinable = false is_combinable = false
@Property({ columnType: "text" }) @ManyToOne(() => TaxRegion, {
type: "text",
fieldName: "tax_region_id",
mapToPk: true,
cascade: [Cascade.REMOVE],
})
@taxRegionIdIndexStatement.MikroORMIndex()
tax_region_id: string tax_region_id: string
@ManyToOne(() => TaxRegion, { @ManyToOne({ entity: () => TaxRegion, persist: false })
fieldName: "tax_region_id",
index: taxRegionIdIndexName,
cascade: [Cascade.REMOVE, Cascade.PERSIST],
})
tax_region: TaxRegion tax_region: TaxRegion
@OneToMany(() => TaxRateRule, (rule) => rule.tax_rate) @OneToMany(() => TaxRateRule, (rule) => rule.tax_rate, {
cascade: ["soft-remove" as Cascade],
})
rules = new Collection<TaxRateRule>(this) rules = new Collection<TaxRateRule>(this)
@Property({ columnType: "jsonb", nullable: true }) @Property({ columnType: "jsonb", nullable: true })
@@ -90,6 +103,14 @@ export default class TaxRate {
@Property({ columnType: "text", nullable: true }) @Property({ columnType: "text", nullable: true })
created_by: string | null = null created_by: string | null = null
@createPsqlIndexStatementHelper({
tableName: TABLE_NAME,
columns: "deleted_at",
where: "deleted_at IS NOT NULL",
}).MikroORMIndex()
@Property({ columnType: "timestamptz", nullable: true })
deleted_at: Date | null = null
@BeforeCreate() @BeforeCreate()
onCreate() { onCreate() {
this.id = generateEntityId(this.id, "txr") this.id = generateEntityId(this.id, "txr")
+27 -8
View File
@@ -1,9 +1,11 @@
import { DAL } from "@medusajs/types" import { DAL } from "@medusajs/types"
import { import {
DALUtils,
createPsqlIndexStatementHelper, createPsqlIndexStatementHelper,
generateEntityId, generateEntityId,
} from "@medusajs/utils" } from "@medusajs/utils"
import { import {
Filter,
BeforeCreate, BeforeCreate,
Collection, Collection,
Entity, Entity,
@@ -18,7 +20,7 @@ import {
} from "@mikro-orm/core" } from "@mikro-orm/core"
import TaxRate from "./tax-rate" import TaxRate from "./tax-rate"
type OptionalTaxRegionProps = DAL.EntityDateColumns type OptionalTaxRegionProps = DAL.SoftDeletableEntityDateColumns
const TABLE_NAME = "tax_region" const TABLE_NAME = "tax_region"
@@ -37,8 +39,9 @@ const taxRegionCountryTopLevelCheckName = "CK_tax_region_country_top_level"
}) })
@countryCodeProvinceIndexStatement.MikroORMIndex() @countryCodeProvinceIndexStatement.MikroORMIndex()
@Entity({ tableName: TABLE_NAME }) @Entity({ tableName: TABLE_NAME })
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
export default class TaxRegion { export default class TaxRegion {
[OptionalProps]: OptionalTaxRegionProps [OptionalProps]?: OptionalTaxRegionProps
@PrimaryKey({ columnType: "text" }) @PrimaryKey({ columnType: "text" })
id!: string id!: string
@@ -49,20 +52,28 @@ export default class TaxRegion {
@Property({ columnType: "text", nullable: true }) @Property({ columnType: "text", nullable: true })
province_code: string | null = null province_code: string | null = null
@Property({ columnType: "text", nullable: true })
parent_id: string | null = null
@ManyToOne(() => TaxRegion, { @ManyToOne(() => TaxRegion, {
index: "IDX_tax_region_parent_id", index: "IDX_tax_region_parent_id",
cascade: [Cascade.PERSIST], fieldName: "parent_id",
onDelete: "set null", cascade: [Cascade.REMOVE],
mapToPk: true,
nullable: true, nullable: true,
}) })
parent_id: string | null = null
@ManyToOne(() => TaxRegion, { persist: false })
parent: TaxRegion parent: TaxRegion
@OneToMany(() => TaxRate, (label) => label.tax_region) @OneToMany(() => TaxRate, (label) => label.tax_region, {
cascade: ["soft-remove" as Cascade],
})
tax_rates = new Collection<TaxRate>(this) tax_rates = new Collection<TaxRate>(this)
@OneToMany(() => TaxRegion, (label) => label.parent, {
cascade: ["soft-remove" as Cascade],
})
children = new Collection<TaxRegion>(this)
@Property({ columnType: "jsonb", nullable: true }) @Property({ columnType: "jsonb", nullable: true })
metadata: Record<string, unknown> | null = null metadata: Record<string, unknown> | null = null
@@ -84,6 +95,14 @@ export default class TaxRegion {
@Property({ columnType: "text", nullable: true }) @Property({ columnType: "text", nullable: true })
created_by: string | null = null created_by: string | null = null
@createPsqlIndexStatementHelper({
tableName: TABLE_NAME,
columns: "deleted_at",
where: "deleted_at IS NOT NULL",
}).MikroORMIndex()
@Property({ columnType: "timestamptz", nullable: true })
deleted_at: Date | null = null
@BeforeCreate() @BeforeCreate()
onCreate() { onCreate() {
this.id = generateEntityId(this.id, "txreg") this.id = generateEntityId(this.id, "txreg")
+18
View File
@@ -99,4 +99,22 @@ export interface ITaxModuleService extends IModuleService {
calculationContext: TaxCalculationContext, calculationContext: TaxCalculationContext,
sharedContext?: Context sharedContext?: Context
): Promise<(ItemTaxLineDTO | ShippingTaxLineDTO)[]> ): Promise<(ItemTaxLineDTO | ShippingTaxLineDTO)[]>
softDelete<TReturnableLinkableKeys extends string = string>(
taxRateIds: string[],
config?: SoftDeleteReturn<TReturnableLinkableKeys>,
sharedContext?: Context
): Promise<Record<string, string[]> | void>
softDeleteTaxRegions<TReturnableLinkableKeys extends string = string>(
taxRegionIds: string[],
config?: SoftDeleteReturn<TReturnableLinkableKeys>,
sharedContext?: Context
): Promise<Record<string, string[]> | void>
softDeleteTaxRateRules<TReturnableLinkableKeys extends string = string>(
taxRateRulePairs: { tax_rate_id: string; reference_id: string }[],
config?: SoftDeleteReturn<TReturnableLinkableKeys>,
sharedContext?: Context
): Promise<Record<string, string[]> | void>
} }