From 6d6c15f2229915477800b41039f30651bc5ac4d4 Mon Sep 17 00:00:00 2001 From: Sebastian Rindom Date: Fri, 1 Mar 2024 10:51:48 +0100 Subject: [PATCH 1/5] feat(tax): add endpoints to manage tax rate rules --- .../modules/__tests__/tax/admin/tax.spec.ts | 135 ++++++++++++++++++ integration-tests/modules/medusa-config.js | 4 +- .../src/tax/steps/create-tax-rate-rules.ts | 31 ++++ .../src/tax/steps/delete-tax-rate-rules.ts | 28 ++++ packages/core-flows/src/tax/steps/index.ts | 2 + .../tax/workflows/create-tax-rate-rules.ts | 15 ++ .../tax/workflows/delete-tax-rate-rules.ts | 12 ++ .../core-flows/src/tax/workflows/index.ts | 3 + .../src/tax/workflows/set-tax-rate-rules.ts | 49 +++++++ .../tax-rates/[id]/rules/[rule_id]/route.ts | 33 +++++ .../tax-rates/[id]/rules/batch/set/route.ts | 40 ++++++ .../admin/tax-rates/[id]/rules/route.ts | 42 ++++++ .../src/api-v2/admin/tax-rates/middlewares.ts | 12 ++ .../src/api-v2/admin/tax-rates/validators.ts | 8 ++ packages/medusa/src/loaders/api.ts | 4 +- .../tax/src/services/tax-module-service.ts | 1 + packages/types/src/tax/common.ts | 1 + packages/types/src/tax/mutations.ts | 2 +- packages/types/src/tax/service.ts | 2 +- 19 files changed, 419 insertions(+), 5 deletions(-) create mode 100644 packages/core-flows/src/tax/steps/create-tax-rate-rules.ts create mode 100644 packages/core-flows/src/tax/steps/delete-tax-rate-rules.ts create mode 100644 packages/core-flows/src/tax/workflows/create-tax-rate-rules.ts create mode 100644 packages/core-flows/src/tax/workflows/delete-tax-rate-rules.ts create mode 100644 packages/core-flows/src/tax/workflows/set-tax-rate-rules.ts create mode 100644 packages/medusa/src/api-v2/admin/tax-rates/[id]/rules/[rule_id]/route.ts create mode 100644 packages/medusa/src/api-v2/admin/tax-rates/[id]/rules/batch/set/route.ts create mode 100644 packages/medusa/src/api-v2/admin/tax-rates/[id]/rules/route.ts diff --git a/integration-tests/modules/__tests__/tax/admin/tax.spec.ts b/integration-tests/modules/__tests__/tax/admin/tax.spec.ts index 23c675cbadda5..6ba69e052526f 100644 --- a/integration-tests/modules/__tests__/tax/admin/tax.spec.ts +++ b/integration-tests/modules/__tests__/tax/admin/tax.spec.ts @@ -373,4 +373,139 @@ describe("Taxes - Admin", () => { expect(rates.length).toEqual(1) expect(rates[0].deleted_at).not.toBeNull() }) + + it("can create a tax rate add rules and remove them", async () => { + const api = useApi() as any + const regionRes = await api.post( + `/admin/tax-regions`, + { + country_code: "us", + default_tax_rate: { code: "default", rate: 2, name: "default rate" }, + }, + adminHeaders + ) + + const usRegionId = regionRes.data.tax_region.id + const rateRes = await api.post( + `/admin/tax-rates`, + { + tax_region_id: usRegionId, + code: "RATE2", + name: "another rate", + rate: 10, + rules: [{ reference: "product", reference_id: "prod_1234" }], + }, + adminHeaders + ) + const rateId = rateRes.data.tax_rate.id + let rules = await service.listTaxRateRules({ tax_rate_id: rateId }) + + expect(rules).toEqual([ + { + id: expect.any(String), + tax_rate_id: rateId, + reference: "product", + reference_id: "prod_1234", + created_by: "admin_user", + created_at: expect.any(Date), + updated_at: expect.any(Date), + deleted_at: null, + tax_rate: { id: rateId }, + metadata: null, + }, + ]) + + await api.post( + `/admin/tax-rates/${rateId}/rules`, + { + reference: "product", + reference_id: "prod_1111", + }, + adminHeaders + ) + + await api.post( + `/admin/tax-rates/${rateId}/rules`, + { + reference: "product", + reference_id: "prod_2222", + }, + adminHeaders + ) + rules = await service.listTaxRateRules({ tax_rate_id: rateId }) + expect(rules).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + tax_rate_id: rateId, + reference: "product", + reference_id: "prod_1234", + created_by: "admin_user", + }), + expect.objectContaining({ + tax_rate_id: rateId, + reference: "product", + reference_id: "prod_1111", + created_by: "admin_user", + }), + expect.objectContaining({ + tax_rate_id: rateId, + reference: "product", + reference_id: "prod_2222", + created_by: "admin_user", + }), + ]) + ) + + const toDeleteId = rules.find((r) => r.reference_id === "prod_1111")!.id + await api.delete( + `/admin/tax-rates/${rateId}/rules/${toDeleteId}`, + adminHeaders + ) + + rules = await service.listTaxRateRules({ tax_rate_id: rateId }) + expect(rules.length).toEqual(2) + + await api.post( + `/admin/tax-rates/${rateId}/rules/batch/set`, + { + rules: [ + { reference: "product", reference_id: "prod_3333" }, + { reference: "product", reference_id: "prod_4444" }, + { reference: "product", reference_id: "prod_5555" }, + { reference: "product", reference_id: "prod_6666" }, + ], + }, + adminHeaders + ) + rules = await service.listTaxRateRules({ tax_rate_id: rateId }) + expect(rules.length).toEqual(4) + expect(rules).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + tax_rate_id: rateId, + reference: "product", + reference_id: "prod_3333", + created_by: "admin_user", + }), + expect.objectContaining({ + tax_rate_id: rateId, + reference: "product", + reference_id: "prod_4444", + created_by: "admin_user", + }), + expect.objectContaining({ + tax_rate_id: rateId, + reference: "product", + reference_id: "prod_5555", + created_by: "admin_user", + }), + expect.objectContaining({ + tax_rate_id: rateId, + reference: "product", + reference_id: "prod_6666", + created_by: "admin_user", + }), + ]) + ) + }) }) diff --git a/integration-tests/modules/medusa-config.js b/integration-tests/modules/medusa-config.js index e0d5289d41384..b333ddf3b0f99 100644 --- a/integration-tests/modules/medusa-config.js +++ b/integration-tests/modules/medusa-config.js @@ -48,8 +48,8 @@ module.exports = { resolve: "@medusajs/cache-inmemory", options: { ttl: 0 }, // Cache disabled }, - [Modules.STOCK_LOCATION]: true, - [Modules.INVENTORY]: true, + // [Modules.STOCK_LOCATION]: true, + // [Modules.INVENTORY]: true, [Modules.PRODUCT]: true, [Modules.PRICING]: true, [Modules.PROMOTION]: true, diff --git a/packages/core-flows/src/tax/steps/create-tax-rate-rules.ts b/packages/core-flows/src/tax/steps/create-tax-rate-rules.ts new file mode 100644 index 0000000000000..8e61a37f3e5de --- /dev/null +++ b/packages/core-flows/src/tax/steps/create-tax-rate-rules.ts @@ -0,0 +1,31 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { CreateTaxRateRuleDTO, ITaxModuleService } from "@medusajs/types" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +export const createTaxRateRulesStepId = "create-tax-rate-rules" +export const createTaxRateRulesStep = createStep( + createTaxRateRulesStepId, + async (data: CreateTaxRateRuleDTO[], { container }) => { + const service = container.resolve( + ModuleRegistrationName.TAX + ) + + const created = await service.createTaxRateRules(data) + + return new StepResponse( + created, + created.map((rate) => rate.id) + ) + }, + async (createdIds, { container }) => { + if (!createdIds?.length) { + return + } + + const service = container.resolve( + ModuleRegistrationName.TAX + ) + + await service.delete(createdIds) + } +) diff --git a/packages/core-flows/src/tax/steps/delete-tax-rate-rules.ts b/packages/core-flows/src/tax/steps/delete-tax-rate-rules.ts new file mode 100644 index 0000000000000..e18673f36b7de --- /dev/null +++ b/packages/core-flows/src/tax/steps/delete-tax-rate-rules.ts @@ -0,0 +1,28 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { ITaxModuleService } from "@medusajs/types" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +export const deleteTaxRateRulesStepId = "delete-tax-rate-rules" +export const deleteTaxRateRulesStep = createStep( + deleteTaxRateRulesStepId, + async (ids: string[], { container }) => { + const service = container.resolve( + ModuleRegistrationName.TAX + ) + + await service.softDeleteTaxRateRules(ids) + + return new StepResponse(void 0, ids) + }, + async (prevIds, { container }) => { + if (!prevIds?.length) { + return + } + + const service = container.resolve( + ModuleRegistrationName.TAX + ) + + await service.restore(prevIds) + } +) diff --git a/packages/core-flows/src/tax/steps/index.ts b/packages/core-flows/src/tax/steps/index.ts index 5bb861dab387d..d926bb269a4bc 100644 --- a/packages/core-flows/src/tax/steps/index.ts +++ b/packages/core-flows/src/tax/steps/index.ts @@ -3,3 +3,5 @@ export * from "./delete-tax-regions" export * from "./create-tax-rates" export * from "./update-tax-rates" export * from "./delete-tax-rates" +export * from "./delete-tax-rate-rules" +export * from "./create-tax-rate-rules" diff --git a/packages/core-flows/src/tax/workflows/create-tax-rate-rules.ts b/packages/core-flows/src/tax/workflows/create-tax-rate-rules.ts new file mode 100644 index 0000000000000..f6f5bc1f57b1c --- /dev/null +++ b/packages/core-flows/src/tax/workflows/create-tax-rate-rules.ts @@ -0,0 +1,15 @@ +import { CreateTaxRateRuleDTO, TaxRateRuleDTO } from "@medusajs/types" +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" +import { createTaxRateRulesStep } from "../steps" + +type WorkflowInput = { + rules: CreateTaxRateRuleDTO[] +} + +export const createTaxRateRulesWorkflowId = "create-tax-rate-rules" +export const createTaxRateRulesWorkflow = createWorkflow( + createTaxRateRulesWorkflowId, + (input: WorkflowData): WorkflowData => { + return createTaxRateRulesStep(input.rules) + } +) diff --git a/packages/core-flows/src/tax/workflows/delete-tax-rate-rules.ts b/packages/core-flows/src/tax/workflows/delete-tax-rate-rules.ts new file mode 100644 index 0000000000000..64a1a5f58cf43 --- /dev/null +++ b/packages/core-flows/src/tax/workflows/delete-tax-rate-rules.ts @@ -0,0 +1,12 @@ +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" +import { deleteTaxRateRulesStep } from "../steps" + +type WorkflowInput = { ids: string[] } + +export const deleteTaxRateRulesWorkflowId = "delete-tax-rate-rules" +export const deleteTaxRateRulesWorkflow = createWorkflow( + deleteTaxRateRulesWorkflowId, + (input: WorkflowData): WorkflowData => { + return deleteTaxRateRulesStep(input.ids) + } +) diff --git a/packages/core-flows/src/tax/workflows/index.ts b/packages/core-flows/src/tax/workflows/index.ts index 5bb861dab387d..962cb3296103f 100644 --- a/packages/core-flows/src/tax/workflows/index.ts +++ b/packages/core-flows/src/tax/workflows/index.ts @@ -3,3 +3,6 @@ export * from "./delete-tax-regions" export * from "./create-tax-rates" export * from "./update-tax-rates" export * from "./delete-tax-rates" +export * from "./set-tax-rate-rules" +export * from "./create-tax-rate-rules" +export * from "./delete-tax-rate-rules" diff --git a/packages/core-flows/src/tax/workflows/set-tax-rate-rules.ts b/packages/core-flows/src/tax/workflows/set-tax-rate-rules.ts new file mode 100644 index 0000000000000..93cf2ae59ff1e --- /dev/null +++ b/packages/core-flows/src/tax/workflows/set-tax-rate-rules.ts @@ -0,0 +1,49 @@ +import { + CreateTaxRateRuleDTO, + ITaxModuleService, + TaxRateRuleDTO, +} from "@medusajs/types" +import { + StepResponse, + WorkflowData, + createStep, + createWorkflow, + transform, +} from "@medusajs/workflows-sdk" +import { createTaxRateRulesStep, deleteTaxRateRulesStep } from "../steps" +import { ModuleRegistrationName } from "@medusajs/modules-sdk" + +type WorkflowInput = { + taxRateId: string + rules: Omit[] +} + +const listRulesStep = createStep( + "set-tax-rate-rules-list-rules", + async ({ rateId }: { rateId: string }, { container }) => { + const service = container.resolve( + ModuleRegistrationName.TAX + ) + + const rules = await service.listTaxRateRules( + { tax_rate_id: rateId }, + { select: ["id"] } + ) + return new StepResponse(rules.map((r) => r.id)) + } +) + +export const setTaxRateRulesWorkflowId = "set-tax-rate-rules" +export const setTaxRateRulesWorkflow = createWorkflow( + setTaxRateRulesWorkflowId, + (input: WorkflowData): WorkflowData => { + const ruleIds = listRulesStep({ rateId: input.taxRateId }) + deleteTaxRateRulesStep(ruleIds) + + const rulesWithRateId = transform(input, ({ rules, taxRateId }) => { + return rules.map((r) => ({ ...r, tax_rate_id: taxRateId })) + }) + + return createTaxRateRulesStep(rulesWithRateId) + } +) diff --git a/packages/medusa/src/api-v2/admin/tax-rates/[id]/rules/[rule_id]/route.ts b/packages/medusa/src/api-v2/admin/tax-rates/[id]/rules/[rule_id]/route.ts new file mode 100644 index 0000000000000..4a8dacfa0210d --- /dev/null +++ b/packages/medusa/src/api-v2/admin/tax-rates/[id]/rules/[rule_id]/route.ts @@ -0,0 +1,33 @@ +import { deleteTaxRateRulesWorkflow } from "@medusajs/core-flows" +import { remoteQueryObjectFromString } from "@medusajs/utils" +import { defaultAdminTaxRatesFields } from "../../../../../../api/routes/admin/tax-rates" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "../../../../../../types/routing" + +export const DELETE = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const { errors } = await deleteTaxRateRulesWorkflow(req.scope).run({ + input: { ids: [req.params.rule_id] }, + throwOnError: false, + }) + + if (Array.isArray(errors) && errors[0]) { + throw errors[0].error + } + + const remoteQuery = req.scope.resolve("remoteQuery") + + const queryObject = remoteQueryObjectFromString({ + entryPoint: "tax_rate", + variables: { id: req.params.id }, + fields: defaultAdminTaxRatesFields, + }) + + const [taxRate] = await remoteQuery(queryObject) + + res.status(200).json({ tax_rate: taxRate }) +} diff --git a/packages/medusa/src/api-v2/admin/tax-rates/[id]/rules/batch/set/route.ts b/packages/medusa/src/api-v2/admin/tax-rates/[id]/rules/batch/set/route.ts new file mode 100644 index 0000000000000..75c3b7d6ee25b --- /dev/null +++ b/packages/medusa/src/api-v2/admin/tax-rates/[id]/rules/batch/set/route.ts @@ -0,0 +1,40 @@ +import { setTaxRateRulesWorkflow } from "@medusajs/core-flows" +import { remoteQueryObjectFromString } from "@medusajs/utils" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "../../../../../../../types/routing" +import { defaultAdminTaxRateFields } from "../../../../query-config" +import { AdminPostTaxRatesTaxRateRulesBatchSetReq } from "../../../../validators" + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const taxRateId = req.params.id + const rateRules = req.validatedBody.rules.map((r) => ({ + ...r, + created_by: req.auth.actor_id, + })) + + const { errors } = await setTaxRateRulesWorkflow(req.scope).run({ + input: { taxRateId, rules: rateRules }, + throwOnError: false, + }) + + if (Array.isArray(errors) && errors[0]) { + throw errors[0].error + } + + const remoteQuery = req.scope.resolve("remoteQuery") + + const queryObject = remoteQueryObjectFromString({ + entryPoint: "tax_rate", + variables: { id: req.params.id }, + fields: defaultAdminTaxRateFields, + }) + + const [taxRate] = await remoteQuery(queryObject) + + res.status(200).json({ tax_rate: taxRate }) +} diff --git a/packages/medusa/src/api-v2/admin/tax-rates/[id]/rules/route.ts b/packages/medusa/src/api-v2/admin/tax-rates/[id]/rules/route.ts new file mode 100644 index 0000000000000..b1d8fbb8e9ab0 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/tax-rates/[id]/rules/route.ts @@ -0,0 +1,42 @@ +import { createTaxRateRulesWorkflow } from "@medusajs/core-flows" +import { remoteQueryObjectFromString } from "@medusajs/utils" +import { defaultAdminTaxRatesFields } from "../../../../../api/routes/admin/tax-rates" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "../../../../../types/routing" +import { AdminPostTaxRatesTaxRateRulesReq } from "../../validators" + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const { errors } = await createTaxRateRulesWorkflow(req.scope).run({ + input: { + rules: [ + { + ...req.validatedBody, + tax_rate_id: req.params.id, + created_by: req.auth.actor_id, + }, + ], + }, + throwOnError: false, + }) + + if (Array.isArray(errors) && errors[0]) { + throw errors[0].error + } + + const remoteQuery = req.scope.resolve("remoteQuery") + + const queryObject = remoteQueryObjectFromString({ + entryPoint: "tax_rate", + variables: { id: req.params.id }, + fields: defaultAdminTaxRatesFields, + }) + + const [taxRate] = await remoteQuery(queryObject) + + res.status(200).json({ tax_rate: taxRate }) +} diff --git a/packages/medusa/src/api-v2/admin/tax-rates/middlewares.ts b/packages/medusa/src/api-v2/admin/tax-rates/middlewares.ts index ffdbe51d641f6..7f8d7a110d0a3 100644 --- a/packages/medusa/src/api-v2/admin/tax-rates/middlewares.ts +++ b/packages/medusa/src/api-v2/admin/tax-rates/middlewares.ts @@ -4,6 +4,8 @@ import { AdminGetTaxRatesTaxRateParams, AdminPostTaxRatesReq, AdminPostTaxRatesTaxRateReq, + AdminPostTaxRatesTaxRateRulesBatchSetReq, + AdminPostTaxRatesTaxRateRulesReq, } from "./validators" import { transformBody, transformQuery } from "../../../api/middlewares" @@ -36,4 +38,14 @@ export const adminTaxRateRoutesMiddlewares: MiddlewareRoute[] = [ ), ], }, + { + method: "POST", + matcher: "/admin/tax-rates/:id/rules/batch/set", + middlewares: [transformBody(AdminPostTaxRatesTaxRateRulesBatchSetReq)], + }, + { + method: "POST", + matcher: "/admin/tax-rates/:id/rules", + middlewares: [transformBody(AdminPostTaxRatesTaxRateRulesReq)], + }, ] diff --git a/packages/medusa/src/api-v2/admin/tax-rates/validators.ts b/packages/medusa/src/api-v2/admin/tax-rates/validators.ts index 938f2a6d15838..45fbd75a617eb 100644 --- a/packages/medusa/src/api-v2/admin/tax-rates/validators.ts +++ b/packages/medusa/src/api-v2/admin/tax-rates/validators.ts @@ -77,3 +77,11 @@ export class AdminPostTaxRatesTaxRateReq { @IsOptional() metadata?: Record } + +export class AdminPostTaxRatesTaxRateRulesReq extends CreateTaxRateRule {} + +export class AdminPostTaxRatesTaxRateRulesBatchSetReq { + @ValidateNested({ each: true }) + @Type(() => CreateTaxRateRule) + rules: CreateTaxRateRule[] +} diff --git a/packages/medusa/src/loaders/api.ts b/packages/medusa/src/loaders/api.ts index 478d6c2f473c0..9010ef30c341d 100644 --- a/packages/medusa/src/loaders/api.ts +++ b/packages/medusa/src/loaders/api.ts @@ -47,7 +47,9 @@ export default async ({ configModule, }).load() } catch (err) { - throw Error("An error occurred while registering Medusa Core API Routes") + throw Error( + "An error occurred while registering Medusa Core API Routes. See error in logs for more details." + ) } } else { app.use(bodyParser.json()) diff --git a/packages/tax/src/services/tax-module-service.ts b/packages/tax/src/services/tax-module-service.ts index 626f3751cb1ad..cd2a7428ba65b 100644 --- a/packages/tax/src/services/tax-module-service.ts +++ b/packages/tax/src/services/tax-module-service.ts @@ -135,6 +135,7 @@ export default class TaxModuleService< rateRules.map((r) => { return { ...r, + created_by: rate.created_by, tax_rate_id: rate.id, } }) diff --git a/packages/types/src/tax/common.ts b/packages/types/src/tax/common.ts index 45e4b97e93571..6a11cc3169972 100644 --- a/packages/types/src/tax/common.ts +++ b/packages/types/src/tax/common.ts @@ -97,6 +97,7 @@ export interface FilterableTaxRegionProps } export interface TaxRateRuleDTO { + id: string reference: string reference_id: string tax_rate_id: string diff --git a/packages/types/src/tax/mutations.ts b/packages/types/src/tax/mutations.ts index 39e302e3ac852..388c60e62103e 100644 --- a/packages/types/src/tax/mutations.ts +++ b/packages/types/src/tax/mutations.ts @@ -47,5 +47,5 @@ export interface CreateTaxRateRuleDTO { reference_id: string tax_rate_id: string metadata?: Record - created_by?: string + created_by?: string | null } diff --git a/packages/types/src/tax/service.ts b/packages/types/src/tax/service.ts index 34f719b30f9c3..6010bdc55d17c 100644 --- a/packages/types/src/tax/service.ts +++ b/packages/types/src/tax/service.ts @@ -149,7 +149,7 @@ export interface ITaxModuleService extends IModuleService { ): Promise | void> softDeleteTaxRateRules( - taxRateRulePairs: { tax_rate_id: string; reference_id: string }[], + taxRateRuleIds: string[], config?: SoftDeleteReturn, sharedContext?: Context ): Promise | void> From 7cd99733a379ea61ea4c3495e6899ad819ff8943 Mon Sep 17 00:00:00 2001 From: Sebastian Rindom Date: Fri, 1 Mar 2024 10:52:30 +0100 Subject: [PATCH 2/5] fix --- integration-tests/modules/medusa-config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration-tests/modules/medusa-config.js b/integration-tests/modules/medusa-config.js index b333ddf3b0f99..e0d5289d41384 100644 --- a/integration-tests/modules/medusa-config.js +++ b/integration-tests/modules/medusa-config.js @@ -48,8 +48,8 @@ module.exports = { resolve: "@medusajs/cache-inmemory", options: { ttl: 0 }, // Cache disabled }, - // [Modules.STOCK_LOCATION]: true, - // [Modules.INVENTORY]: true, + [Modules.STOCK_LOCATION]: true, + [Modules.INVENTORY]: true, [Modules.PRODUCT]: true, [Modules.PRICING]: true, [Modules.PROMOTION]: true, From 51e56b55b340692696d0162963a210df1e612fc2 Mon Sep 17 00:00:00 2001 From: Sebastian Rindom Date: Fri, 1 Mar 2024 16:04:43 +0100 Subject: [PATCH 3/5] fix: snake_case --- .../src/tax/workflows/set-tax-rate-rules.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/core-flows/src/tax/workflows/set-tax-rate-rules.ts b/packages/core-flows/src/tax/workflows/set-tax-rate-rules.ts index 93cf2ae59ff1e..3534d9b8e4ec8 100644 --- a/packages/core-flows/src/tax/workflows/set-tax-rate-rules.ts +++ b/packages/core-flows/src/tax/workflows/set-tax-rate-rules.ts @@ -14,19 +14,19 @@ import { createTaxRateRulesStep, deleteTaxRateRulesStep } from "../steps" import { ModuleRegistrationName } from "@medusajs/modules-sdk" type WorkflowInput = { - taxRateId: string + tax_rate_id: string rules: Omit[] } -const listRulesStep = createStep( +const listRuleIdsStep = createStep( "set-tax-rate-rules-list-rules", - async ({ rateId }: { rateId: string }, { container }) => { + async ({ rate_id }: { rate_id: string }, { container }) => { const service = container.resolve( ModuleRegistrationName.TAX ) const rules = await service.listTaxRateRules( - { tax_rate_id: rateId }, + { tax_rate_id: rate_id }, { select: ["id"] } ) return new StepResponse(rules.map((r) => r.id)) @@ -37,11 +37,11 @@ export const setTaxRateRulesWorkflowId = "set-tax-rate-rules" export const setTaxRateRulesWorkflow = createWorkflow( setTaxRateRulesWorkflowId, (input: WorkflowData): WorkflowData => { - const ruleIds = listRulesStep({ rateId: input.taxRateId }) + const ruleIds = listRuleIdsStep({ rate_id: input.tax_rate_id }) deleteTaxRateRulesStep(ruleIds) - const rulesWithRateId = transform(input, ({ rules, taxRateId }) => { - return rules.map((r) => ({ ...r, tax_rate_id: taxRateId })) + const rulesWithRateId = transform(input, ({ rules, tax_rate_id }) => { + return rules.map((r) => ({ ...r, tax_rate_id })) }) return createTaxRateRulesStep(rulesWithRateId) From 53d06376424254ccf39bd217ec9457618a07bb56 Mon Sep 17 00:00:00 2001 From: Sebastian Rindom Date: Fri, 1 Mar 2024 17:56:05 +0100 Subject: [PATCH 4/5] fix --- .../src/api-v2/admin/tax-rates/[id]/rules/batch/set/route.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/medusa/src/api-v2/admin/tax-rates/[id]/rules/batch/set/route.ts b/packages/medusa/src/api-v2/admin/tax-rates/[id]/rules/batch/set/route.ts index 75c3b7d6ee25b..abc3cb9db513d 100644 --- a/packages/medusa/src/api-v2/admin/tax-rates/[id]/rules/batch/set/route.ts +++ b/packages/medusa/src/api-v2/admin/tax-rates/[id]/rules/batch/set/route.ts @@ -11,14 +11,13 @@ export const POST = async ( req: AuthenticatedMedusaRequest, res: MedusaResponse ) => { - const taxRateId = req.params.id const rateRules = req.validatedBody.rules.map((r) => ({ ...r, created_by: req.auth.actor_id, })) const { errors } = await setTaxRateRulesWorkflow(req.scope).run({ - input: { taxRateId, rules: rateRules }, + input: { tax_rate_id: req.params.id, rules: rateRules }, throwOnError: false, }) From d60564ca4ffbb37a45e51addb19070db4c14ee61 Mon Sep 17 00:00:00 2001 From: Sebastian Rindom Date: Sat, 2 Mar 2024 15:37:58 +0100 Subject: [PATCH 5/5] Set rules as part of update * fix * fix * fix: update tax rate with rule setting * fix: add created_by --- .../modules/__tests__/tax/admin/tax.spec.ts | 2 +- .../__tests__/tax/workflow/tax.spec.ts | 209 ++++++++++++++++++ .../src/tax/steps/create-tax-rate-rules.ts | 4 +- .../src/tax/steps/delete-tax-rate-rules.ts | 6 +- packages/core-flows/src/tax/steps/index.ts | 2 + .../src/tax/steps/list-tax-rate-ids.ts | 23 ++ .../src/tax/steps/list-tax-rate-rule-ids.ts | 22 ++ .../src/tax/workflows/set-tax-rate-rules.ts | 55 +++-- .../src/tax/workflows/update-tax-rates.ts | 134 ++++++++++- .../src/api-v2/admin/tax-rates/[id]/route.ts | 2 +- .../tax-rates/[id]/rules/batch/set/route.ts | 39 ---- .../src/api-v2/admin/tax-rates/middlewares.ts | 6 - .../src/api-v2/admin/tax-rates/validators.ts | 10 +- .../integration-tests/__tests__/index.spec.ts | 83 +++++++ packages/tax/src/models/tax-rate-rule.ts | 4 +- packages/tax/src/models/tax-rate.ts | 1 + .../tax/src/services/tax-module-service.ts | 77 +++++++ packages/types/src/tax/common.ts | 2 +- packages/types/src/tax/mutations.ts | 4 +- packages/types/src/tax/service.ts | 10 +- 20 files changed, 601 insertions(+), 94 deletions(-) create mode 100644 integration-tests/modules/__tests__/tax/workflow/tax.spec.ts create mode 100644 packages/core-flows/src/tax/steps/list-tax-rate-ids.ts create mode 100644 packages/core-flows/src/tax/steps/list-tax-rate-rule-ids.ts delete mode 100644 packages/medusa/src/api-v2/admin/tax-rates/[id]/rules/batch/set/route.ts diff --git a/integration-tests/modules/__tests__/tax/admin/tax.spec.ts b/integration-tests/modules/__tests__/tax/admin/tax.spec.ts index 6ba69e052526f..9a4e7efce3979 100644 --- a/integration-tests/modules/__tests__/tax/admin/tax.spec.ts +++ b/integration-tests/modules/__tests__/tax/admin/tax.spec.ts @@ -466,7 +466,7 @@ describe("Taxes - Admin", () => { expect(rules.length).toEqual(2) await api.post( - `/admin/tax-rates/${rateId}/rules/batch/set`, + `/admin/tax-rates/${rateId}`, { rules: [ { reference: "product", reference_id: "prod_3333" }, diff --git a/integration-tests/modules/__tests__/tax/workflow/tax.spec.ts b/integration-tests/modules/__tests__/tax/workflow/tax.spec.ts new file mode 100644 index 0000000000000..ea6a57e0d1598 --- /dev/null +++ b/integration-tests/modules/__tests__/tax/workflow/tax.spec.ts @@ -0,0 +1,209 @@ +import path from "path" +import { ITaxModuleService } from "@medusajs/types" +import { ModuleRegistrationName } from "@medusajs/modules-sdk" + +import { createAdminUser } from "../../../helpers/create-admin-user" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import { getContainer } from "../../../../environment-helpers/use-container" +import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" +import { useApi } from "../../../../environment-helpers/use-api" +import { + createTaxRateRulesStepId, + maybeSetTaxRateRulesStepId, + updateTaxRatesStepId, + updateTaxRatesWorkflow, +} from "@medusajs/core-flows" + +jest.setTimeout(50000) + +const env = { MEDUSA_FF_MEDUSA_V2: true } +const adminHeaders = { + headers: { "x-medusa-access-token": "test_token" }, +} + +describe("Taxes - Workflow", () => { + let dbConnection + let appContainer + let shutdownServer + let service: ITaxModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + service = appContainer.resolve(ModuleRegistrationName.TAX) + }) + + beforeEach(async () => { + await createAdminUser(dbConnection, adminHeaders) + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("compensates rules correctly", async () => { + const taxRegion = await service.createTaxRegions({ + country_code: "us", + }) + + const [rateOne, rateTwo] = await service.create([ + { + tax_region_id: taxRegion.id, + rate: 10, + code: "standard", + name: "Standard", + rules: [ + { reference: "shipping", reference_id: "shipping_12354" }, + { reference: "shipping", reference_id: "shipping_11111" }, + { reference: "shipping", reference_id: "shipping_22222" }, + ], + }, + { + tax_region_id: taxRegion.id, + rate: 2, + code: "reduced", + name: "Reduced", + rules: [ + { reference: "product", reference_id: "product_12354" }, + { reference: "product", reference_id: "product_11111" }, + { reference: "product", reference_id: "product_22222" }, + ], + }, + ]) + + const workflow = updateTaxRatesWorkflow(appContainer) + + workflow.appendAction("throw", createTaxRateRulesStepId, { + invoke: async function failStep() { + throw new Error(`Failed to update`) + }, + }) + + await workflow.run({ + input: { + selector: { tax_region_id: taxRegion.id }, + update: { + rate: 2, + rules: [ + { reference: "product", reference_id: "product_12354" }, + { reference: "shipping", reference_id: "shipping_12354" }, + ], + }, + }, + throwOnError: false, + }) + + const taxRateRules = await service.listTaxRateRules({ + tax_rate: { tax_region_id: taxRegion.id }, + }) + + expect(taxRateRules.length).toEqual(6) + expect(taxRateRules).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + tax_rate_id: rateOne.id, + reference_id: "shipping_12354", + }), + expect.objectContaining({ + tax_rate_id: rateOne.id, + reference_id: "shipping_11111", + }), + expect.objectContaining({ + tax_rate_id: rateOne.id, + reference_id: "shipping_22222", + }), + expect.objectContaining({ + tax_rate_id: rateTwo.id, + reference_id: "product_12354", + }), + expect.objectContaining({ + tax_rate_id: rateTwo.id, + reference_id: "product_11111", + }), + expect.objectContaining({ + tax_rate_id: rateTwo.id, + reference_id: "product_22222", + }), + ]) + ) + }) + + it("creates rules correctly", async () => { + const taxRegion = await service.createTaxRegions({ + country_code: "us", + }) + + const [rateOne, rateTwo] = await service.create([ + { + tax_region_id: taxRegion.id, + rate: 10, + code: "standard", + name: "Standard", + rules: [ + { reference: "shipping", reference_id: "shipping_12354" }, + { reference: "shipping", reference_id: "shipping_11111" }, + { reference: "shipping", reference_id: "shipping_22222" }, + ], + }, + { + tax_region_id: taxRegion.id, + rate: 2, + code: "reduced", + name: "Reduced", + rules: [ + { reference: "product", reference_id: "product_12354" }, + { reference: "product", reference_id: "product_11111" }, + { reference: "product", reference_id: "product_22222" }, + ], + }, + ]) + + await updateTaxRatesWorkflow(appContainer).run({ + input: { + selector: { tax_region_id: taxRegion.id }, + update: { + rate: 2, + rules: [ + { reference: "product", reference_id: "product_12354" }, + { reference: "shipping", reference_id: "shipping_12354" }, + ], + }, + }, + }) + + const taxRateRules = await service.listTaxRateRules({ + tax_rate: { tax_region_id: taxRegion.id }, + }) + + expect(taxRateRules.length).toEqual(4) + expect(taxRateRules).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + tax_rate_id: rateOne.id, + reference_id: "shipping_12354", + }), + expect.objectContaining({ + tax_rate_id: rateTwo.id, + reference_id: "shipping_12354", + }), + expect.objectContaining({ + tax_rate_id: rateOne.id, + reference_id: "product_12354", + }), + expect.objectContaining({ + tax_rate_id: rateTwo.id, + reference_id: "product_12354", + }), + ]) + ) + }) +}) diff --git a/packages/core-flows/src/tax/steps/create-tax-rate-rules.ts b/packages/core-flows/src/tax/steps/create-tax-rate-rules.ts index 8e61a37f3e5de..f73e3477708e6 100644 --- a/packages/core-flows/src/tax/steps/create-tax-rate-rules.ts +++ b/packages/core-flows/src/tax/steps/create-tax-rate-rules.ts @@ -14,7 +14,7 @@ export const createTaxRateRulesStep = createStep( return new StepResponse( created, - created.map((rate) => rate.id) + created.map((rule) => rule.id) ) }, async (createdIds, { container }) => { @@ -26,6 +26,6 @@ export const createTaxRateRulesStep = createStep( ModuleRegistrationName.TAX ) - await service.delete(createdIds) + await service.deleteTaxRateRules(createdIds) } ) diff --git a/packages/core-flows/src/tax/steps/delete-tax-rate-rules.ts b/packages/core-flows/src/tax/steps/delete-tax-rate-rules.ts index e18673f36b7de..9003b4bdf7b9e 100644 --- a/packages/core-flows/src/tax/steps/delete-tax-rate-rules.ts +++ b/packages/core-flows/src/tax/steps/delete-tax-rate-rules.ts @@ -6,6 +6,10 @@ export const deleteTaxRateRulesStepId = "delete-tax-rate-rules" export const deleteTaxRateRulesStep = createStep( deleteTaxRateRulesStepId, async (ids: string[], { container }) => { + if (!ids?.length) { + return new StepResponse(void 0, []) + } + const service = container.resolve( ModuleRegistrationName.TAX ) @@ -23,6 +27,6 @@ export const deleteTaxRateRulesStep = createStep( ModuleRegistrationName.TAX ) - await service.restore(prevIds) + await service.restoreTaxRateRules(prevIds) } ) diff --git a/packages/core-flows/src/tax/steps/index.ts b/packages/core-flows/src/tax/steps/index.ts index d926bb269a4bc..12daa23ac3da1 100644 --- a/packages/core-flows/src/tax/steps/index.ts +++ b/packages/core-flows/src/tax/steps/index.ts @@ -5,3 +5,5 @@ export * from "./update-tax-rates" export * from "./delete-tax-rates" export * from "./delete-tax-rate-rules" export * from "./create-tax-rate-rules" +export * from "./list-tax-rate-rule-ids" +export * from "./list-tax-rate-ids" diff --git a/packages/core-flows/src/tax/steps/list-tax-rate-ids.ts b/packages/core-flows/src/tax/steps/list-tax-rate-ids.ts new file mode 100644 index 0000000000000..30da06c2372b8 --- /dev/null +++ b/packages/core-flows/src/tax/steps/list-tax-rate-ids.ts @@ -0,0 +1,23 @@ +import { createStep, StepResponse } from "@medusajs/workflows-sdk" +import { FilterableTaxRateProps, ITaxModuleService } from "@medusajs/types" +import { ModuleRegistrationName } from "@medusajs/modules-sdk" + +type StepInput = { + selector: FilterableTaxRateProps +} + +export const listTaxRateIdsStepId = "list-tax-rate-ids" +export const listTaxRateIdsStep = createStep( + listTaxRateIdsStepId, + async (input: StepInput, { container }) => { + const service = container.resolve( + ModuleRegistrationName.TAX + ) + + const rates = await service.list(input.selector, { + select: ["id"], + }) + + return new StepResponse(rates.map((r) => r.id)) + } +) diff --git a/packages/core-flows/src/tax/steps/list-tax-rate-rule-ids.ts b/packages/core-flows/src/tax/steps/list-tax-rate-rule-ids.ts new file mode 100644 index 0000000000000..8f1fa65c2b0fd --- /dev/null +++ b/packages/core-flows/src/tax/steps/list-tax-rate-rule-ids.ts @@ -0,0 +1,22 @@ +import { createStep, StepResponse } from "@medusajs/workflows-sdk" +import { FilterableTaxRateRuleProps, ITaxModuleService } from "@medusajs/types" +import { ModuleRegistrationName } from "@medusajs/modules-sdk" + +type StepInput = { + selector: FilterableTaxRateRuleProps +} + +export const listTaxRateRuleIdsStepId = "list-tax-rate-rule-ids" +export const listTaxRateRuleIdsStep = createStep( + listTaxRateRuleIdsStepId, + async (input: StepInput, { container }) => { + const service = container.resolve( + ModuleRegistrationName.TAX + ) + + const rules = await service.listTaxRateRules(input.selector, { + select: ["id"], + }) + return new StepResponse(rules.map((r) => r.id)) + } +) diff --git a/packages/core-flows/src/tax/workflows/set-tax-rate-rules.ts b/packages/core-flows/src/tax/workflows/set-tax-rate-rules.ts index 3534d9b8e4ec8..d31e6da345550 100644 --- a/packages/core-flows/src/tax/workflows/set-tax-rate-rules.ts +++ b/packages/core-flows/src/tax/workflows/set-tax-rate-rules.ts @@ -1,48 +1,45 @@ +import { CreateTaxRateRuleDTO, TaxRateRuleDTO } from "@medusajs/types" import { - CreateTaxRateRuleDTO, - ITaxModuleService, - TaxRateRuleDTO, -} from "@medusajs/types" -import { - StepResponse, WorkflowData, - createStep, createWorkflow, transform, } from "@medusajs/workflows-sdk" -import { createTaxRateRulesStep, deleteTaxRateRulesStep } from "../steps" -import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { + createTaxRateRulesStep, + deleteTaxRateRulesStep, + listTaxRateRuleIdsStep, +} from "../steps" type WorkflowInput = { - tax_rate_id: string + tax_rate_ids: string[] rules: Omit[] } -const listRuleIdsStep = createStep( - "set-tax-rate-rules-list-rules", - async ({ rate_id }: { rate_id: string }, { container }) => { - const service = container.resolve( - ModuleRegistrationName.TAX - ) - - const rules = await service.listTaxRateRules( - { tax_rate_id: rate_id }, - { select: ["id"] } - ) - return new StepResponse(rules.map((r) => r.id)) - } -) - export const setTaxRateRulesWorkflowId = "set-tax-rate-rules" export const setTaxRateRulesWorkflow = createWorkflow( setTaxRateRulesWorkflowId, (input: WorkflowData): WorkflowData => { - const ruleIds = listRuleIdsStep({ rate_id: input.tax_rate_id }) + const ruleIds = listTaxRateRuleIdsStep({ + selector: { tax_rate_id: input.tax_rate_ids }, + }) + deleteTaxRateRulesStep(ruleIds) - const rulesWithRateId = transform(input, ({ rules, tax_rate_id }) => { - return rules.map((r) => ({ ...r, tax_rate_id })) - }) + const rulesWithRateId = transform( + { rules: input.rules, rateIds: input.tax_rate_ids }, + ({ rules, rateIds }) => { + return rules + .map((r) => { + return rateIds.map((id) => { + return { + ...r, + tax_rate_id: id, + } + }) + }) + .flat() + } + ) return createTaxRateRulesStep(rulesWithRateId) } diff --git a/packages/core-flows/src/tax/workflows/update-tax-rates.ts b/packages/core-flows/src/tax/workflows/update-tax-rates.ts index 16a10b59f3dee..fe5c8ae670f69 100644 --- a/packages/core-flows/src/tax/workflows/update-tax-rates.ts +++ b/packages/core-flows/src/tax/workflows/update-tax-rates.ts @@ -1,10 +1,23 @@ import { FilterableTaxRateProps, + ITaxModuleService, TaxRateDTO, UpdateTaxRateDTO, } from "@medusajs/types" -import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" -import { updateTaxRatesStep } from "../steps" +import { + StepResponse, + WorkflowData, + createStep, + createWorkflow, + transform, +} from "@medusajs/workflows-sdk" +import { + createTaxRateRulesStep, + deleteTaxRateRulesStep, + updateTaxRatesStep, +} from "../steps" +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +// import { setTaxRateRulesWorkflow } from "./set-tax-rate-rules" type UpdateTaxRatesStepInput = { selector: FilterableTaxRateProps @@ -13,10 +26,125 @@ type UpdateTaxRatesStepInput = { type WorkflowInput = UpdateTaxRatesStepInput +type StepInput = { + tax_rate_ids: string[] + update: UpdateTaxRateDTO +} + +// TODO: When we figure out how to compensate nested workflows, we can use this +// +// export const maybeSetTaxRateRulesStepId = "maybe-set-tax-rate-rules" +// const maybeSetTaxRateRules = createStep( +// maybeSetTaxRateRulesStepId, +// async (input: StepInput, { container }) => { +// const { update } = input +// +// if (!update.rules) { +// return new StepResponse([], "") +// } +// +// const { result, transaction } = await setTaxRateRulesWorkflow( +// container +// ).run({ +// input: { +// tax_rate_ids: input.tax_rate_ids, +// rules: update.rules, +// }, +// }) +// +// return new StepResponse(result, transaction.transactionId) +// }, +// async (transactionId, { container }) => { +// if (!transactionId) { +// return +// } +// +// await setTaxRateRulesWorkflow(container).cancel(transactionId) +// } +// ) + +const maybeListTaxRateRuleIdsStepId = "maybe-list-tax-rate-rule-ids" +const maybeListTaxRateRuleIdsStep = createStep( + maybeListTaxRateRuleIdsStepId, + async (input: StepInput, { container }) => { + const { update, tax_rate_ids } = input + + if (!update.rules) { + return new StepResponse([]) + } + + const service = container.resolve( + ModuleRegistrationName.TAX + ) + + const rules = await service.listTaxRateRules( + { tax_rate_id: tax_rate_ids }, + { select: ["id"] } + ) + + return new StepResponse(rules.map((r) => r.id)) + } +) + export const updateTaxRatesWorkflowId = "update-tax-rates" export const updateTaxRatesWorkflow = createWorkflow( updateTaxRatesWorkflowId, (input: WorkflowData): WorkflowData => { - return updateTaxRatesStep(input) + const cleanedUpdateInput = transform(input, (data) => { + // Transform clones data so we can safely modify it + if (data.update.rules) { + delete data.update.rules + } + + return { + selector: data.selector, + update: data.update, + } + }) + + const updatedRates = updateTaxRatesStep(cleanedUpdateInput) + const rateIds = transform(updatedRates, (rates) => rates.map((r) => r.id)) + + // TODO: Use when we figure out how to compensate nested workflows + // maybeSetTaxRateRules({ + // tax_rate_ids: rateIds, + // update: input.update, + // }) + + // COPY-PASTE from set-tax-rate-rules.ts + const ruleIds = maybeListTaxRateRuleIdsStep({ + tax_rate_ids: rateIds, + update: input.update, + }) + + deleteTaxRateRulesStep(ruleIds) + + const rulesWithRateId = transform( + { update: input.update, rateIds }, + ({ update, rateIds }) => { + if (!update.rules) { + return [] + } + + const updatedBy = update.updated_by + + return update.rules + .map((r) => { + return rateIds.map((id) => { + return { + ...r, + created_by: updatedBy, + tax_rate_id: id, + } + }) + }) + .flat() + } + ) + + createTaxRateRulesStep(rulesWithRateId) + // end of COPY-PASTE from set-tax-rate-rules.ts + + return updatedRates } ) diff --git a/packages/medusa/src/api-v2/admin/tax-rates/[id]/route.ts b/packages/medusa/src/api-v2/admin/tax-rates/[id]/route.ts index 6f8fdf6b3151a..51aa8f0bafaf0 100644 --- a/packages/medusa/src/api-v2/admin/tax-rates/[id]/route.ts +++ b/packages/medusa/src/api-v2/admin/tax-rates/[id]/route.ts @@ -18,7 +18,7 @@ export const POST = async ( const { errors } = await updateTaxRatesWorkflow(req.scope).run({ input: { selector: { id: req.params.id }, - update: req.validatedBody, + update: { ...req.validatedBody, updated_by: req.auth.actor_id }, }, throwOnError: false, }) diff --git a/packages/medusa/src/api-v2/admin/tax-rates/[id]/rules/batch/set/route.ts b/packages/medusa/src/api-v2/admin/tax-rates/[id]/rules/batch/set/route.ts deleted file mode 100644 index abc3cb9db513d..0000000000000 --- a/packages/medusa/src/api-v2/admin/tax-rates/[id]/rules/batch/set/route.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { setTaxRateRulesWorkflow } from "@medusajs/core-flows" -import { remoteQueryObjectFromString } from "@medusajs/utils" -import { - AuthenticatedMedusaRequest, - MedusaResponse, -} from "../../../../../../../types/routing" -import { defaultAdminTaxRateFields } from "../../../../query-config" -import { AdminPostTaxRatesTaxRateRulesBatchSetReq } from "../../../../validators" - -export const POST = async ( - req: AuthenticatedMedusaRequest, - res: MedusaResponse -) => { - const rateRules = req.validatedBody.rules.map((r) => ({ - ...r, - created_by: req.auth.actor_id, - })) - - const { errors } = await setTaxRateRulesWorkflow(req.scope).run({ - input: { tax_rate_id: req.params.id, rules: rateRules }, - throwOnError: false, - }) - - if (Array.isArray(errors) && errors[0]) { - throw errors[0].error - } - - const remoteQuery = req.scope.resolve("remoteQuery") - - const queryObject = remoteQueryObjectFromString({ - entryPoint: "tax_rate", - variables: { id: req.params.id }, - fields: defaultAdminTaxRateFields, - }) - - const [taxRate] = await remoteQuery(queryObject) - - res.status(200).json({ tax_rate: taxRate }) -} diff --git a/packages/medusa/src/api-v2/admin/tax-rates/middlewares.ts b/packages/medusa/src/api-v2/admin/tax-rates/middlewares.ts index 7f8d7a110d0a3..3c06e5757131a 100644 --- a/packages/medusa/src/api-v2/admin/tax-rates/middlewares.ts +++ b/packages/medusa/src/api-v2/admin/tax-rates/middlewares.ts @@ -4,7 +4,6 @@ import { AdminGetTaxRatesTaxRateParams, AdminPostTaxRatesReq, AdminPostTaxRatesTaxRateReq, - AdminPostTaxRatesTaxRateRulesBatchSetReq, AdminPostTaxRatesTaxRateRulesReq, } from "./validators" import { transformBody, transformQuery } from "../../../api/middlewares" @@ -38,11 +37,6 @@ export const adminTaxRateRoutesMiddlewares: MiddlewareRoute[] = [ ), ], }, - { - method: "POST", - matcher: "/admin/tax-rates/:id/rules/batch/set", - middlewares: [transformBody(AdminPostTaxRatesTaxRateRulesBatchSetReq)], - }, { method: "POST", matcher: "/admin/tax-rates/:id/rules", diff --git a/packages/medusa/src/api-v2/admin/tax-rates/validators.ts b/packages/medusa/src/api-v2/admin/tax-rates/validators.ts index 45fbd75a617eb..2c39c98d1b822 100644 --- a/packages/medusa/src/api-v2/admin/tax-rates/validators.ts +++ b/packages/medusa/src/api-v2/admin/tax-rates/validators.ts @@ -65,6 +65,10 @@ export class AdminPostTaxRatesTaxRateReq { @IsOptional() name?: string + @ValidateNested({ each: true }) + @Type(() => CreateTaxRateRule) + rules: CreateTaxRateRule[] + @IsBoolean() @IsOptional() is_default?: boolean @@ -79,9 +83,3 @@ export class AdminPostTaxRatesTaxRateReq { } export class AdminPostTaxRatesTaxRateRulesReq extends CreateTaxRateRule {} - -export class AdminPostTaxRatesTaxRateRulesBatchSetReq { - @ValidateNested({ each: true }) - @Type(() => CreateTaxRateRule) - rules: CreateTaxRateRule[] -} diff --git a/packages/tax/integration-tests/__tests__/index.spec.ts b/packages/tax/integration-tests/__tests__/index.spec.ts index cd28f2f9ae3de..e6f1704fc3378 100644 --- a/packages/tax/integration-tests/__tests__/index.spec.ts +++ b/packages/tax/integration-tests/__tests__/index.spec.ts @@ -75,6 +75,89 @@ moduleIntegrationTestRunner({ ) }) + it("should update tax rates with rules", async () => { + const region = await service.createTaxRegions({ + country_code: "US", + default_tax_rate: { + name: "Test Rate", + rate: 0.2, + }, + }) + + const rate = await service.create({ + tax_region_id: region.id, + name: "Shipping Rate", + code: "test", + rate: 8.23, + }) + + await service.update(rate.id, { + name: "Updated Rate", + code: "TEST", + rate: 8.25, + rules: [ + { reference: "product", reference_id: "product_id_1" }, + { reference: "product_type", reference_id: "product_type_id" }, + ], + }) + + const rules = await service.listTaxRateRules({ tax_rate_id: rate.id }) + + expect(rules).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + reference: "product", + reference_id: "product_id_1", + }), + expect.objectContaining({ + reference: "product_type", + reference_id: "product_type_id", + }), + ]) + ) + + await service.update(rate.id, { + rules: [ + { reference: "product", reference_id: "product_id_1" }, + { reference: "product", reference_id: "product_id_2" }, + { reference: "product_type", reference_id: "product_type_id_2" }, + { reference: "product_type", reference_id: "product_type_id_3" }, + ], + }) + + const rulesWithDeletes = await service.listTaxRateRules( + { tax_rate_id: rate.id }, + { withDeleted: true } + ) + + expect(rulesWithDeletes).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + reference: "product", + reference_id: "product_id_2", + }), + expect.objectContaining({ + reference: "product_type", + reference_id: "product_type_id_2", + }), + expect.objectContaining({ + reference: "product_type", + reference_id: "product_type_id_3", + }), + expect.objectContaining({ + reference: "product", + reference_id: "product_id_1", + deleted_at: expect.any(Date), + }), + expect.objectContaining({ + reference: "product_type", + reference_id: "product_type_id", + deleted_at: expect.any(Date), + }), + ]) + ) + }) + it("should create a tax region", async () => { const region = await service.createTaxRegions({ country_code: "US", diff --git a/packages/tax/src/models/tax-rate-rule.ts b/packages/tax/src/models/tax-rate-rule.ts index 998d7e00f0400..a16b08dae18a4 100644 --- a/packages/tax/src/models/tax-rate-rule.ts +++ b/packages/tax/src/models/tax-rate-rule.ts @@ -105,11 +105,11 @@ export default class TaxRateRule { @BeforeCreate() onCreate() { - this.id = generateEntityId(this.id, "txr") + this.id = generateEntityId(this.id, "txrule") } @OnInit() onInit() { - this.id = generateEntityId(this.id, "txr") + this.id = generateEntityId(this.id, "txrule") } } diff --git a/packages/tax/src/models/tax-rate.ts b/packages/tax/src/models/tax-rate.ts index 77d5203dc30d6..cbc853549e57a 100644 --- a/packages/tax/src/models/tax-rate.ts +++ b/packages/tax/src/models/tax-rate.ts @@ -79,6 +79,7 @@ export default class TaxRate { @OneToMany(() => TaxRateRule, (rule) => rule.tax_rate, { cascade: ["soft-remove" as Cascade], + persist: false, }) rules = new Collection(this) diff --git a/packages/tax/src/services/tax-module-service.ts b/packages/tax/src/services/tax-module-service.ts index cd2a7428ba65b..caab1eac0be00 100644 --- a/packages/tax/src/services/tax-module-service.ts +++ b/packages/tax/src/services/tax-module-service.ts @@ -22,6 +22,7 @@ import { import { TaxProvider, TaxRate, TaxRegion, TaxRateRule } from "@models" import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" import { TaxRegionDTO } from "@medusajs/types" +import { EntityManager } from "@mikro-orm/postgresql" type InjectedDependencies = { baseRepository: DAL.RepositoryService @@ -194,9 +195,85 @@ export default class TaxModuleService< ? { id: idOrSelector } : idOrSelector + if (data.rules) { + await this.setTaxRateRulesForTaxRates( + idOrSelector, + data.rules, + data.updated_by, + sharedContext + ) + + delete data.rules + } + return await this.taxRateService_.update({ selector, data }, sharedContext) } + private async setTaxRateRulesForTaxRates( + idOrSelector: string | string[] | TaxTypes.FilterableTaxRateProps, + rules: Omit[], + createdBy?: string, + sharedContext: Context = {} + ) { + const selector = + Array.isArray(idOrSelector) || isString(idOrSelector) + ? { id: idOrSelector } + : idOrSelector + + await this.taxRateRuleService_.softDelete( + { tax_rate: selector }, + sharedContext + ) + + // TODO: this is a temporary solution seems like mikro-orm doesn't persist + // the soft delete which results in the creation below breaking the unique + // constraint + await this.taxRateRuleService_.list( + { tax_rate: selector }, + { select: ["id"] }, + sharedContext + ) + + if (rules.length === 0) { + return + } + + const rateIds = await this.getTaxRateIdsFromSelector(idOrSelector) + const toCreate = rateIds + .map((id) => { + return rules.map((r) => { + return { + ...r, + created_by: createdBy, + tax_rate_id: id, + } + }) + }) + .flat() + + return await this.createTaxRateRules(toCreate, sharedContext) + } + + private async getTaxRateIdsFromSelector( + idOrSelector: string | string[] | TaxTypes.FilterableTaxRateProps, + sharedContext: Context = {} + ) { + if (Array.isArray(idOrSelector)) { + return idOrSelector + } + + if (isString(idOrSelector)) { + return [idOrSelector] + } + + const rates = await this.taxRateService_.list( + idOrSelector, + { select: ["id"] }, + sharedContext + ) + return rates.map((r) => r.id) + } + async upsert( data: TaxTypes.UpsertTaxRateDTO[], sharedContext?: Context diff --git a/packages/types/src/tax/common.ts b/packages/types/src/tax/common.ts index 6a11cc3169972..b9c2c72567f7c 100644 --- a/packages/types/src/tax/common.ts +++ b/packages/types/src/tax/common.ts @@ -60,7 +60,7 @@ export interface TaxProviderDTO { export interface FilterableTaxRateProps extends BaseFilterable { id?: string | string[] - + tax_region_id?: string | string[] rate?: number | number[] | OperatorMap code?: string | string[] | OperatorMap name?: string | string[] | OperatorMap diff --git a/packages/types/src/tax/mutations.ts b/packages/types/src/tax/mutations.ts index 388c60e62103e..2a30bbdfbac43 100644 --- a/packages/types/src/tax/mutations.ts +++ b/packages/types/src/tax/mutations.ts @@ -23,8 +23,10 @@ export interface UpdateTaxRateDTO { rate?: number | null code?: string | null name?: string + rules?: Omit[] is_default?: boolean - created_by?: string + is_combinable?: boolean + updated_by?: string metadata?: Record } diff --git a/packages/types/src/tax/service.ts b/packages/types/src/tax/service.ts index 6010bdc55d17c..691786dea29d7 100644 --- a/packages/types/src/tax/service.ts +++ b/packages/types/src/tax/service.ts @@ -110,11 +110,11 @@ export interface ITaxModuleService extends IModuleService { ): Promise deleteTaxRateRules( - taxRateRulePair: { tax_rate_id: string; reference_id: string }, + taxRateRuleId: string, sharedContext?: Context ): Promise deleteTaxRateRules( - taxRateRulePair: { tax_rate_id: string; reference_id: string }[], + taxRateRuleIds: string[], sharedContext?: Context ): Promise @@ -153,4 +153,10 @@ export interface ITaxModuleService extends IModuleService { config?: SoftDeleteReturn, sharedContext?: Context ): Promise | void> + + restoreTaxRateRules( + taxRateRuleIds: string[], + config?: RestoreReturn, + sharedContext?: Context + ): Promise | void> }