From 3c5b020c5e316d7bdb59e8d6bb233b427d87cb32 Mon Sep 17 00:00:00 2001 From: Sebastian Rindom Date: Sun, 25 Feb 2024 22:38:41 +0100 Subject: [PATCH] feat(tax): singular creates and deletes of regions, rates, rules (#6464) **What** - Adds support for creating single rates, regions, rules. - Adds delete methods. --- .../integration-tests/__tests__/index.spec.ts | 111 ++++++++++- .../tax/src/services/tax-module-service.ts | 181 ++++++++++++++---- packages/types/src/tax/mutations.ts | 1 + packages/types/src/tax/service.ts | 24 +++ 4 files changed, 269 insertions(+), 48 deletions(-) diff --git a/packages/tax/integration-tests/__tests__/index.spec.ts b/packages/tax/integration-tests/__tests__/index.spec.ts index 3d4869bfee..27c96a5df8 100644 --- a/packages/tax/integration-tests/__tests__/index.spec.ts +++ b/packages/tax/integration-tests/__tests__/index.spec.ts @@ -9,15 +9,13 @@ moduleIntegrationTestRunner({ testSuite: ({ service }: SuiteOptions) => { describe("TaxModuleService", function () { it("should create a tax region", async () => { - const [region] = await service.createTaxRegions([ - { - country_code: "US", - default_tax_rate: { - name: "Test Rate", - rate: 0.2, - }, + const region = await service.createTaxRegions({ + country_code: "US", + default_tax_rate: { + name: "Test Rate", + rate: 0.2, }, - ]) + }) 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 // // Using the following structure to setup tests. diff --git a/packages/tax/src/services/tax-module-service.ts b/packages/tax/src/services/tax-module-service.ts index ab425256b8..a1af013d14 100644 --- a/packages/tax/src/services/tax-module-service.ts +++ b/packages/tax/src/services/tax-module-service.ts @@ -11,7 +11,9 @@ import { InjectManager, InjectTransactionManager, MedusaContext, + MedusaError, ModulesSdkUtils, + isDefined, promiseAll, } from "@medusajs/utils" import { TaxRate, TaxRegion, TaxRateRule } from "@models" @@ -86,11 +88,7 @@ export default class TaxModuleService< ): Promise { const input = Array.isArray(data) ? data : [data] const rates = await this.create_(input, sharedContext) - const result = await this.baseRepository_.serialize( - rates, - { populate: true } - ) - return Array.isArray(data) ? result : result[0] + return Array.isArray(data) ? rates : rates[0] } @InjectTransactionManager("baseRepository_") @@ -98,28 +96,80 @@ export default class TaxModuleService< data: TaxTypes.CreateTaxRateDTO[], @MedusaContext() sharedContext: Context = {} ) { - return await this.taxRateService_.create(data, sharedContext) - } - - @InjectManager("baseRepository_") - async createTaxRegions( - data: TaxTypes.CreateTaxRegionDTO[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - // TODO: check that country_code === parent.country_code - const [defaultRates, regionData] = data.reduce( + const [rules, rateData] = data.reduce( (acc, region) => { - const { default_tax_rate, ...rest } = region - acc[0].push({ - ...default_tax_rate, - is_default: true, - created_by: region.created_by, - }) + const { rules, ...rest } = region + acc[0].push(rules) acc[1].push(rest) return acc }, [[], []] as [ - Omit[], + (Omit[] | undefined)[], + Partial[] + ] + ) + + 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(rates, { + populate: true, + }) + } + + createTaxRegions( + data: TaxTypes.CreateTaxRegionDTO, + sharedContext?: Context + ): Promise + + createTaxRegions( + data: TaxTypes.CreateTaxRegionDTO[], + sharedContext?: Context + ): Promise + + @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 | null)[], Partial[] ] ) @@ -129,35 +179,53 @@ export default class TaxModuleService< sharedContext ) - const rates = regions.map((region: TaxRegionDTO, i: number) => { - return { - ...defaultRates[i], - tax_region_id: region.id, - } + const rates = regions + .map((region, i) => { + if (!defaultRates[i]) { + 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 await this.baseRepository_.serialize( - regions, - { - populate: true, - } - ) + return Array.isArray(data) ? result : result[0] } + createTaxRateRules( + data: TaxTypes.CreateTaxRateRuleDTO, + sharedContext?: Context + ): Promise + createTaxRateRules( + data: TaxTypes.CreateTaxRateRuleDTO[], + sharedContext?: Context + ): Promise + @InjectManager("baseRepository_") async createTaxRateRules( - data: TaxTypes.CreateTaxRateRuleDTO[], + data: TaxTypes.CreateTaxRateRuleDTO | TaxTypes.CreateTaxRateRuleDTO[], @MedusaContext() sharedContext: Context = {} - ): Promise { - 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< TaxTypes.TaxRateRuleDTO[] >(rules, { populate: true, }) - return result + return Array.isArray(data) ? result : result[0] } @InjectTransactionManager("baseRepository_") @@ -210,6 +278,41 @@ export default class TaxModuleService< 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( item: TaxTypes.TaxableItemDTO | TaxTypes.TaxableShippingDTO, rates: TTaxRate[] diff --git a/packages/types/src/tax/mutations.ts b/packages/types/src/tax/mutations.ts index dfbb680484..95b02433a7 100644 --- a/packages/types/src/tax/mutations.ts +++ b/packages/types/src/tax/mutations.ts @@ -3,6 +3,7 @@ export interface CreateTaxRateDTO { rate?: number | null code?: string | null name: string + rules?: Omit[] is_default?: boolean created_by?: string metadata?: Record diff --git a/packages/types/src/tax/service.ts b/packages/types/src/tax/service.ts index 0d16599496..f48fb09e70 100644 --- a/packages/types/src/tax/service.ts +++ b/packages/types/src/tax/service.ts @@ -1,4 +1,5 @@ import { FindConfig } from "../common" +import { SoftDeleteReturn } from "../dal" import { IModuleService } from "../modules-sdk" import { Context } from "../shared-context" import { @@ -48,22 +49,45 @@ export interface ITaxModuleService extends IModuleService { delete(taxRateIds: string[], sharedContext?: Context): Promise delete(taxRateId: string, sharedContext?: Context): Promise + createTaxRegions( + data: CreateTaxRegionDTO, + sharedContext?: Context + ): Promise createTaxRegions( data: CreateTaxRegionDTO[], sharedContext?: Context ): Promise + deleteTaxRegions( + taxRegionIds: string[], + sharedContext?: Context + ): Promise + deleteTaxRegions(taxRegionId: string, sharedContext?: Context): Promise + listTaxRegions( filters?: FilterableTaxRegionProps, config?: FindConfig, sharedContext?: Context ): Promise + createTaxRateRules( + data: CreateTaxRateRuleDTO, + sharedContext?: Context + ): Promise createTaxRateRules( data: CreateTaxRateRuleDTO[], sharedContext?: Context ): Promise + deleteTaxRateRules( + taxRateRulePair: { tax_rate_id: string; reference_id: string }, + sharedContext?: Context + ): Promise + deleteTaxRateRules( + taxRateRulePair: { tax_rate_id: string; reference_id: string }[], + sharedContext?: Context + ): Promise + listTaxRateRules( filters?: FilterableTaxRateRuleProps, config?: FindConfig,